LoginSignup
9
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-07-29
1 / 17

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? を使おう。


参考文献

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3