08. Enumerator. Lazy. Freeze. Класови методи. Ruby gems. Code spelunking из skeptic

08. Enumerator. Lazy. Freeze. Класови методи. Ruby gems. Code spelunking из skeptic

08. Enumerator. Lazy. Freeze. Класови методи. Ruby gems. Code spelunking из skeptic

3 ноември 2014

Днес

Въпрос 1

Изпълнението на кода по-долу ще предизвика ли грешка или ще изведе нещо на екрана:

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 

Въпрос 2

Какво ще се случи при изпълнение на следния код (и защо):

Object::String = Class.new
puts String.new('bar').upcase

Ще се предизвика грешка при извикване на String#new, защото top-level константата String е предефинирана на първия ред и сочи към клас, чийто конструктор не приема аргументи. Дори няма да се достигне до извикване на upcase.

Допълнително, Ruby ще иведе warning на STDERR, че предефинираме вече дефинирана константа.

Въпрос 3

Какво ще се случи при изпълнение на следния код (и защо):

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.

Въпрос 4

Какво ще се случи при изпълнение на следния код (и защо):

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.

Call for speakers

Подиумът на този курс е и ваш

Enumerator-и

Някои методи на 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]

Enumerator-и

нещо като итератори

Енумераторите могат да се държат като итератори.

numbers = 1.upto(3)

numbers.next   # 1
numbers.next   # 2
numbers.next   # 3
numbers.next   # error: StopIteration

Kernel#loop

loop прави безкраен цикъл. Спира на StopIteration.

numbers = 1.upto(3)

loop do
  puts numbers.next
end

Enumerable и Enumerator

Enumerators 101

примери

enum = [1, 2].each # #<Enumerator: [1, 2]:each>

enum.next # 1
enum.next # 2
enum.next # error: StopIteration

Enumerators 102

примери

enum = Enumerator.new do |yielder|
  yielder << 1
  yielder << 2
end

enum.next # 1
enum.next # 2
enum.next # error: StopIteration

Object#enum_for

Може да извадите енумератор от произволен метод с 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]

Object#to_enum

примери

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]

Enumerable и Enumerator (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>

#with_object и #with_index

Енумераторите имат някои интересни методи.

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_with_index

навръзване на енумератори

Ако ви се е случвало да ви трябва индекс в 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]

Решение 1 създава междинен масив с всички елементи

Мързеливи енумератори

Примери

безкрайни поредици

(1..Float::INFINITY).map { |i| i * i }.first(5) # => ?
(1..Float::INFINITY).lazy.map { |i| i * i }.first(5) # [1, 4, 9, 16, 25]

Примери

еднократно оценяване на целия chain

[
  ' a ',
  ' b ',
  ' c ',
].lazy.map { |x| p x.strip }.map { |x| p x.upcase }.take(1).to_a

Горното ще изведе на екрана:

"a"
"A" 

И ще върне списъка ["A"].

SampleEnumerator

module Enumerator
  class Sample < Enumerator
    def initialize(obj)
      super() do |yielder|
        obj.each do |val|
          yielder << val
        end
      end
    end
  end
end

Enumerator::Lazy

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

Enumerator::Lazy#map

примерна дефиниция на map

module Enumerator
  class Lazy < Enumerator
    def map
      Lazy.new(self) do |yielder, val|
        yielder << yield(val)
      end
    end
  end
end

Enumerator::Lazy

примерна дефиниция на нов lazy метод

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]

LazyEnumerator methods

Методи имплементирани в Enumerator::Lazy (документация) към момента:

Методи, които "материализират" lazy колекцията:

Кога ни е полезно?

Първите 10 четни числа на Фибоначи

# Решение 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

Това работи, понеже:

Класови методи

трети начин

Използва се при нужда да се дефинират няколко класови метода:

module RBFS
  class File
    class << self
      def parse(string_data)
        # Solution lost.
      end
    end
  end
end

Класови методи

конвенции

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

Преди да продължим със следващата тема.

Замразяване на обекти в Ruby

Замразяване на обекти

module Entities
  ENTITIES = {
    '&' => '&amp;',
    '"' => '&quot;',
    '<' => '&lt;',
    '>' => '&gt;',
  }.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

Замразяване на низове

пример за workaround

class HTTP
  attr_reader :method
  METHOD_POST = 'POST'

  def post?
    method == METHOD_POST
  end
end

Това е досадно. Затова в Ruby 2.1 има и по-добро решение.

String#freeze

''.freeze.object_id                        # 551882650
''.freeze.object_id                        # 551882650
''.freeze.object_id == ''.freeze.object_id # true

Синтаксис за замразяване на низове

Оптимизация при замразяване

работи само с литералния синтаксис

'text'.freeze.object_id                         # 549273490
foo = 'text'
foo.freeze.object_id                            # 549272000
foo.freeze.object_id == 'text'.freeze.object_id # false

Замразяване на обекти

Въпрос 5

Каква е разликата между 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 и $LOAD_PATH

преговор

Библиотеките в Ruby

преговор

Типичната структура на един gem

skeptic опростен

.
├── README.rdoc
├── Rakefile
├── bin
│   └── skeptic
├── features
├── lib
│   ├── skeptic
│   │   ├── rules.rb
│   │   └── scope.rb
│   └── skeptic.rb
├── skeptic.gemspec
└── spec 

Особеностите

Останалите неща

Kernel#load

Въпроси по require

Имате ли въпроси по require, load, gem-ове в Ruby?

Code Spelunking

из skeptic

Демонстрация с цел да усвоим основни пещернячески умения.

Въпроси