何番煎じですかってお題ですが、地味に詰まったので言葉にして残してみます。
はじめに: 筆者について
Ruby歴2ヶ月ほどで、バックエンドの開発が得意です。Javaのエンジニアからキャリアをスタートし、最近1年半くらいはサーバーサイドKotlinの導入やプロダクション用のビルドやアプリ基盤開発などに従事してきました。個人的な趣味としても、規約よりも設定、型によるモデルの実装といった堅牢だが冗長な実装を好む傾向にあります。
そのような背景がありますので、型やnullの状態がコンパイル時にわかるような気で、 "Java屋的な" 発想でRubyを書いてしまいがちです。
お題: 小数点付きならエラー
簡単な修正の担当となり、「小数点付きならエラー」を実装しようとしていました。
以下の修正が結論です。変数名などはぼかして書いています。
do_on_error unless !@subject&.is_a(Integer)
インスタンスの型もnullabilityも明示的ではない
はじめに気づいたことは、検査対象は数値のインスタンスとは限らないということです。Integerクラスのドキュメントを見ておりましたが、そのような前提があるので Integer#integer?
は使えないということです。当たり前のことですが... Integer#parseInt をtry-catchで囲いたいなあなんて思っておりました。
最初に考えた方針には、次の2つの落とし穴があります(指摘されるまでもなく自分ですぐに気付けたけど恥ずかしい)
- 「小数点付きなら」 は、要件としては「整数であること」であること
- そもそもnilかもしれないことを考慮する必要がある
1については、要件が前提条件にあるのでここでは簡単にだけ触れますが、「小数点付きであること」と「整数であること」は等価ではないです。そして、Rubyにおける数値系データ型の型階層は Numeric
を中心に整数と小数点付きに分かれております。つまり、 Integer
と Float
同士は is_a にも has_a の関係にもなっていない。
Numeric
|_ Integer
|_ Float
冒頭にも書いたように、そもそも検査対象の型が不明の状態では、 integer?
をインスタンスメソッドとしてコールすることで NoMethodError
になる可能性が拭えません。そのため、対象の型を判定します。
# nilないしはIntegerのインスタンス以外ではNoMethodErrorになるかも
@subject.integer?
また、当初検査対象がnilかもしれないことを考慮しきれていませんでした。実践的には、nilないしは空文字でないことをバリデーションした状態で、ビジネスロジックとして整数判定を入れるかもしれないですが、一般的にnil判定も含めたほうが安全です。
nil判定を型判定と独立してやってもいいですが、safe navigation operator(通称ぼっち演算子)あるじゃんと教えていただき、まとめて判定するようにしました(アドバイスありがとうございました)。
# 否定を2回もしていてツライ
!@subject.nil? and !@subject.is_a?(Integer)
おまけ: safe navigation operator付きでは逆にならない
否定 !
付きで呼び出していたので運良く(?)要件通り判定できましたが、safe navigation operatorはレシーバがnilの場合メソッドコールせず nil
を返して終わることにも注意を要します(Boolean
にならない)。
# レシーバがnil
>> subject = nil
>> !subject&.is_a?(Integer)
=> true
# コールされないのでbooleanが返ってこない
>> subject&.is_a?(Integer)
=> nil
よく考えたら難しいことではないですが、否定 !
で裏返せばtrueとfalseが簡単に裏返るわけではないことも明示的ではなかったということです。
まとめ
Rubyistの皆様からすれば至極単純な実装なはずですが、 型やnullablityがコード上で明確な世界から来ると
- 変数の型が明示的でない -> 安直にインスタンスメソッドをコールできない
- 常にnilないしは空文字かもしれないこと
といった前提条件から逃れられないことは改めて学びになりました。kotlinやtypescriptにもnull safe operatorやoptional chainingなど似たような言語仕様がありますが、そもそもnullにならないよう型宣言するようにしていたのですっかり抜けていたのもあります。