Enumerable
Ако имаме следната дефиниция на речник: numbers = {:one => :eins, :two => :zwei}
, какво ще върне всеки един от следните три реда код:
numbers[:eins] # => ?
numbers.fetch(:two, :something) # => ?
numbers.fetch(:four) # => ?
nil
:zwei
Какво прави def
, ако не се намираме в class
?
def
винаги дефинира метод в някакъв клас
def
не е в дефиниция на клас, отива като private метод на ObjectКакво е *b
в този контекст? Колко аргумента можем да подаваме на тази функция?
def something(a, *b, c)
end
Параметър, който при извикване на функцията ще съдържа списък от аргументите, които може да са променлив брой.
Какво са drink
, size
и syrup
в този контекст, има ли някаква разлика между тях и ако да, каква?
def order(drink:, size: 'grande', syrup: nil)
end
drink
е задължителен аргумент
size
и syrup
имат стойности по подразбиране.
Какви начини знаете за извикване на следната анонимна функция?
pow = lambda { |a, b| a**b }
pow.call 2, 3
pow[2, 3]
pow.(2, 3)
Каква е конвенцията за употреба на !
в края на името на метод?
Символът !
се поставя в края на метод, ако съществуват две версии на метода, с еднакво име и с разлика в поведението. Обикновено удивителната получава "по-опасният" метод, каквото и да означава това.
Метод с !
в края не е задължително метод, който мутира обект, както и има методи, които мутират обекти, но не са с удивителна в края (например, Array#pop
).
Анонимни функции в Ruby се дефинират с lambda
. Имат три начина на извикване:
pow = lambda { |a, b| a**b }
pow.call 2, 3
pow[2, 3]
pow.(2, 3)
За нещастие, не може да извиквате така: double(2)
. Това е несъвместимо с
изтърваването на скобите при извикването на метод.
Може и така:
double = lambda do |x|
x * 2
end
Важи стандартната конвенция за { }
и do
/end
.
От 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 }
Може и така:
def make_block(&block)
block
end
doubler = make_block { |n| n * 2 }
doubler.(2) # 4
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
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
->(foo:, **opts) { p [opts] }.call foo: 'bar', larodi: 'baz'
В Ruby има два вида анонимни функции. Другият е Proc.
double = Proc.new { |x| x * 2 }
double.call(2)
double[2]
double.(2)
Дотук е същото като при lambda
, но има някои разлики при извикване.
f = | Proc.new { |x, y| p x, y } | lambda { |x, y| p x, y } |
---|---|---|
f.call(1) | 1 nil | ArgumentError |
f.call(1, 2) | 1 2 | 1 2 |
f.call(1, 2, 3) | 1 2 | ArgumentError |
f.call([1, 2]) | 1 2 | ArgumentError |
f.call(*[1, 2]) | 1 2 | 1 2 |
yield
ползва семантиката на Proc.new
lambda
Ако първият аргумент на функция е хеш, трябва да изтървете скобите на хеша, ако изтървете скобите на метода.
# Валиден код
order drink: 'latte', size: 'grande'
order({drink: 'latte', size: 'grande'})
# Невалиден код
order {drink: 'latte', size: 'grande'}
Във втория случай, Ruby си мисли, че му подавате блок.
{
/}
, do
/end
) и yield
Стандартните функционални неща:
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
свежда списък до единична стойност с някаква операция:
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
са синоними. Ползвайте първото.
Имаме списък с думи. Искаме да получим хеш от вида {дума => дължина на думата}
:
words = %w[chunky bacon is awesome]
words.reduce({}) { |hash, word| hash[word] = word.length; hash }
Това е неидиоматично използване на #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 }
За забавлението. Неяснотите — следващия път.
class Array
def reduce(initial = nil)
remaining = dup
buffer = initial || remaining.shift
until remaining.empty?
buffer = yield buffer, remaining.shift
end
buffer
end
end
answer = 42
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
nil
head, *tail = [1, 2, 3]
head # 1
tail # [2, 3]
first, *middle, last = 1, 2, 3, 4
first # 1
middle # [2, 3]
last # 4
middle
и tail
обират всичко останало
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
head, (title, (body,)) = [1, [2, [3]]]
)
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>
Горното важи не само за блокове, но и за методи.
success, message = execute(job)
[[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
Имате ли въпроси по тази тема?
auto_link("Some text here.", options = {link_emails: false}, sanitize: true)
Дефинират се с 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
По подразбиране имат стойност 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
е референция към обекта,
на който е извикан методът. Като 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
Разбира се, 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
Последното е досадно за писане. Затова:
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
е метод, който генерира два метода — #foo
и #foo=
. Достъпен е в дефинициите на класове. Неформален термин за такива
методи е "class macro".
Има ги в изобилие.
Обърнете внимание, че следните два реда правят едно и също:
person.age()
person.age
Няма разлика между достъпване на атрибут и извикване на метод, който го изчислява. Това се нарича Uniform Access Principle и като цяло е хубаво нещо.
В Ruby важат следните конвенции.
UpperCamelCase
SCREAMING_SNAKE_CASE
plain_snake_case
(в т.ч. и инстанционните променливи)Във всеки момент може да "отворите" клас и да му добавите методи. Това вече дори сте го правили.
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
Понякога дори е полезно:
class Object
if RUBY_VERSION <= '1.8.6'
def tap
yield self
self
end
end
end
Сега е моментът да ги зададете :-)
Ако извикате #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)
Ето и всички оператори, които можете да предефинирате:
+ - * / % **
^ | &
<= < > >=
<=> == === != =~ !~
[] []= >> <<
, както и унарните + -
a + b
всъщност се извиква a.+(b)
(методът + от класа на a
)
+= >>= &&=
не могат да се предефинират, но действат според очакваното.
a += b
е просто синтаксис за a = a + b
+ -
съответно чрез методите +@ -@
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
, не може да го викате с явен получател.
Дори със 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
private
protected
и private
отново в следващи лекцииObject#tap
извиква блока със себе си и връща обекта, на който е извикан.
array = [].tap do |items|
items # []
items.equal? array # false
items << 'foo'
'other thing'
end
array # ["foo"]
Имате следния код
(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 }
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}
Следните два реда са (почти) еквивалентни:
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]
Всъщност, малко по-сложно е:
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
['Foo', :bar, 3].map(&:to_s).map(&:upcase)
class Symbol
def to_proc
# ...?
end
end
send
send
, за да викате произволни методи на този обект
private
или protected
такива
public_send
, ако искате да не прекрачвате този праг
3.send :+, 4 # 7
class Symbol
def to_proc
->(object, *args) { object.public_send self, *args }
end
end
Модулите в 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
е инстанцията от класа, в който модулът е бил миксиран и на която е извикан даденият метод.
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"
Ако два модула дефинират един и същи метод, ползва се методът от последно миксирания модул:
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"
Просто за информация: методите на mixin-ите имат приоритет пред тези на родителя.
Всичко това е свързано с нещо, наречено ancestor chain, за което ще си говорим следващия път.
Помните ли тези методи и техните синоними?
[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 ги има.
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, за да генерираме тази таблица.
Хешовете също са 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
Някои от 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
връща хеш.
Hash
, Array
Range
от числа, дати, символи и прочее
Set
и други...include Enumerable
#each
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]
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]