Ruby

Rubyと型とダックタイピング

TokyuRuby会議12 (2018-07-29) 発表のスライドです

#tqrk12


@tkawa

  • 川村 徹
  • プログラマ
  • ソニックガーデン
  • REST厨
  • Sendagaya.rb
    • 毎週月曜 19:30-
    • 株式会社トクバイさんオフィス(渋谷)
    • ゆるーいRubyコミュニティ

問題: users の「型」は何でしょう?

def some_process(users)
  users.each do |user|
    puts user.email
    # いろんな処理
  end
end
  • Array
  • ActiveRecord::Relation
  • 他にも……

配列の代わりに Enumerator

users = Enumerator.new do |y|
  open('huge.csv') do |f|
    f.each do |line|
      y << User.new(line)
    end
  end
end

# こう書いてもできるけどファイルが close できないな 😔
# users = open('huge.csv').lazy.map{|line| User.new(line) }

ファイルを最初にすべてStringに読み込まなくてもOK
ファイルをダウンロードする場合にも応用できる


似た例: Rackアプリのレスポンス

app = Proc.new do |env|
  [200, {'content-type' => 'text/plain'}, ["Hello world\n"]]
  # [ステータス, ヘッダのhash, レスポンスボディ]
end

レスポンスボディの仕様は
eachでブロックパラメータに文字列が受け取れるオブジェクト

例えばIOやFileインスタンスをそのまま渡すことも可能。


問題: users の「型」は何でしょう?

def some_process(users)
  users.each do |user|
    puts user.email
    # いろんな処理
  end
end

正解: eachでブロックパラメータに User が受け取れるオブジェクト
User もクラスに関係なく email 他を実装していればよい)

このようにクラスやモジュールではなくメソッドによって型が決まるのが ダックタイピング


あなたが配列と思ったものは、実は配列ではないかもしれない


「型」の扱いが問題になる例

def prepare(source)
  io = if source.is_a?(IO)
         source
       elsif source.is_a?(String)
         open(source)
       else
         raise 'Invalid source'
       end
  

source がファイルならそのまま使う、文字列ならファイル名とみなしてopen
FileはIOだからこれでOK、と思いきや……


Tempfile.new.is_a?(IO) # => false
TempfileはFileでもIOでもない!!


def prepare(source)
  io = case source
       when IO, Tempfile # ←追加した
         source
       when String
         open(source)
       else
         raise 'Invalid source'
       end
  

さらに、StringIOもIOではない。
Rack::Test::UploadedFile も ActionDispatch::Http::UploadedFile も。。
全部足していくの。。😵


これは罠ではあるけれど、Rubyの欠陥ではない。
TempfileもStringIOもRubyの標準添付ライブラリ。
つまり、Rubyは最初から is_a? ではうまくいかないようにできている。

何を期待しているのか?
IO のように振る舞うこと?
もっと直接的に言うと、eachのような特定のメソッドに応答すること。


もし io.read したいのなら respond_to? を使おう

def prepare(source)
  io = if source.respond_to?(:read)
         source
       elsif source.is_a?(String)
         open(source)
       else
         raise 'Invalid source'
       end
  

is_a?(IO)多い 😣
Search · "is_a IO" language:Ruby.png


is_a? を使うのは特定のクラスだけ

Integer/Float/Numeric, String, Symbol, Hash, Date, Time, Module/Class

使う必要がないもの

Array → each
IO → read/write
Proc → call

to_i, to_f, to_s, to_a, to_h を使えば済む場合も多い。


もし「型チェックツール」が is_a? を使うと同じ問題が起こる。respond_to? を使おう。


参考文献