Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

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
    }
}

こうすることで、SomeNoneの両方のケースを必ず処理するよう強制されます。

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らしい書き方とは言えませんが、改良を重ねればより快適な構文が生まれるのではないでしょうか。

  1. Rubyのラムダ徹底解説――基本から実践的な応用テクニックまで

    Rubyにおいてブロックは非常に重要な存在であり、ブロックなしにこの言語を語ることはできません。では、ラムダはどうでしょうか?ラムダを愛している人はいるのでしょうか?実際のところ、何年もラムダを使わずに過ごすことだって可能かもしれません。まるで昔の時代の遺物のように見えることもあるでしょう。 しかし、それは完全な真実ではありません。少し掘り下げてみると、ラムダには興味深い技が数多く隠されているのです。 この記事では、まずラムダの基本的な使い方から始め、その後、より高度で面白い応用へと進んでいきます。すでに毎日ラムダを使いこなしている方は、ぜひ後半の応用編までスクロールしてください。 ラムダにつ

  2. 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)のように終端