05. Proc обекти. Паралелно присвояване. Основи на класове. Enumerable

05. Proc обекти. Паралелно присвояване. Основи на класове. Enumerable

05. Proc обекти. Паралелно присвояване. Основи на класове. Enumerable

22 октомври 2014

Днес

Втора задача

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

Code Spelunking

Въпрос 1

Ако имаме следната дефиниция на речник: numbers = {:one => :eins, :two => :zwei}, какво ще върне всеки един от следните три реда код:

numbers[:eins]                  # => ?
numbers.fetch(:two, :something) # => ?
numbers.fetch(:four)            # => ?
  • nil
  • :zwei
  • KeyError

Въпрос 2

Какво прави def, ако не се намираме в class?

  • def винаги дефинира метод в някакъв клас
  • Ако def не е в дефиниция на клас, отива като private метод на Object

Въпрос 3

Какво е *b в този контекст? Колко аргумента можем да подаваме на тази функция?

def something(a, *b, c)
end

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

Въпрос 4

Какво са drink, size и syrup в този контекст, има ли някаква разлика между тях и ако да, каква?

def order(drink:, size: 'grande', syrup: nil)
end

drink е задължителен аргумент

size и syrup имат стойности по подразбиране.

Въпрос 5

Какви начини знаете за извикване на следната анонимна функция?

pow = lambda { |a, b| a**b }
  • pow.call 2, 3
  • pow[2, 3]
  • pow.(2, 3)

Въпрос 6

Каква е конвенцията за употреба на ! в края на името на метод?

Символът ! се поставя в края на метод, ако съществуват две версии на метода, с еднакво име и с разлика в поведението. Обикновено удивителната получава "по-опасният" метод, каквото и да означава това.

Метод с ! в края не е задължително метод, който мутира обект, както и има методи, които мутират обекти, но не са с удивителна в края (например, Array#pop).

Анонимни функции (преговор)

ламбди

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

Блокове (преговор)

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

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

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

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 args работят и в ламбди и блокове

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

Proc.new

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

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

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

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

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

Разлики между 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)

Ruby и ООП

Класове

прост пример

Дефинират се с class. Методите, дефинирани в тялото на класа, стават методи на инстанциите му. Инстанцират се с ИмеНаКласа.new.

class Bacon
  def chunky?
    'yes, of course!'
  end
end

bacon = Bacon.new
bacon.chunky?      # "yes, of course!"

Класове

полета

Полетата (още: instance variables) имат представка @.

class Vector
  def initialize(x, y)
    @x = x
    @y = y
  end

  def length
    (@x * @x + @y * @y)**0.5
  end
end

vector = Vector.new 2.0, 3.0
vector.length()     # 3.605551275463989
vector.length       # 3.605551275463989

Класове

полета (2)

По подразбиране имат стойност nil.

class Person
  def soul
    @nothingness
  end
end

person = Person.new
person.soul      # nil

Класове

викане на методи

В метод може да извикате друг със self.име_на_метод или просто име_на_метод:

class Person
  def initialize(name) @name = name                end
  def say_hi()         puts "My name is #{@name}!" end
  def sound_smart()    puts "1101000 1101001"      end

  def talk
    self.say_hi
    sound_smart
  end
end

mel = Person.new 'Mel'
mel.talk

Такова подравняване на методи е гадно, но пък се събира в слайд.

Класове

self

В методите на класа, self е референция към обекта, на който е извикан методът. Като this в Java или C++.

class Person
  def me
    self
  end
end

person = Person.new
person           # #<Person:0x425ef2f8>
person.me        # #<Person:0x425ef2f8>
person.me.me     # #<Person:0x425ef2f8>

Атрибути

Полетата не са публично достъпни. Може да ги достигнете само чрез метод.

class Person
  def initialize(age)
    @age = age
  end

  def age
    @age
  end

  def set_age(age)
    @age = age
  end
end

person = Person.new(33)
person.age          # 33
person.set_age 20
person.age          # 20

Атрибути

setter-и

Разбира се, set_age е гадно име на метод. Може и по-добре:

class Person
  def age
    @age
  end

  def age=(value)
    @age = value
  end
end

person = Person.new
person.age = 33  # Същото като person.age=(33)

person.age       # 33

Атрибути

attr_accessor

Последното е досадно за писане. Затова:

class Person
  attr_accessor :age
end

person = Person.new
person.age = 33

person.age # 33

Атрибути

другите макроси

Ако ви трябва само getter или setter, може така:

class Person
  attr_reader :name
  attr_writer :grade
  attr_accessor :age, :height

  attr :address # като attr_reader
