07. case, Enumerator, Class methods, Object#freeze

07. case, Enumerator, Class methods, Object#freeze

07. case, Enumerator, Class methods, Object#freeze

2 ноември 2015

Днес

Call for speakers

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

Четвърта задача

Въпрос 1

Можем ли в Ruby да създаваме обекти от тип "клас", без те да си имат име (анонимни класове)? Ами модули? Ако да, как? Ако не, защо не?

Можем, така:

foo = Class.new { def hello() 'HI!' end }
# => #<Class:0x002ba32c8d7110>

foo.new.hello
# => "HI!"

bar = Module.new { '...' }
# => #<Module:0x002ba32c8d4528>

Въпрос 2

Какъв ще бъде резултатът от кода по-долу? Защо?

FOO = 'toplevel'

module Private
  class Object
    FOO = 'nested'
  end

  puts Object::FOO
  puts ::Object::FOO
end

Ще се изведе първо nested, после toplevel.

Въпрос 3

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

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

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

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

Въпрос 4

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

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.

Въпрос 5

Ще сработи ли кодът по-долу – да, не – и ако сработи, какво ще изведе? Защо?

class String
  def to_proc
    ->(object) { object.public_send(self) }
  end
end

['I', 'am', 'Matz'].map(&"length")

Ще сработи. Ще изведе [1, 2, 4]. Пресъздаваме дефиницията на вградения Symbol#to_proc за низове.

Въпрос 6

Какъв ще е резултатът от кода по-долу?

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

Въпрос 7

Какъв ще е резултатът от кода по-долу? Защо?

answer = 42

universe = proc do
  def say_answer
    puts "The answer is: #{answer}"
  end

  say_answer
end
  • Нищо няма да стане, понеже не викаме Procuniverse (trolololo)
  • При извикване щяхме да получим грешка, понеже answer е недефинирано в say_answer
  • Причината е, че def, class и module са scope gate-ове
  • Дефиницията на блок не е scope gate!

Въпрос 8

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

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 

case

В 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

case

особености

case

алтернативен синтаксис

case operation
  when :& then puts 'And?'
  when :| then puts 'Or...'
  when :! then puts 'Not!'
  else         puts 'WAT?'
end

case

връщана стойност

На какво ще се оцени следният код?

case 'Wat?'
  when 'watnot' then puts "I'm on a horse."
end

Ако няма else и никой when не match-не, се връща nil.

case

стойности

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

Object#===

case сравнява с ===. Няколко класа го имплементират:

case

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

Range#===

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

case

Regexp#===

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

case

when с няколко стойности

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

case

с обикновени условия

thing = 42
case
  when thing == 1 then 1
  else 'no_idea'
end

Въпроси по case

Сега е моментът.

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:0x002b5a2cffc2e0>: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 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

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

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

трети начин

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

module Snake
  class Level
    class << self
      def load(level_data)
        # Code
      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                 # => 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

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

пример за workaround

class HTTP
  attr_reader :method
  METHOD_POST = 'POST'

  def post?
    method == METHOD_POST
  end
end

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

String#freeze

''.freeze.object_id                        # => 23544681604760
''.freeze.object_id                        # => 23544681604760
''.freeze.object_id == ''.freeze.object_id # => true

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

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

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

'text'.freeze.object_id                         # => 23901326942960
foo = 'text'
foo.freeze.object_id                            # => 23901326931320
foo.freeze.object_id == 'text'.freeze.object_id # => false

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

Въпроси