2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

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だと下記のようにリアルタイムに警告してくれます。
このように実装しながらリアルタイムで型解析して警告してくれるとスムーズに実装できてかなり良いと感じています。

スクリーンショット 2022-02-02 8.56.38.png

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にもそういう世界が早く来て欲しいなーと期待しています。

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?