04. Блокове Reloaded

04. Блокове Reloaded

04. Блокове Reloaded

21 октомври 2013

Днес

Трето предизвикателство

Keyword arguments

преговор

Пример от предишни лекции

def order(drink:, size: 'grande', syrup: nil)
  message = "You ordered a #{size} #{drink}"
  message << " with a #{syrup} syrup" if syrup
  message
end

order drink: 'Latte'                    # You ordered a grande Latte
order syrup: 'hazelnut', drink: 'Latte' # You ordered a grande Latte with a hazelnut syrup
order                                   # error: ArgumentError: missing keyword: drink

**kwargs

def order(drink:, size: 'grande', **options)
  message = "You ordered a #{size} #{drink}"
  message << " with these options: #{options.inspect}" unless options.empty?
  message
end

order drink: 'Latte'                    # You ordered a grande Latte
order syrup: 'hazelnut', drink: 'Latte' # You ordered a grande Latte with these options: {:syrup=>"hazelnut"}
order                                   # error: ArgumentError: missing keyword: drink

Анонимни функции

ламбди

Анонимни функции в Ruby се дефинират с lambda. Имат три начина на извикване:

pow = lambda { |a, b| a ** b }

pow.call 2, 3
pow[2, 3]
pow.(2, 3)

За нещастие, не може да извиквате така: double(2). Това е несъвместимо с изтърваването на скобите при извикването на метод.

Анонимни функции

ламбди (2)

Може и така:

double = lambda do |x|
  x * 2
end

Важи стандартната конвенция за { } и do/end.

Анонимни функции

ламбди (3)

От 1.9 има по-симпатичен синтаксис за ламбди:

say_hi = lambda { puts 'Hi there!' }
double = lambda { |x| x * 2 }
divide = lambda { |a, b| a / b }

say_hi = -> { puts 'Hi there' }
double = ->(x) { x * 2 }
divide = ->(a, b) { a / b }

Блокове

Всеки метод може да приеме допълнителен аргумент, който е "анонимна функция". Може да го извикате от метода с yield:

def twice
  yield
  yield
end

twice { puts 'Ruby rocks!' }

Блокове

аргументи

Блокът може да приема аргументи:

def sequence(first, last, step)
  current = first
  while current < last
    yield current
    current += step
  end
end

sequence(1, 10, 2) { |n| puts n }
# Извежда 1, 3, 5, 7, 9

Keyword args работят и в ламбди и блокове

->(**opts) { p [opts] }.call foo: 'bar', larodi: 'baz'

Блокове

стойности

yield се оценява до стойността на блока:

def calculate
  result = yield(2)
  "The result for 2 is #{result}"
end

calculate { |x| x ** 2 } # "The result for 2 is 4"

Блокове

един пример

Или как можем да напишем filter:

def filter(array)
  result = []
  array.each do |item|
    result << item if yield item
  end
  result
end

filter([1, 2, 3, 4, 5]) { |n| n.odd? } # [1, 3, 5]

Разбира се, такъв метод в Ruby вече съществува – Enumerable#select.

Блокове

#block_given?

block_given? ще ви каже дали методът е извикан с блок:

def i_can_haz_block
  if block_given?
    puts 'yes'
  else
    puts 'no'
  end
end

i_can_haz_block                  # no
i_can_haz_block { 'something' }  # yes

Блокове

& при извикване на метод

Ако имате ламбда, която искате да подадете като блок, може да ползвате &:

is_odd = lambda { |n| n.odd? }

filter([1, 2, 3, 4, 5], &is_odd)
filter([1, 2, 3, 4, 5]) { |n| n.odd? }

Горните са (почти) еквиваленти. Има малка разлика в някои други случаи.

Блокове

в сигнатурата

Ако искате да вземете блока като обект, има начин:

def invoke_with(*args, &block)
  block.(*args)
end

invoke_with(1, 2) { |a, b| puts a + b }

Блокове

в сигнатурата (2)

Може и така:

def make_block(&block)
  block
end

doubler = make_block { |n| n * 2 }
doubler.(2) # 4

Proc.new

където става странно

В Ruby има два вида анонимни функции. Другият е Proc.

double = Proc.new { |x| x * 2 }

double.call(2)
double[2]
double.(2)

Дотук е същото, но има разлики при извикване.

Разлики между Proc.new и lambda

f =Proc.new { |x, y| p x, y }lambda { |x, y| p x, y }
f.call(1)1 nilArgumentError
f.call(1, 2)1 21 2
f.call(1, 2, 3)1 2ArgumentError
f.call([1, 2])1 2ArgumentError
f.call(*[1, 2])1 21 2

Блокове

...и една особеност

Ако първият аргумент на функция е хеш, трябва да изтървете скобите на хеша, ако изтървете скобите на метода.

