Enumerator
-и и мързел
Можем ли в Ruby да създаваме обекти от тип "клас", без те да си имат име (анонимни класове)? Ами модули? Ако да, как? Ако не, защо не?
Можем, така:
foo = Class.new { def hello() 'HI!' end }
# => #<Class:0x002ba32c8d7110>
foo.new.hello
# => "HI!"
bar = Module.new { '...' }
# => #<Module:0x002ba32c8d4528>
Какъв ще бъде резултатът от кода по-долу? Защо?
FOO = 'toplevel'
module Private
class Object
FOO = 'nested'
end
puts Object::FOO
puts ::Object::FOO
end
Ще се изведе първо nested
, после toplevel
.
Какво ще се случи при изпълнение на следния код (и защо):
Object::String = Class.new
puts String.new('bar').upcase
Ще се предизвика грешка при извикване на String#new
, защото top-level константата String
е предефинирана на първия ред и сочи към клас, чийто конструктор не приема аргументи. Дори няма да се достигне до извикване на upcase
.
Допълнително, Ruby ще иведе warning на STDERR, че предефинираме вече дефинирана константа.
Какво ще се случи при изпълнение на следния код (и защо):
PIE = 'Apple'
module Foo
PIE = 'Strawberry'
end
module Foo::Bar
puts PIE
end
На екрана ще се изведе текстът "Apple". module Foo::Bar
ще създаде нов модул Bar
, ще запише създадения обект в таблицата с константи на модула Foo
и ще отвори нов scope в контекста на Bar
, като родителският контекст ще бъде root scope-а, а не този на Foo
. Затова и константата PIE
ще бъде намерена след един fallback в root scope-a.
Ще сработи ли кодът по-долу – да, не – и ако сработи, какво ще изведе? Защо?
class String
def to_proc
->(object) { object.public_send(self) }
end
end
['I', 'am', 'Matz'].map(&"length")
Ще сработи. Ще изведе [1, 2, 4]
. Пресъздаваме дефиницията
на вградения Symbol#to_proc
за низове.
Какъв ще е резултатът от кода по-долу?
def foo(x, _, _) p x end
def bar(y, z, z) p y end
foo 1, 2, 3
bar 1, 2, 3
Ще даде синтактична грешка заради дефиницията на bar
заради
повтарящи се имена на аргументи. Кодът без bar
е валиден и
щеше да изведе 1
, защото _
се използва като
placeholder със смисъл на "игнорирай този аргумент" и може да се повтаря.
В интерес на истината, и това е валидно:
def foo(x, _bar, _bar) end
Какъв ще е резултатът от кода по-долу? Защо?
answer = 42
universe = proc do
def say_answer
puts "The answer is: #{answer}"
end
say_answer
end
Proc
-а universe
(trolololo)
answer
е недефинирано в say_answer
def
, class
и module
са scope gate-ове
Изпълнението на кода по-долу ще предизвика ли грешка или ще изведе нещо на екрана? Какво? Защо?
def foo(klass = Class.new { def bar() 'Roll the dice' end })
puts klass.new.bar
def foo() puts 'You feel it running through your bones' end
end
foo
foo
Кодът няма да продуцира грешки при изпълнението си. На екрана ще се изведе:
Roll the dice You feel it running through your bones
В Ruby има "switch". Казва се case
.
def quote(name)
case name
when 'Yoda'
puts 'Do or do not. There is no try.'
when 'Darth Vader'
puts 'The Force is strong with this one.'
when 'R2-D2'
puts 'Beep. Beep. Beep.'
else
puts 'Dunno what to say'
end
end
break
when
не мине, изпълнява се else
when
не мине, и няма else
, не става нищо
case
е израз, което значи, че връща стойностcase operation
when :& then puts 'And?'
when :| then puts 'Or...'
when :! then puts 'Not!'
else puts 'WAT?'
end
На какво ще се оцени следният код?
case 'Wat?'
when 'watnot' then puts "I'm on a horse."
end
Ако няма else
и никой when
не match-не, се връща nil
.
case
не сравнява с ==
. Може да напишете следното:
def qualify(age)
case age
when 0..12
'still very young'
when 13..19
'a teenager! oh no!'
when 33
'the age of jesus'
when 90..200
'wow. that is old!'
else
'not very interesting'
end
end
case
сравнява с ===
. Няколко класа го имплементират:
Range
Regexp
Class
==
def qualify(thing)
case thing
when Integer then 'this is a number'
when String then 'it is a string'
when Array then thing.map { |item| qualify item }
else 'huh?'
end
end
case hours_of_sleep
when 8..10 then 'I feel fine.'
when 6...8 then 'I am a little sleepy.'
when 0..3 then 'OUT OF MY WAY! I HAVE PLACES TO BE AND PEOPLE TO SEE!'
end
def parse_date(date_string)
case date_string
when /(\d{4})-(\d\d)-(\d\d)/
Date.new $1.to_i, $2.to_i, $3.to_i
when /(\d\d)\/(\d\d)\/(\d{4})/
Date.new $3.to_i, $1.to_i, $2.to_i
end
end
Ruby позволява да обедините проверката за няколко възможни стойности в един when
, дори да не са от един и същи тип.
case number
when (42+0i) then 'This is too complex for me!'
when 42, 42.0 then 'This is more like it!'
end
when
case
, например:thing = 42
case
when thing == 1 then 1
else 'no_idea'
end
if
-овеСега е моментът.
Някои методи на Enumerable
могат да не вземат блок.
numbers = []
1.upto(5) { |x| numbers << x }
numbers # => [1, 2, 3, 4, 5]
other = 1.upto(5)
other # => #<Enumerator: 1:upto(5)>
other.to_a # => [1, 2, 3, 4, 5]
1.upto(5).map(&:succ) # => [2, 3, 4, 5, 6]
Енумераторите могат да се държат като итератори.
numbers = 1.upto(3)
numbers.next # => 1
numbers.next # => 2
numbers.next # => 3
numbers.next # => error: StopIteration
loop
прави безкраен цикъл. Спира на StopIteration
.
numbers = 1.upto(3)
loop do
puts numbers.next
end
Enumerable
връщат Enumerator
, ако ги извикате без блок
Enumerable
, помислете и за съвместимост с enumerator-иenum = [1, 2].each # => #<Enumerator: [1, 2]:each>
enum.next # => 1
enum.next # => 2
enum.next # => error: StopIteration
enum = Enumerator.new do |yielder|
yielder << 1
yielder << 2
end
enum.next # => 1
enum.next # => 2
enum.next # => error: StopIteration
Може да извадите енумератор от произволен метод с enum_for
.
class Numbers
def primes
yield 2
yield 3
yield 5
yield 7
end
end
first_four_primes = Numbers.new.enum_for(:primes)
first_four_primes.to_a # => [2, 3, 5, 7]
o = Object.new
def o.each
yield
yield 'hello'
yield [1, 2]
end
enum = o.to_enum
enum.next # => nil
enum.next # => "hello"
enum.next # => [1, 2]
class Foo
def each
return to_enum unless block_given?
yield 1
yield 2
end
end
f = Foo.new
f.each { |x| puts x } # => nil
f.each # => #<Enumerator: #<Foo:0x002b5a2cffc2e0>:each>
Енумераторите имат някои интересни методи.
numbers = 1.upto(3)
numbers.with_index.to_a # => [[1, 0], [2, 1], [3, 2]]
numbers.with_object(:x).to_a # => [[1, :x], [2, :x], [3, :x]]
Ако ви се е случвало да ви трябва индекс в map
:
words = %w( foo bar baz ).map.with_index do |word, index|
"#{index}: #{word.upcase}"
end
words # => ["0: FOO", "1: BAR", "2: BAZ"]
data = "Some really long\ntext with new lines."
# Вариант 1
data.lines.map(&:split).map(&:size) # => [3, 4]
# Вариант 2
data.lines.map { |line| line.split.size } # => [3, 4]
Enumerable#lazy
връща "мързелив" енумератор
Enumerator#lazy
(1..Float::INFINITY).map { |i| i * i }.first(5) # => ?
(1..Float::INFINITY).lazy.map { |i| i * i }.first(5) # => [1, 4, 9, 16, 25]
[
' a ',
' b ',
' c ',
].lazy.map { |x| p x.strip }.map { |x| p x.upcase }.take(1).to_a
Горното ще изведе на екрана:
"a" "A"
И ще върне списъка ["A"]
.
module Enumerator
class Sample < Enumerator
def initialize(obj)
super() do |yielder|
obj.each do |val|
yielder << val
end
end
end
end
end
module Enumerator
class Lazy < Enumerator
def initialize(obj)
super() do |yielder|
obj.each do |val|
if block_given?
yield(yielder, val)
else
yielder << val
end
end
end
end
end
end
module Enumerator
class Lazy < Enumerator
def map
Lazy.new(self) do |yielder, val|
yielder << yield(val)
end
end
end
end
module Enumerable
def filter_map(&block)
map(&block).compact
end
end
class Enumerator::Lazy
def filter_map
Lazy.new(self) do |yielder, *values|
result = yield *values
yielder << result if result
end
end
end
(1..Float::INFINITY).lazy.filter_map { |i| i * i if i.even? }.first(5) # => [4, 16, 36, 64, 100]
Методи имплементирани в Enumerator::Lazy (документация) към момента:
map
/ collect
flat_map
/ collect_concat
select
/ find_all
reject
grep
zip
take
и take_while
drop
и drop_while
slice_before
, slice_after
и slice_when
([документация](http://ruby-doc.org/core-2.2.3/Enumerable.html#method-i-slice_when))Методи, които "материализират" lazy колекцията:
to_a
/ force
each
next
# Решение 1
Fib.lazy.select(&:even?).take(10).to_a
# Решение 2
a = []
Fib.each do |x|
next if x.odd?
a << x
break if a.size == 10
end
Неканоничният и праволинеен начин за дефиниране на класов метод:
module Snake
class Level
def Level.load(level_data)
# Code
end
end
end
Този начин е еквивалентен на предишния слайд:
module Snake
class Level
def self.load(level_data)
# Code
end
end
end
Това работи, понеже:
self
self
и Snake::Level
в тялото на класа реферират към един и същ обектИзползва се при нужда да се дефинират няколко класови метода:
module Snake
class Level
class << self
def load(level_data)
# Code
end
end
end
end
def ClassName.method_name() end
)
Преди да продължим със следващата тема.
Object#freeze
(Мутира обекта!)
Object#frozen?
dup
.)
module Entities
ENTITIES = {
'&' => '&',
'"' => '"',
'<' => '<',
'>' => '>',
}.freeze
ENTITY_PATTERN = /#{ENTITIES.keys.join('|')}/.freeze
def escape(text)
text.gsub ENTITY_PATTERN, ENTITIES
end
end
Тъй като низовете в Ruby са mutable, всяко срещане на низ = нов обект:
''.object_id # => 23474979468400
''.object_id # => 23474979467760
''.object_id == ''.object_id # => false
class HTTP
attr_reader :method
def post?
method == 'POST' # New string object on each call
end
end
class HTTP
attr_reader :method
METHOD_POST = 'POST'
def post?
method == METHOD_POST
end
end
Това е досадно. Затова в Ruby 2.1 има и по-добро решение.
''.freeze.object_id # => 23544681604760
''.freeze.object_id # => 23544681604760
''.freeze.object_id == ''.freeze.object_id # => true
freeze
-нати низове и символи се размива в последните версии на Ruby с това, че се очаква и символите да започнат да се garbage collect-ват."Chunky bacon".freeze
се оптимизира от Ruby и връща един и същи обект
'text'.freeze.object_id # => 23901326942960
foo = 'text'
foo.freeze.object_id # => 23901326931320
foo.freeze.object_id == 'text'.freeze.object_id # => false