Ruby3.0.0に型を定義する機能が導入されるなどRubyでも型についての議論が活発になってきていますが、まだ広く使われている状況にはないと思います。
ただ、型を明確に定義しないとしても、例えばStringを入れていた変数にIntegerに入れ直したり、様々な型の変数が返却されるメソッドを作ることはほぼないと思います。
このように、Rubyでも型を意識して実装すると思いますが、機械的に静的解析しているわけではないので型がイケてない実装が紛れ込んでしまうことがあります。
この記事では、型を意識していてもやりがちな型がイケてない実装を3つ挙げようと思います。
今後、Rubyに本格的に型の静的解析が導入されることで、ここであげる実装例がリアルタイムに検知できる世界が来たら素敵だなーと思います。
?メソッドでnilを返却
Rubyではメソッド名が?
で終わっている場合、Booleanを返却するという慣習があります。
この慣習はかなりメジャーだと思うのでRubyを書く方は守っていると思いますが、たまにnilを返却しているものを見かけます。
例えば下記のvalueが正の数値かチェックするメソッドを考えます。
def value_positive?
return unless value
value.positive?
end
このメソッドはvalueが未設定の場合はnilが返却されます。
nilはfalsyとして扱われるため、例えばif value_positive?
という使い方をしてもfalseを返却した場合と同じ挙動になるので問題は起きませんが、このメソッドが返却する型はBoolean | nil
になります。
一方、下記の実装にすると戻り値の型はBoolean
になります。
def value_positive?
return false unless value
value.positive?
end
もう1つ似た例を紹介します。
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の場合は除く仕様とします。
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だと下記のようにリアルタイムに警告してくれます。
このように実装しながらリアルタイムで型解析して警告してくれるとスムーズに実装できてかなり良いと感じています。
ActiveRecordと空配列
これが個人的には一番見かけるパターンです。
例えばチームがアクティブな場合のみユーザーの一覧を返却するメソッドを考えます。
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の昇順に並び替えたいという仕様が追加され、下記のように実装しました。
team.active_team_users.order(:user_id).each {...}
こちらはActiveRecord::Associations::CollectionProxy
が返却されている場合はうまく動きますが、Array
が返却された場合はorderをチェーンすることはできません。
今回の例の場合、none
というメソッドが使って下記のように実装することで回避できます。
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にもそういう世界が早く来て欲しいなーと期待しています。