# Валиден код
order drink: 'latte', size: 'grande'
order({drink: 'latte', size: 'grande'})

# Невалиден код
order {drink: 'latte', size: 'grande'}

Във втория случай, Ruby си мисли, че му подавате блок.

Блокове

в Ruby като цяло

Функционални закачки

Стандартните функционални неща:

numbers  = [-9, -4, -1, 0, 1, 4, 9]

positive = numbers.select { |n| n >= 0 }
even     = numbers.reject { |n| n.odd? }
squares  = numbers.collect { |n| n ** 2 }
roots    = numbers.select { |n| n > 0 }.collect { |n| n ** 0.5 }

Функционални закачки

синоними

#select и #collect имат синоними #find_all и #map:

numbers  = [-9, -4, -1, 0, 1, 4, 9]

squares  = numbers.map { |n| n ** 2 }
positive = numbers.find_all { |n| n >= 0 }

В Ruby подобни синоними се срещат често.

#reduce

ако разбирате това, значи сте ОК

#reduce свежда списък до единична стойност с някаква операция:

numbers = [1, 2, 3, 4, 5]

numbers.reduce(0) { |a, b| a + b }
numbers.reduce(1) { |a, b| a * b }

numbers.reduce { |a, b| a + b }
numbers.reduce { |a, b| "#{a}, #{b}" }

#reduce и #inject са синоними. Ползвайте първото.

#reduce

често срещана грозотия

Имаме списък с думи. Искаме да получим хеш от вида {дума => дължина на думата}:

words = %w[chunky bacon is awesome]
words.reduce({}) { |hash, word| hash[word] = word.length; hash }

Това е неидиоматично използване на #reduce, но е интересен пример.

#reduce

примерна имплементация

def reduce(array, initial = nil)
  remaining = array.dup
  buffer    = initial || remaining.shift

  until remaining.empty?
    buffer = yield buffer, remaining.shift
  end

  buffer
end

reduce([1, 2, 3, 4]) { |a, b| a + b }
reduce([1, 2, 3, 4], 0) { |a, b| a + b }

#reduce

още по-примерна имплементация

За забавлението. Неяснотите — следващия път.

class Array
  def reduce(initial = nil)
    remaining = dup
    buffer    = initial || remaining.shift

    until remaining.empty?
      buffer = yield buffer, remaining.shift
    end

    buffer
  end
end

Присвояване

Паралелно присвояване

прост пример

a, b = 1, 2
a              # 1
b              # 2

a, b = b, a
a              # 2
b              # 1

Има няколко различни случая, които ще разгледаме.

Паралелно присвояване

присвояване на една променлива

a = 1, 2, 3
a # [1, 2, 3]

Практически същото като a = [1, 2, 3]

Паралелно присвояване

разпакетиране на дясната страна

a, b = [1, 2, 3]
a # 1
b # 2

a, b = 1, 2, 3
a # 1
b # 2

Паралелно присвояване

със splat аргументи

head, *tail = [1, 2, 3]
head   # 1
tail   # [2, 3]

first, *middle, last = 1, 2, 3, 4
first  # 1
middle # [2, 3]
last   # 4

Паралелно присвояване

splat аргументи отдясно

first, *middle, last = 1, [2, 3, 4]
first  # 1
middle # []
last   # [2, 3, 4]

first, *middle, last = 1, *[2, 3, 4]
first  # 1
middle # [2, 3]
last   # 4

Вложено присвояване

head, (title, body) = [1, [2, 3]]
head   # 1
title  # 2
body   # 3

Вложено присвояване и splat-ове

head, (title, *sentences) = 1, [2, 3, 4, 5, 6]
head      # 1
title     # 2
sentences # [3, 4, 5, 6]

Ред на оценка

Бележка за реда на оценка при присвояване — първо отдясно, след това отляво:

x = 0
a, b, c = x, (x += 1), (x += 1)
x # 2
a # 0
b # 1
c # 2

Променливата _

Променливата _

Може да ползвате едно име само един път, когато то се среща в списък с параметри на метод, блок и прочее.

Proc.new { |a, b, a| } # SyntaxError: duplicated argument name
Proc.new { |_, b, _| } # => #<Proc:0x007f818af68de0@(irb):23>

Горното важи не само за блокове, но и за методи.

Присвояване в Ruby

Къде важат тези правила?

[[1, [2, 3]], [4, [5, 6]], [7, [8, 9]]].each do |a, (b, c)|
  puts "#{a}, #{b}, #{c}"
end
# 1, 2, 3
# 4, 5, 6
# 7, 8, 9

Присвояване в Ruby

Имате ли въпроси по тази тема?

Нещо особено в този код?

auto_link("Some text here.", options = {link_emails: false}, sanitize: true)

Въпроси