end

Атрибути

какво е attr_accessor?

attr_accessor е метод, който генерира два метода — #foo и #foo=. Достъпен е в дефинициите на класове. Неформален термин за такива методи е "class macro".

Има ги в изобилие.

Атрибути

Meyer's Uniform Access Principle

Обърнете внимание, че следните два реда правят едно и също:

person.age()
person.age

Няма разлика между достъпване на атрибут и извикване на метод, който го изчислява. Това се нарича Uniform Access Principle и като цяло е хубаво нещо.

Конвенции

напомняне

В Ruby важат следните конвенции.

"Отваряне" на класове

Във всеки момент може да "отворите" клас и да му добавите методи. Това вече дори сте го правили.

class Person
  def name
    'River'
  end
end

class Person
  def say_hi
    "Hi, I am #{name}."
  end
end

Person.new.say_hi # "Hi, I am River."
Person.new.name   # "River"

Повторно дефиниране на метод

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

class Someone
  def name
    'Tom Baker'
  end

  def name
    'Colin Baker'
  end
end

Someone.new.name # => 'Colin Baker'

Тялото на класа

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

Тялото на класа е напълно изпълним код:

class Something
  a = 1
  b = 2
  a + b # 3
end

Тялото на класа (2)

Понякога дори е полезно:

class Object
  if RUBY_VERSION <= '1.8.6'
    def tap
      yield self
      self
    end
  end
end

Въпроси до тук?

Сега е моментът да ги зададете :-)

Object#methods

Ако извикате #methods на нещо, ще получите масив от символи с имената на методите му.

Помните ли Array#-?

class Person
  def foo() end
  def bar() end
end

Person.new.methods - Object.new.methods # [:foo, :bar]

Предефиниране на оператори

Много интуитивно.

class Vector
  attr_accessor :x, :y

  def initialize(x, y)
    @x, @y = x, y
  end

  def +(other)
    Vector.new(x + other.x, y + other.y)
  end

  def inspect
    "Vector.new(#@x, #@y)"
  end
end

Vector.new(1, 5) + Vector.new(3, 10) # Vector.new(4, 15)

Предефинируеми оператори

Ето и всички оператори, които можете да предефинирате:

Забележки относно предефиниране на оператори

private

class Person
  def say_hi
    "Hello! I am #{name}"
  end

  private

  def name
    'the Doctor'
  end
end

person = Person.new
person.say_hi     # "Hello! I am the Doctor"
person.name       # error: NoMethodError

private (2)

Ако един метод е private, не може да го викате с явен получател. Дори със self.

class Person
  def say_hi
    "Hello! I am #{self.name}"
  end

  private

  def name
    'the Doctor'
  end
end

person = Person.new
person.say_hi     # error: NoMethodError

protected

Object#tap

Object#tap извиква блока със себе си и връща обекта, на който е извикан.

array = [].tap do |items|
  items              # []
  items.equal? array # false

  items << 'foo'
  'other thing'
end

array                # ["foo"]

Object#tap

за debug-ване

Имате следния код

(1..10).select { |x| x.odd? }.map { |x| x**2 }

Искате да видите какво остава след select-а:

(1..10).select { |x| x.odd? }.tap { |x| p x }.map { |x| x**2 }

Object#tap

друг пример

class Array
  def occurences_count
    Hash.new(0).tap do |result|
      each { |item| result[item] += 1 }
    end
  end
end

[nil, 1, 2, 1, :a, 'X', 1, nil].occurences_count # {nil=>2, 1=>3, 2=>1, :a=>1, "X"=>1}

Symbol#to_proc

Следните два реда са (почти) еквивалентни:

name = ->(object) { object.name }
name = :name.to_proc

Когато подавате блок на метод с &block, Ruby извиква #to_proc, ако block не е ламбда или proc.

Съответно, следните два реда са еквивалентни

%w(foo plugh larodi).map { |s| s.length } # [3, 5, 6]
%w(foo plugh larodi).map(&:length)        # [3, 5, 6]

Symbol#to_proc

с повече от един аргумент

Всъщност, малко по-сложно е:

block = ->(obj, *args) { obj.method_name *args }
block = :method_name.to_proc

Това значи, че може да направите така:

[{a: 1}, {b: 2}, {c: 3}].reduce { |a, b| a.merge b } # {:a=>1, :b=>2, :c=>3}
[{a: 1}, {b: 2}, {c: 3}].reduce(&:merge)             # {:a=>1, :b=>2, :c=>3}

Или дори:

[1, 2, 3, 4].reduce { |sum, b| sum + b } # 10
[1, 2, 3, 4].reduce(&:+)             # 10

