やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

動的型付け言語(Ruby)でも関数の戻り値の型は可能な限り統一した方が良い

出会いは突然に😲

いつものように眠い目を擦りながらコードレビューをしていると以下のようなコードに遭遇しました。
(※部分的に書き換えてあるので、実際には動作していないコードです)

def fetch_companies(setting_id)
  setting = Setting.find_by(id: setting_id)
  if setting
    companies = Company.where(setting.kind)
    companies
  end
end

処理としては指定されたidSettingデータを取得して、Settingに記録されているkindを持つCompanyの一覧を取得するという単純なものです。ActiveRecord(O/RMapper)を使ったデータベースとのやりとりを行う処理です。
動作自体には何の問題もないのですが、この関数は指定したidを持つ、Settingの値が見つかった時と見つからなかった時で返す値の型が異なります。

RubyElixirでは明示的にreturn構文を書かなくても最終行に記述されている値が自動で返ります。現時点では以下のように値が返ります。

  • Settingが見つかった時 -> Companyの配列 もしくは空配列
    • whereは1件もヒットしなかった場合に空配列を返します
  • Settingが見つからなかった時 -> nil
def verification
end

res = verification
p res # nil

この関数に型アノテーションをしてあげるとすると[Company] | nilという感じになります。

何が問題なのか🤔

状態によって返る値の型が異なることが分かりましたが、これがどのような問題につながるでしょうか。
オブジェクト指向をサポートする言語でよくある例としてはfetch_companiesの戻り値に対してmapfilterを適応したい場合です。

resp_companies = fetch_companies(6)
extract_names = resp_companies.map { |company| company.name }

このコードはSettingが存在していた場合にCompanyの配列が返ってきてくれた場合には上手く動作してくれますが、Settingが見つからなかった場合にはどうでしょうか。先ほど記述したように、その場合にはnilが返ってきます。nilにはmap関数は実装されていないので、当然ですがnil::NilClassエラーになります。

nil.map { |company| company.name }
# undefined method `map' for nil:NilClass (NoMethodError)

この問題を回避するために、fetch_companiesの呼び出し元で戻り値を精査する必要があります。

resp_companies = fetch_companies(6)
return [] if resp_companies.empty? 
extract_names = resp_companies.map { |company| company.name }

fetch_companiesを呼び出す箇所が少なければ良いのでしょうが、たくさんあったとするならば、この単調な判定を各所に記述するのは面倒です。

解決策📖

解決方法はシンプルで静的型付け言語で当たり前のように行う、関数の戻り値の型を統一してあげれば良いです。
今回の場合では空配列を返すのが適切です。これでSettingの有無に関わらず、全ての場合で空配列か、Companyの配列が返るようになります。

# fetch_companies :: number -> [Company] 
def fetch_companies(setting_id)
  setting = Setting.find_by(id: setting_id)
  return [] if setting.nil?

  Company.where(setting.kind)
end

心なしかifのネストも無くなりコードの見栄えも良くなりました。必ず配列が返るので先程の問題は発生しません。 合わせて、呼び出し元でnilが返ってくる場合を考えなくて良くなりました。やりました。関数の戻り値の型を揃えるのは良いことだらけです🎉

xn--97-273ae6a4irb6e2hsoiozc2g4b8082p.com