LoginSignup
0
0

More than 1 year has passed since last update.

`#coerce` を refinements で追加してもうまく使われない

Posted at

1 + nil などがエラーでなく nil を返すようにしたかったが、 refinements で NilClass#coerce を生やす方法はうまくいかなかった。たぶん refinements の有効範囲的に仕方ない?

試したコード

Ruby の数値クラスの二項演算は、レシーバー(演算子の左側)の知らないクラスが相手(演算子の右側、第1引数)である場合、相手の #coerce を呼び出してクラスを変換してもらう仕組みになっている。それにより、相手のクラスに二項演算を任せることができる。

というわけで、二項演算で右側を nil にしたいなら、数値クラスは触らず nil のほうに変換と二項演算を定義すればいい。ひとまず +- (とついでに単項演算)に対応させてみた。

module NullableOperators
  refine NilClass do
    def coerce(other)
      [nil, self]  # 注: <=> については考慮していない
    end

    %i[+@ -@ + -].each do |name|
      define_method(name) { |*| nil }
    end
  end
end

しかし使ってみると、レシーバーが nil のとき( nil + 1 など)はうまくいくが、逆のときはエラーになってしまった。

using NullableOperators

nil + 1  #=> nil
-nil     #=> nil
nil.coerce(1)  #=> [nil, nil]

1 + nil  # nil can't be coerced into Integer (TypeError)

原因

公式ドキュメントの refinements の説明を見てみると、スコープについて書いてあった。

Refinements are lexical in scope. Refinements are only active within a scope after the call to using. Any code before the using statement will not have the refinement activated.

When control is transferred outside the scope, the refinement is deactivated. This means that if you require or load a file or call a method that is defined outside the current scope the refinement will be deactivated:

(コード例)

using の有効な範囲はコードの構造で決まり(静的スコープ)、他のファイル内のコードや using より前に定義されたメソッド内では、たとえ using 実行後に呼び出したとしても refinements が無効となる。組み込みの数値クラスの二項演算は明らかに別の場所で定義されているので、 refinements の効果が及ばないということになる。

代案

refinements を使う限りは、 #coerce の仕組みを使わずやるしかなさそう。

数値クラスも refine する

#coerce を呼ばないよう、使う予定の数値クラスの二項演算を改造する。メソッド内で super を呼べば、 refine 前のメソッドを呼び出せる1

module NullableOperators
  unary = %i[+@ -@]
  binary = %i[+ -]

  refine NilClass do
    unary.each do |name|
      define_method(name) { nil }
    end

    binary.each do |name|
      define_method(name) { |_| nil }
    end
  end

  [Integer, Rational, Float, Complex].each do |klass|
    refine klass do
      binary.each do |name|
        define_method(name) { |other| other.nil? ? nil : super(other) }
      end
    end
  end
end
using NullableOperators

nil + 1  #=> nil
-nil     #=> nil

1 + nil  #=> nil
1 + 2.0  #=> 3.0

NaN で済ませる

今回の場合はただ「他の数値と演算しても変化しない」ものが欲しかったので、 nil にこだわらなければ浮動小数点数の特別な値 NaNFloat::NAN など)が使える。元々ある仕組みなので refinements も何も必要ない。

NaN には種類があるとか、 NaN 同士の等値性判定が偽になるとか、 nil とは異なる面もあるので細かいところは注意がいる。

nan = Float::NAN

nan + 1  #=> NaN
-nan     #=> NaN ※符号はマイナス
nan.coerce(1)  #=> [1.0, NaN]

1 + nan  #=> NaN
  1. refine の中でモジュールを include している場合は、モジュールのメソッドが優先される。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0