Rubyのnilは危険:RustのOption型に学ぶ安全な値の処理方法
ここ半年ほど、私はRustでNESエミュレータの開発に取り組んできました。予想どおり、Rustについては多くを学びましたし、NESの内部構造に関してはさらに多くを学びました。しかし、この経験は私がRubyを見る目まで変えてしまったのです。
具体的に言えば、nilを返すメソッドに対して、かなりの疑心暗鬼に陥るようになりました。
何かを信じない者は、何にでも騙される
Rubyにおいてnilとは一体何を意味するのでしょうか? 答えは「ほぼ何でもあり」です。メソッドがnilを返すとき、それは次のような意味があり得ます。
- メソッドに戻り値が存在しない
- 通常は戻り値があるが、今回はたまたまない
- データベースから取得した値がNULLである
- 何か予期しないことが起きた
この曖昧さこそがコードを読みにくくする元凶であり、Rubyで最も頻発する例外NoMethodErrorの主な原因となっています。例外監視サービスの共同オーナーとして言わせていただければ、NoMethodErrorは我が子を学校に通わせてくれるほど発生しているのです。
次のコードを見てください。このメソッドはほとんどの場合nilを返します。条件式がマッチせず、かつelse節がない場合、if文はnilと評価されるためです。
def color_name(rgb)
if rgb == 0x000000
"black"
end
end
color_name("#FFFFFF").titleize
# => NoMethodError: undefined method `titleize' for nil:NilClass
経験豊富なRuby開発者であれば、この落とし穴がどれほど深いかをご存じでしょう。時にはnilの持つ複数の意味が奇妙な形で重なり合い、たとえばデータベース内の値がNULLなのか、それともそもそもレコード自体が存在しないのかを判別できなくなることさえあります。
より良いアプローチ
Rustにはnilのような概念は存在しません。代わりに、関数が値を返す場合と「何も返さない」場合があることを表現したいときは、Option型を使用します。
Optionとは、特定の値を保持しているか、あるいは値を一切保持していないかのどちらかである型です。コード上では次のように表現されます。
Option::Some(42); // 数値42をOptionで包む
Option::None; // 「結果なし」を示す
これだけでも、場当たり的なnilの使い方より優れているように見えますが、さらに嬉しいことがあります。RustのコンパイラはNoneケースへの対処を強制するのです。うっかり無視することは許されません。
match my_option {
Some(x) => do_something_with_x(x),
// 下の`None`マッチを削除すると、
// このコードはコンパイルできません。
None => do_the_default_thing()
}
したがって、先ほどの色名の例はRustでは次のように書けます。
fn color_name(rgb: u32) -> Option<String> {
if rgb == 0x000000 {
Some("black".to_owned())
} else {
None
}
}
こうすることで、SomeとNoneの両方のケースを必ず処理するよう強制されます。
let name = color_name(0xFFFFFF);
let name = match color_name(0xFFFFFF) {
Some(value) => value,
None => "unknown".to_owned(),
}
確かに少々冗長で、奇妙に見えるかもしれません。しかし、関数が有用な値を返さないケースを無視することが不可能になります。その結果として生まれるのは、理解しやすく保守しやすいコードです。
RubyでOptionを実装してみる
Rubyに欠けている厳密さは、その柔軟性で補うことができます。Optionのような仕組みをRubyで実装してみるのは面白そうです。
Rubyはインタプリタ言語なので、コンパイル時エラーを発生させることはできません。しかし、誤ったコードがエッジケースに達したときだけ例外を投げるのではなく、常に例外を発生させるようにすることは可能です。
まず、2つのクラスを作成しましょう。Someは読み取り専用の値を保持し、Noneは空です。見た目どおりのシンプルな作りです。
class Some
attr_reader :value
def initialize(value)
@value = value
end
end
class None
end
次に、SomeまたはNoneを内部に保持し、両方のケースに対するハンドラが提供されたときにのみ値へアクセスできるOptionクラスを作ります。
class Option
def initialize(value)
@value = value
end
def self.some(value)
self.new(Some.new(value))
end
def self.none()
self.new(None.new)
end
def match(some_lambda, none_lambda)
if @value.is_a?(Some)
some_lambda.call(@value.value)
elsif @value.is_a?(None)
none_lambda.call()
else
raise "Option value must be either Some or None"
end
end
end
最後に、色名の例を新しいOptionクラスを使って書き直してみましょう。
def color_name(rgb)
if rgb == 0x000000
Option.some("black")
else
Option.none()
end
end
puts color_name(0x000000).match(
-> value { value },
-> { "no match" })
# "black"と出力される
まとめ
この手法はまだ実際のプロジェクトで試してはいませんが、本番環境に紛れ込んでしまう数々のNoMethodErrorを防げる可能性は十分にあると感じています。見た目はやや煩雑で、あまりRubyらしい書き方とは言えませんが、改良を重ねればより快適な構文が生まれるのではないでしょうか。
-
Rubyのラムダ徹底解説――基本から実践的な応用テクニックまで
Rubyにおいてブロックは非常に重要な存在であり、ブロックなしにこの言語を語ることはできません。では、ラムダはどうでしょうか?ラムダを愛している人はいるのでしょうか?実際のところ、何年もラムダを使わずに過ごすことだって可能かもしれません。まるで昔の時代の遺物のように見えることもあるでしょう。 しかし、それは完全な真実ではありません。少し掘り下げてみると、ラムダには興味深い技が数多く隠されているのです。 この記事では、まずラムダの基本的な使い方から始め、その後、より高度で面白い応用へと進んでいきます。すでに毎日ラムダを使いこなしている方は、ぜひ後半の応用編までスクロールしてください。 ラムダにつ
-
Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説
Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端