Enumerator
-и и мързел
Изпълнението на кода по-долу ще предизвика ли грешка или ще изведе нещо на екрана:
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
Какво ще се случи при изпълнение на следния код (и защо):
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 Proc
def ===(testable)
call testable
end
end
case 42
when String then 'This is a string'
when :odd?.to_proc then 'This is an odd number'
when :even?.to_proc then 'This is an even number'
else 'I have no f*cking idea what this is.'
end
case
използва "оператора" ===
, за да оценява различните случаи. Всеки when
се оценява така: condition === testable
.
Някои методи на 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:0x41767870>: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
Методи, които "материализират" 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 RBFS
class File
def File.parse(string_data)
# Solution lost.
end
end
end
Този начин е еквивалентен на предишния слайд:
module RBFS
class File
def self.parse(string_data)
# Solution lost.
end
end
end
Това работи, понеже:
self
self
и RBFS::File
в тялото на класа реферират към един и същ обектИзползва се при нужда да се дефинират няколко класови метода:
module RBFS
class File
class << self
def parse(string_data)
# Solution lost.
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 # 549133130
''.object_id # 549132850
''.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 # 551882650
''.freeze.object_id # 551882650
''.freeze.object_id == ''.freeze.object_id # true
freeze
-нати низове и символи се размива в последните версии на Ruby"Chunky bacon".freeze
се оптимизира от Ruby и връща един и същи обект
'text'.freeze.object_id # 549273490
foo = 'text'
foo.freeze.object_id # 549272000
foo.freeze.object_id == 'text'.freeze.object_id # false
Каква е разликата между require './foo'
и require_relative 'foo'
?
require './foo' # Релативно спрямо Dir.pwd
require_relative 'foo' # Релативно спрямо __FILE__
"Текущата директория" на процеса може да се променя както при стартиране, така и по време на изпълнение на програмата (с Dir.chdir
) и е различна от директорията, в която се намира Ruby файла, изпълняващ require метода:
cd /home
ruby /foo/bar/baz.rb # Dir.pwd ще е: /home
require 'foo'
търси файл foo.rb
в $LOAD_PATH
.
, ~
или /
. ├── README.rdoc ├── Rakefile ├── bin │ └── skeptic ├── features ├── lib │ ├── skeptic │ │ ├── rules.rb │ │ └── scope.rb │ └── skeptic.rb ├── skeptic.gemspec └── spec
lib/
обикновено съдържа foo.rb
и lib/foo/
foo.rb
обикновено е единственото нещо в lib/
lib/foo
lib/
се добавя в load path
require 'foo'
или require 'foo/something'
require
областта
require
и $LOAD_PATH
и вижте какво се случва
load
е много сходен с require
, но има няколко разлики
load 'foo.rb'
load
-ове изпълняват файла
load
не може да зарежда .so
/.dll
библиотеки
load
има опционален параметър, с който може да обвие файла в анонимен модул
Имате ли въпроси по require
, load
, gem-ове в Ruby?
Демонстрация с цел да усвоим основни пещернячески умения.