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

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

---

## @tkawa

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

---

## 問題: `users` の「型」は何でしょう？

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

- Array
- ActiveRecord::Relation
- 他にも……

---

## 配列の代わりに Enumerator

```rb
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アプリのレスポンス

```rb
app = Proc.new do |env|
  [200, {'content-type' => 'text/plain'}, ["Hello world\n"]]
  # [ステータス, ヘッダのhash, レスポンスボディ]
end
```
レスポンスボディの仕様は
**`each`でブロックパラメータに文字列が受け取れるオブジェクト**

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

---

## 問題: `users` の「型」は何でしょう？

```rb
def some_process(users)
  users.each do |user|
    puts user.email
    # いろんな処理
  end
end
```
正解: **`each`でブロックパラメータに `User` が受け取れるオブジェクト**
<small>（`User` もクラスに関係なく `email` 他を実装していればよい）</small>

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

---

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

---

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

```rb
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、と思いきや……

---

<blockquote class="twitter-tweet" data-lang="ja"><a href="https://twitter.com/joker1007/status/832225618989191169?ref_src=twsrc%5Etfw"><img width="634" alt="ジョーカー 1007さんのツイート: &quot;Rubyで`is_a?(IO)`使うんじゃねええ！Tempfileが使えねえだろうが！&quot;" src="https://qiita-image-store.s3.amazonaws.com/0/6740/8ba8df2c-bf29-2b42-006f-c0103fa7e345.png"></a></blockquote>

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

---

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

```rb
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](https://qiita-image-store.s3.amazonaws.com/0/6740/577a8a84-639f-de14-097f-855b350fd6cc.png)](https://github.com/search?l=&q=%22is_a+IO%22+language%3ARuby&type=Code)

---

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

<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><a href="https://twitter.com/tkawa/status/1002433524224282624"><img width="768" alt="Toru KAWAMURAさんのツイート: &quot;Rubyでは「ここは配列だな…」と思いながら書いている場所が実は配列でなくても良い、ということに自覚的でいられるかどうかが大事だと思ってる&quot;" src="https://qiita-image-store.s3.amazonaws.com/0/6740/7659b941-bde0-19b2-6e01-c49b699aa4ce.png"></a></blockquote>

---

## 参考文献

- [Tempfile from:joker1007 - Twitter検索](https://twitter.com/search?f=tweets&vertical=default&q=Tempfile%20from%3Ajoker1007&src=typd&lang=ja)
- [Ruby のオススメの機能７選 - Qiita](https://qiita.com/cuzic/items/a265f140fdff289d5c07)
- [Stop including Enumerable, return Enumerator instead | Arkency Blog](https://blog.arkency.com/2014/01/ruby-to-enum-for-enumerator/)
- [Rubyの明示的/暗黙的な型変換についてのメモ（翻訳）](https://techracho.bpsinc.jp/hachi8833/2018_06_06/56984)