04. Методи. Блокове. Анонимни функции

04. Методи. Блокове. Анонимни функции

04. Методи. Блокове. Анонимни функции

20 октомври 2014

Днес

Втора задача

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

Откриваме нова рубрика

Ще има кал

Възможно е да засядаме понякога

Ще откриваме красиви места

Code Spelunking

Първа задача

Преглед и коментари над решенията.

Решения

the ugly

...the ugly 01

def sum_two_series(first, second, index)
        for i in 1 .. index - 1
                first += second
                first, second = second, first
        end
        first
end
def series(name, index)
        if name == 'fibonacci' then sumtwo_series(1, 1, index) end
        if name == 'lucas' then sumtwo_series(2, 1, index) end
        if name=='summed' then sumtwo_series(1, 1, index)+sumtwo_series(2, 1,index) end
end

...the ugly 02

def series(type, n)
  if   type === 'fibonacci'
    (0..n).inject([1, 0]){|(sum, item)| [item, sum + item]}[0]
  elsif type === 'lucas'
    (1..n).inject([-1, 2]){|(sum, item)| [item, sum + item]}[0]
  else
    series('fibonacci', n) + series('lucas', n)
  end
end

...the ugly 03

def series(series_calculate , number)
  fibonacci = (1..number).inject([0 , 1]) { |(a , b) , _| [b , a + b]}[0]
  lucas = (1..number).inject([2 , 1]) { |(a , b) , _| [b , a + b] }[0]
  if series_calculate == "fibonacci" then fibonacci
  elsif series_calculate == "lucas" then lucas
  elsif series_calculate == "sum" then fibonacci + lucas
  end
end

...the ugly 04

def fibonacci(n)
        return 1 if n == 1 or n == 2
        return fibonacci(n-1) + fibonacci(n-2)
end

def lucas(n)
        return 2 if n == 1
        return 1 if n == 2
        return lucas(n-1) + lucas(n-2)
end

def summed(n)
        fibonacci(n) + lucas(n)
end

def series(name, n)
puts send(name, n)
end

...the bad

правилният mindset при оптимизиране

Решения

the good

...the good 01

def series(sequence, index)
  case sequence
    when 'fibonacci' then fibonacci(index)
    when 'lucas' then lucas(index)
    else fibonacci(index) + lucas(index)
  end
end

def fibonacci(index)
  case index
    when 1, 2 then 1
    else fibonacci(index - 1) + fibonacci(index - 2)
  end
end

def lucas(index)
  case index
    when 1 then 2
    when 2 then 1
    else lucas(index - 1) + lucas(index - 2)
  end
end

...the good 02

# http://mathforum.org/library/drmath/view/51448.html
PHI = (1 + Math.sqrt(5)) / 2

def fibonacci(n)
  ((PHI**n - (-PHI)**(-n)) / Math.sqrt(5)).round
end

def lucas(n)
  (PHI**(n - 1) + (1 - PHI)**(n - 1)).round
end

def series(name, n)
  case name
    when "fibonacci" then fibonacci(n)
    when "lucas" then lucas(n)
    when "summed" then lucas(n) + fibonacci(n)
  end
end

...the good 03

class SequenceMemberGenerator
  def initialize(first_element, second_element)
    @sequence_store = Hash.new do |sequence, index|
      sequence[index] = sequence[index - 1] + sequence[index - 2]
    end
    @sequence_store[1] = first_element
    @sequence_store[2] = second_element
  end

  def [](index)
    @sequence_store[index]
  end
end

class Fibonacci
  def initialize
    @@fibonacci_sequence ||= SequenceMemberGenerator.new(1, 1)
  end

  def [](index)
    @@fibonacci_sequence[index]
  end
end

class Lucas
  def initialize
    @@lucas_sequence ||= SequenceMemberGenerator.new(2, 1)
  end

  def [](index)
    @@lucas_sequence[index]
  end
end

class Summed
  def [](index)
    Fibonacci.new[index] + Lucas.new[index]
  end
end

def series(name, index)
  if name == 'fibonacci'
    Fibonacci.new[index]
  elsif name == 'lucas'
    Lucas.new[index]
  else
    Summed.new[index]
  end
