🌟

【bugs.ruby Advent Calender】引数が渡されなかったことを判定する【1日目】

2024/12/01に公開

bugs.ruby Advent Calender 1日目の記事です。

これはなに

今年1年間通してみてきた bugs.ruby のチケットの中から気になったものを1つずつ取り上げていく Advent Calender です。
取り上げるチケットは基本的にこのブログで取り上げたものになります。
記事のまとめは ここを参照 してください。

[Feature #20326] Add an undefined for use as a default argument.

メソッドで『引数を受け取るんだけど引数がなかった場合に特別な処理を行いたい』みたいなことが稀にあります。
その場合、例えばデフォルト引数を nil にして nil かどうかで判定することが多いと思います。

class X
  def any_method(item = nil)
    if item.nil?
      raise "引数がありません!"
    end
    # ...
  end
end

ただ、上記の場合では x.any_method(nil)x.any_method() の呼び出しが区別できずに困ることもあります。
なので厳密な意味で『引数がないこと』を判定する場合は nil 以外の一意となるオブジェクトを使用することで回避することができます。

class X
  # 『未定義用』のオブジェクトを生成しておく
  UNDEFINED = Object.new
  private_constant :UNDEFINED

  def any_method(item = UNDEFINED)
    if item == UNDEFINED
      raise "引数がありません!"
    end
    # ...
  end
end

# no error
pp X.new.any_method(nil)

# error: 引数がありません! (RuntimeError)
pp X.new.any_method()

このチケットでは上記の UNDEFINED 相当の機能を Ruby の本体に組み込む提案になります。
具体的には以下のように利用できる機能を提案しています。

# Kernel に #undefined と #undefined? の定義を追加する
module Kernel
  UNDEFINED = Object.new
  def UNDEFINED.inspect = -'undefined'
  UNDEFINED.freeze
  private_constant :UNDEFINED

  def undefined? = self == UNDEFINED

  module_function

  def undefined = UNDEFINED
end

class X
  # #undefined と undefined? を利用してそれが未定義なオブジェクトかどうかを判定する
  def any_method(item = undefined)
    if item.undefined?
      raise "引数がありません!"
    end
    # ...
  end
end

これだけみるとなんかよさそうな雰囲気はあるのですがコメントでは『引数として undefined が渡せてしまう』と指摘されています。

# error: 引数がありません! (RuntimeError)
pp X.new.any_method(undefined)

また JavaScript の null undefined と同じような問題になるともコメントされていますねー。
あと個人的に目からウロコだったのが以下のように簡略的にかけると提示されています。

class X
  def any_method(item = (item_not_set = true))
    if item_not_set
      raise "引数がありません!"
    end
    # ...
  end
end

# no error
pp X.new.any_method(nil)

# error: 引数がありません! (RuntimeError)
pp X.new.any_method()

これはどういう動きをしているのかというと any_method に対してなにか引数が渡されると (item_not_set = true) の部分は評価されません。
評価されないとどういう状態になるのかというと item_not_set というローカル変数は定義されているのですが値が未割り当ての状態なので item_not_set の値は nil になります。

class X
  def any_method(item = (item_not_set = true))
    { item:, item_not_set: }
  end
end

pp X.new.any_method(42)
# => {:item=>42, :item_not_set=>nil}

一方で any_method に対して引数を渡さなかった場合には (item_not_set = true) が評価されるので item_not_set の値は true になります。

class X
  def any_method(item = (item_not_set = true))
    { item:, item_not_set: }
  end
end

pp X.new.any_method()
# => {:item=>42, :item_not_set=>true}

これは Ruby のことを熟知しているような使い方で芸術点が高いですね。
それ以外にもいくつかアイディアがコメントに書いてあるので気になる人はチケットの内容を読んでみるとよいと思います。

関連

GitHubで編集を提案

Discussion