Ruby(Rails)でやりがちな型がイケてない実装

Ruby3.0.0に型を定義する機能が導入されるなどRubyでも型についての議論が活発になってきていますが、まだ広く使われている状況にはないと思います。

ただ、型を明確に定義しないとしても、例えばStringを入れていた変数にIntegerに入れ直したり、様々な型の変数が返却されるメソッドを作ることはほぼないと思います。
このように、Rubyでも型を意識して実装すると思いますが、機械的に静的解析しているわけではないので型がイケてない実装が紛れ込んでしまうことがあります。

この記事では、型を意識していてもやりがちな型がイケてない実装を３つ挙げようと思います。
今後、Rubyに本格的に型の静的解析が導入されることで、ここであげる実装例がリアルタイムに検知できる世界が来たら素敵だなーと思います。

# ?メソッドでnilを返却

Rubyではメソッド名が`?`で終わっている場合、Booleanを返却するという慣習があります。
この慣習はかなりメジャーだと思うのでRubyを書く方は守っていると思いますが、たまにnilを返却しているものを見かけます。

例えば下記のvalueが正の数値かチェックするメソッドを考えます。

```ruby
def value_positive?
  return unless value

  value.positive?
end
```

このメソッドはvalueが未設定の場合はnilが返却されます。
nilはfalsyとして扱われるため、例えば`if value_positive?`という使い方をしてもfalseを返却した場合と同じ挙動になるので問題は起きませんが、このメソッドが返却する型は`Boolean | nil`になります。

一方、下記の実装にすると戻り値の型は`Boolean`になります。

```ruby
def value_positive?
  return false unless value

  value.positive?
end
```

もう1つ似た例を紹介します。

```ruby
def value_positive?
  value&.positive?
end
```

やっていることは最初の例と同じですが、ボッチ演算子を使っています。
valueがnilの場合、`value&.positive?`がnilになるため、このメソッドも戻り値の型は`Boolean | nil`になります。
ボッチ演算子を使うことで実装が簡素になり便利なので、この例のような実装はよく見かけます。

これらのメソッドはRuby内で使っている限りはこの実装でも問題が起きることはほとんどないと思いますが、例えばAPIのレスポンスに`value_positive?`の戻り値を使う場合、スキーマの型が`Boolean | nil`になってしまいます。
APIスキーマの型で`Boolean | nil`を見かけたら`Boolean`にできないの？と思いますよね。
これからは型を明確にする時代が来そうなので、?メソッドの戻り値の型は`Boolean`にしておくと良さそうです。

# mapの処理でnilが混じる

mapはとても便利で多用されるメソッドの1つだと思います。
例えば配列の数値を二乗するメソッドを考えます。ただし、入力値が3の場合は除く仕様とします。

```ruby
def square_except3(ary)
  ary.map do |item|
    next if item == 3
    item * item
  end
end

ary = [1, 2, 3, 4, 5]
return_ary = square_except3(ary)
```

square_except3の返却値はどのようになるでしょうか？
今回の例の場合は`[1, 4, 16, 25]`ではなく、`[1, 4, nil, 16, 25]`が返却されます。
mapの中でnextした場合はその要素が省略されるわけではなく、nilとして返却されるためです。
そのため、return_aryに数値しか入っていない前提で例えば`return_ary.map {|item| item + 1 }`のような処理をするとエラーが発生します。

このような実装はたまに見かけますが、コードレビューなど目視確認だけではnilが入ることを見落としやすいです。
nilが混じるパターンで動作させれば気づけると思いますが、例えばnextになるパターンが超レアケースの場合はテストが漏れてしまうこともあるかもしれません。

もし型解析があったとしたら戻り値の型は`(Integer | nil)[]`となるので、動作させなくても異常に気づける可能性が高いです。
例えば同様の処理をTypescriptで実装している場合、VSCodeだと下記のようにリアルタイムに警告してくれます。
このように実装しながらリアルタイムで型解析して警告してくれるとスムーズに実装できてかなり良いと感じています。

![スクリーンショット 2022-02-02 8.56.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/02e8cb97-89df-f7f5-f4b1-3b3b93a12cfb.png)

# ActiveRecordと空配列

これが個人的には一番見かけるパターンです。
例えばチームがアクティブな場合のみユーザーの一覧を返却するメソッドを考えます。

```ruby
class Team < ApplicationRecord
  has_many :users

  def active_team_users
    return [] unless active?

    users
  end
end
```

active?がtrueの場合は、usersを返却しており、falseの場合は`[]`を返却しています。
usersはhas_manyで定義されているので、型は`ActiveRecord::Associations::CollectionProxy`となります。
一方、`[]`の型はArrayです。

呼び出し元では`team.active_team_users.each {...}`のように配列のようにしか使っていなかった場合はこの実装でも問題は起きませんが、その後user_idの昇順に並び替えたいという仕様が追加され、下記のように実装しました。

```ruby
team.active_team_users.order(:user_id).each {...}
```

こちらは`ActiveRecord::Associations::CollectionProxy`が返却されている場合はうまく動きますが、`Array`が返却された場合はorderをチェーンすることはできません。

今回の例の場合、`none`というメソッドが使って下記のように実装することで回避できます。

```ruby
class Team < ApplicationRecord
  has_many :users

  def active_team_users
    return users.none unless active?

    users
  end
end
```

noneを使うことで`ActiveRecord::AssociationRelation`が返却されるようになり、orderなどActiveRecordのチェーンも問題なく使えるようになります。
型が微妙に違うため`ActiveRecord::Associations::CollectionProxy | ActiveRecord::AssociationRelation`となりますが、どちらもActiveRecordの`Relation`クラスを継承しているため、whereやorderなどActiveRecordのメソッドを使うことができます。

こちらも前者で実装していたとしても[]が返却されるパターンをテストすれば異常に気づくことができますが、リアルタイムで型解析できるようになったら実装しながら気づけるようになるのでRubyにもそういう世界が早く来て欲しいなーと期待しています。