quattro_4 scribble

scribble 落書き (調べた事をただ落書きする)

RubyTapas #199 - #204

199 Regexp Union

★★

patterns = [
  ".each do |",
  ".each {|",
]
Regexp.union(patterns)
# => /\.each\ do\ \||\.each\ \{\|/

Regexp.union(".each do |", ".each {|", /for .* in .*/)
# => /\.each\ do\ \||\.each\ \{\||(?-mix:for .* in .*)/
Regexp.union(/Avdi/i, /RubyTapas/)
# => /(?i-mx:Avdi)|(?-mix:RubyTapas)/

200 Quantity

量(数値)の扱いなどは、単位の変換、型の変換、実装者・チームの違いで精度が失われる。重大なシステムでは悲劇につながる。

class Altimeter
  ...

  def change_by(change_amount)
    raise TypeError unless Feet === change_amount
    old_value = @value_in_feet
    @value_in_feet += change_amount
    puts "Altitude changed from #{old_value} to #{@value_in_feet}"
  end

in change_by: TypeError, in change_by: undefined method +

class Feet
  attr_accessor :magnitude

  ...

  def +(other)
    Feet.new(@magnitude + other.magnitude)
  end
end

alt = Altimeter.new(Feet.new(10_000))
alt.change_by(Feet.new(-600))  
# >> Altitude changed from 10000.0 feet to 9400.0 feet

201 Immutable Object

Numbers are immutable

良くない

START_ALTITUDE = Feet.new(10_000)

alt = Altimeter.new(START_ALTITUDE)
alt.change_by(Feet.new(500))

START_ALTITUDE                  # => #<Feet:0x000000017e0e90 @magnitude=10500.0>
# >> Altitude changed from 10500.0 feet to 10500.0 feet

メソッドの実装を間違えた場合を防ぐ (>=を間違えて=)

class Feet
  attr_reader :magnitude

  def initialize(magnitude)
    @magnitude = magnitude.to_f
    freeze
  end

  ...

  # 間違い
  def positive?
    @magnitude = 0.0
  end
end

f = Feet.new(30_000)
f.positive?                     # => 
# ~> -:18:in `positive?': can't modify frozen Feet (RuntimeError)
# ~>    from -:23:in `<main>'

freezeを使う

202 Identity And Equality

class Feet
  ...

  def ==(other)
    other.is_a?(Feet) && magnitude == other.magnitude
  end
end

case-equality

one.equal?(other_one)                # => false

one === other_one                # => true
case one
when Feet.new(1) then puts "one"
when Feet.new(2) then puts "two"
end
# >> one

There is one other kind of equality which we haven't touched on yet: hash equality

203 Hash Table

hash values

123.hash                        # => -4326452433101730515
"hello".hash                    # => 1575747404428851756
"goodbye".hash                  # => 2103260368034954340
:foo.hash                       # => -924210406470135610

Ruby's actual Hash implementation is quite sophisticated

even with very large hashes, the Hash only ever needs to perform full comparisons against a few keys before finding the one it's looking for

204 Hash Equality

期待しない動作

h = { one => "one", two => "two" }
h[Feet.new(1)]                  # => nil
h.key?(Feet.new(1))             # => false

require "set"
s = Set.new([one, two])
s                               # => #<Set: {#<Feet:0x00000002435da8 @magnitude=1.0>, #<Feet:0x00000002435d80 @magnitude=2.0>}>
s.include?(Feet.new(1))         # => false

Ruby doesn't use the double-equals equivalence operator to do this. It uses a different method which is specifically intended for hash equality, spelled eql?

  def hash
    [magnitude, Feet].hash
  end

  alias_method :eql?, :==
  • Value Object class
    • define an equivalency operator that uses the object's state, rather than its identity
    • define a custom #hash method which uses the object's state to generate a hash value
    • alias the hash equality operator to the equivalency operator