Symbol#to_proc

Употреба

['Foo', :bar, 3].map(&:to_s).map(&:upcase)

Symbol#to_proc

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

class Symbol
  def to_proc
    # ...?
  end
end

Object#send

3.send :+, 4 # 7

Symbol#to_proc

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

class Symbol
  def to_proc
    ->(object, *args) { object.public_send self, *args }
  end
end

pry

$ gem install pry

Модули

Модулите в Ruby имат няколко предназначения:

Днес ще разгледаме последното.

Модули

като колекция от методи

Модулите в Ruby просто съдържат методи. Дефинират се подобно на класове:

module UselessStuff
  def almost_pi
    3.1415
  end

  def almost_e
    2.71
  end
end

Модули

миксиране

Модулите могат да се "миксират" с клас. Тогава той получава всички методи на модула като инстанционни методи.

module UselessStuff
  def almost_pi
    3.1415
  end
end

class Something
  include UselessStuff
end

Something.new.almost_pi # 3.1415

Модули

self

В метод на модула, self е инстанцията от класа, в който модулът е бил миксиран и на която е извикан даденият метод.

module Introducable
  def introduction
    "Hello, I am #{name}"
  end
end

class Person
  include Introducable
  def name() 'The Doctor' end
end

doctor = Person.new
doctor.introduction # "Hello, I am The Doctor"

Модули

приоритет на методите

Методите на класа имат приоритет пред методите на модула.

module Includeable
  def name() 'Module' end
end

class Something
  def name() 'Class' end
  include Includeable
end

Something.new.name # "Class"

Модули

приоритет на методите (2)

Ако два модула дефинират един и същи метод, ползва се методът от последно миксирания модул:

module Chunky
  def name() 'chunky' end
end

module Bacon
  def name() 'bacon' end
end

class Something
  include Chunky
  include Bacon
end

Something.new.name # "bacon"

Модули

приоритет на методите (3)

Просто за информация: методите на mixin-ите имат приоритет пред тези на родителя.

Всичко това е свързано с нещо, наречено ancestor chain, за което ще си говорим следващия път.

Enumerable

#collect, #find_all и #inject

Помните ли тези методи и техните синоними?

[1, 2, 3, 4, 5].select(&:odd?)     # [1, 3, 5]
%w(foo plugh barney).map(&:length) # [3, 5, 6]
[1, 2, 3, 4, 5].reduce(&:*)        # 120

Те са имплементирани в Enumerable, а не в Array.

Всяка колекция в Ruby ги има.

Други методи на Enumerable

all?        any?          chunk       collect          collect_concat
count       cycle         detect      drop             drop_while
each_cons   each_entry    each_slice  each_with_index  each_with_object
entries     find          find_all    find_index       first
flat_map    grep          group_by    include?         inject
lazy        map           max         max_by           member?
min         min_by        minmax      minmax_by        none?
one?        partition     reduce      reject           reverse_each
select      slice_before  sort        sort_by          take
take_while  to_a          zip 

По-нататък ще видите как използваме Enumerable, за да генерираме тази таблица.

Hash ← Enumerable

Хешовете също са Enumerable:

hash = {2 => 3, 4 => 5}

hash.to_a                                 # [[2, 3], [4, 5]]
hash.map { |p| p[0] + p[1] }              # [5, 9]
hash.map { |k, v| k + v }                 # [5, 9]
hash.reduce(0) { |s, p| s + p[0] * p[1] } # 26

Hash ← Enumerable

бележка под линия

Някои от Enumerable методите в Hash са предефинирани.

hash = {2 => 3, 4 => 5, 6 => 7, 8 => 9}

hash.select { |k, v| v > 6 }      # {6=>7, 8=>9}
hash.to_a.select { |k, v| v > 6 } # [[6, 7], [8, 9]]

Enumerable#select връща списък, но Hash#select връща хеш.

Други неща за обхождане

include Enumerable

или как да го ползваме за наши класове

include Enumerable

пример

class FibonacciNumbers
  include Enumerable

  def initialize(limit)
    @limit = limit
  end

  def each
    current, previous = 1, 0

    while current < @limit
      yield current
      current, previous = current + previous, current
    end
  end
end

FibonacciNumbers.new(100).to_a # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

include Enumerable

пример

class StepRange
  include Enumerable

  def initialize(first, last, step)
    @first, @last, @step = first, last, step
  end

  def each
    @first.step(@last, @step) { |n| yield n }
  end
end

StepRange.new(1, 10, 2).select { |n| n > 5 } # [7, 9]

Въпроси