end

Предизвикателства

разбирате ги погрешно

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

Методи

дефиниране

Дефинирането става с ключовата дума def. Резултатът от функцията е последният оценен израз, ако няма return някъде по-рано.

def factorial(n)
  if n == 1
    1
  else
    factorial(n - 1) * n
  end
end

В Ruby няма tail recursion оптимизация. Този код яде стек.

Методи

отгоре-отгоре

Методи в съществуващи класове

Ще ви трябва за бъдещи домашни

За да добавите метод в съществуващ клас, например Array, просто "отваряте" класа и дефинирате метода:

class Array
  def fourty_second
    self[41]
  end
end

list     = []
list[41] = 'The Universe'

list.fourty_second # "The Universe"

Методи

return

Можете да излезете от функция с return:

def includes?(array, element)
  array.each do |item|
    return true if item == element
  end
  false
end

Разбира се, такава функция е излишна. Може да ползвате array.include?(element).

Методи

стойности по подразбиране

Параметрите в Ruby могат да имат стойности по подразбиране:

def order(drink, size = 'large')
  "A #{size} #{drink}, please!"
end

order 'tea'             # "A large tea, please!"
order 'coffee', 'small' # "A small coffee, please!"

Методи

стойности по подразбиране (2)

Методи

променлив брой аргументи

Методите в ruby могат да вземат променлив брой аргументи. Параметърът се означава със * и при извикване на функцията съдържа списък от аргументите.

def say_hi(name, *drinks)
  "Hi, I am #{name} and I enjoy: #{drinks.join(', ')}"
end

say_hi 'Mityo', 'coffee', 'tea', 'water' # "Hi, I am Mityo and I enjoy: coffee, tea, water"

Методи

променлив брой аргументи

Параметърът за променлив брой аргументи може да е на всяка позиция в дефиницията:

def something(*a, b, c)
end

def something(a, *b, c)
end

Очевидно, може да има само един такъв параметър във функция.

Методи

...и техните приятели, хешовете

Когато последният аргумент е хеш, може да изтървете фигурните скоби около него. Долните редове правят едно и също:

def order(drink, preferences)
end

order('Latte', {:size => 'grande', :syrup => 'hazelnut'})
order 'Latte', {:size => 'grande', :syrup => 'hazelnut'}
order 'Latte', :size => 'grande', :syrup => 'hazelnut'
order 'Latte', size: 'grande', syrup: 'hazelnut'

Така Ruby симулира извикване на функция с наименовани аргументи. Последният ред работи при версия 1.9+.

Методи

...и хешове, отново

Често ще видите код в този вид:

def order(drink, preferences = {})
end

order 'Latte'
order 'Latte', size: 'grande', syrup: 'hazelnut'

Така preferences е незадължителен и няма нужда да го подавате, ако нямате предпочитания.

Псевдо-keyword arguments

недостатъци

Този "трик" с хешовете се ползва много, понякога прекалено много. Той има и ред недостатъци:

Истински keyword arguments

Горните недостатъци и нуждата водят до появата на истински keyword arguments в Ruby 2.0.

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

order 'Latte'                       # "You ordered a grande Latte"
order 'Latte', syrup: 'hazelnut'    # "You ordered a grande Latte with a hazelnut syrup"
order 'Latte', filling: 'chocolate' # error: ArgumentError

Задължителни 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

Истински keyword arguments

предимства

Методи

предикати

Името на метод може да завършва на ?. Това се ползва за методи, които връщат лъжа или истина (предикати):

def even?(n)
  n % 2 == 0
end

even? 2
even? 3

Това е само конвенция.

Методи

две версии

Името на метод може да завършва на !. Това се ползва, когато методът има две версии с различно поведение:

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

numbers.sort   # връща нов списък
numbers.sort!  # променя списъка на място

В случая, "по-опасният" метод завършва на удивителна.

Ако имате само една версия на метод с такова име, не слагайте удивителна.

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

ламбди

Анонимни функции в 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

Блокове

стойности

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.call *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.call 2 # 4

Въпроси