04. Procs. Класове. Модули. Enumerable.

04. Procs. Класове. Модули. Enumerable.

04. Procs. Класове. Модули. Enumerable.

19 октомври 2015

Днес

С-бира-не

сряда

Втора задача

няколко думи

Втора задача

...и "плитките" копия

dup прави "плитко" копие

immutable методите също (+, drop, ...)

snake = [[2, 3], [2, 4], [2, 5]] # => [[2, 3], [2, 4], [2, 5]]
grown_snake = snake + [[2, 6]]   # => [[2, 3], [2, 4], [2, 5], [2, 6]]

snake.equal?(snake)              # => true
snake.equal?(grown_snake)        # => false

snake[0]                         # => [2, 3]
grown_snake[0]                   # => [2, 3]
snake[0].equal?(grown_snake[0])  # => true

Трета задача

"тия не се спряха, бе..."

Въпрос 1

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

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

Въпрос 2

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

def something(a, *b, c)
end
  • Параметър, който при извикване на функцията ще съдържа списък от аргументите, които може да са променлив брой
  • Поне 2 аргумента

Въпрос 3

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

def order(drink:, size: 'grande', syrup: nil)
end
  • drink е задължителен аргумент
  • size и syrup имат стойности по подразбиране
  • Всички са keyword arguments

Въпрос 4

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

`a**b` или `a ** b`?

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

Въпрос 5

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

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

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

# Синтаксис
say_hi = lambda { puts 'Hi there!' }
double = lambda { |x| x * 2 }
pow = lambda { |a, b| a**b }

# Симпатичен синтаксис (1.9+)
say_hi = -> { puts 'Hi there' }
double = ->(x) { x * 2 }
pow = ->(a, b) { a**b }

# Извикване
pow.call 2, 3
pow[2, 3]
pow.(2, 3)

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

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.call(*args)
end

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

Блокове (допълнение)

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

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

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

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

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

Keyword arguments (допълнение)

Работят и в ламбди, и в блокове

->(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, но има някои разлики при извикване.

lambda е специален вид Proc

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

Анонимните функции са обекти тип `Proc`, но с вдигнат специален флаг

lambda { |n| n**2 }   # => #<Proc:0x002ab59a7f7db0@-:1 (lambda)>
Proc.new { |n| n**2 } # => #<Proc:0x002ab59a7f7888@-: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

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

return

def return_in_lambda
  -> { return 42 }.call
  'Hello world'
end

def return_in_proc
  Proc.new { return 42 }.call
  'Hello world'
end

return_in_lambda # => "Hello world"
return_in_proc   # => 42

Блокове, Proc-ове и ламбди

обобщение

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

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

Блокове

в Ruby като цяло

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

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

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

positive = numbers.select { |n| n >= 0 }
# => [0, 1, 4, 9]
even = numbers.reject { |n| n.odd? }
# => [-4, 0, 4]
squares = numbers.map { |n| n**2 }
# => [81, 16, 1, 0, 1, 16, 81]
roots = numbers.select { |n| n > 0 }.map { |n| n**0.5 }
# => [1.0, 2.0, 3.0]

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

синоними

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

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

squares = numbers.collect { |n| n**2 }
# => [81, 16, 1, 0, 1, 16, 81]
positive = numbers.find_all { |n| n >= 0 }
# => [0, 1, 4, 9]

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

#reduce

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

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

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

numbers.reduce(0) { |a, b| a + b }
# => 15
numbers.reduce(1) { |a, b| a * b }
# => 120

numbers.reduce { |a, b| a + b }
# => 15
numbers.reduce { |a, b| "#{a}, #{b}" }
# => "1, 2, 3, 4, 5"

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

#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

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

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

Ruby и ООП

Класове

прост пример

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

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

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

Класове

конструктури

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

Класове

полета

Полетата (още: 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:0x002b39374de6a8>
person.me        # => #<Person:0x002b39374de6a8>
person.me.me     # => #<Person:0x002b39374de6a8>

Атрибути

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

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

Атрибути

какво е `attr_accessor`?

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

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

Атрибути

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

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

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

  attr :address # като attr_reader
end

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

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

Атрибути

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

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

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

Модули

Модулите в 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 { |n| n.odd? }           # => [1, 3, 5]
%w(foo plugh barney).map { |word| word.length } # => [3, 5, 6]
[1, 2, 3, 4, 5].reduce { |a, b| a * b }         # => 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]

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

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

Масив от yield-ваща функция? (1)

def each_digit
  yield 0; yield 1; yield 2; yield 3; yield 4
  yield 5; yield 6; yield 7; yield 8; yield 9
end

Как да вземем yield-натите стойности в масив?

Масив от yield-ваща функция? (2)

result = []
each_digit do |n|
  result << n
end

Така става, ама е... какъв беше техническият термин?

"грозно"

Масив от yield-ваща функция? (3)

Ето как може да стане:

result = enum_for(:each_digit).to_a

Малко е шантаво, но пък много удобно.

Масив от yield-ваща функция? (3)

Резултатът от enum_for имплементира Enumerable:

evens = enum_for(:each_digit).
          select { |digit| digit.event? }

#lazy

Има една хватка, особено удобна за безкрайни функции:

def each_number
  n = 0
  loop do
    n += 1
    yield n
  end
end

enum_for(:each_number).
  lazy.
  select { |x| x.odd? }.
  take(10)

Въпроси