HN 日本語サマリー

← 一覧へ戻る
プログラミング

Moment of Ruby: 何かについての議論

Moment of Ruby: An Argument for Nothing (aha.io)

53 pointsby zemptime11 コメント

要約

この記事では、Rubyのメソッドがゲッターとセッターの両方として機能する際に、`false`や`nil`を値として設定したい場合に発生する問題を解決する、Rubyの興味深いテクニックを紹介しています。デフォルト引数式内で代入を行い、その結果を評価することで、引数が省略された場合と明示的に値が渡された場合を区別する巧妙な方法が解説されています。このパターンはRailsの`index_with`メソッドでも使用されています。

全文翻訳

Moment of Ruby: 何かについての議論

先週、Rubyで非常に小さくても役立つテクニックを学びましたが、以前に出会ったことがなかったことに驚きました。

メソッドがゲッターとセッターの両方として機能すると仮定します。値を渡して設定を更新するか、何も渡さずに読み取ります。

module Cloakable
  def cloaking(value = nil)
      if value
       @setting = value
      else
       @setting
      end
  end
end

これは、falseやnilを値として設定したい場合には機能しなくなります。

class Spaceship
  extend Cloakable
  
  cloaking(true) # 隠す。
  cloaking(false) # こんにちはと手を振る。
  cloaking(nil) # 設定をクリアする
  cloaking # 振る前にチェックする
end

既存の動作を壊すことなく、最後のケースを考慮するようにメソッドの契約を拡張するにはどうすればよいでしょうか? value.nil?をチェックしても、問題全体は解決しません。明示的なnilと省略された引数は、依然として同じように見えます。

これを行う方法の1つは次のとおりです。

module Cloakable
  NOT_PROVIDED = Object.new.freeze

  def cloaking(value = NOT_PROVIDED)
    unless value.equal?(NOT_PROVIDED)
      @setting = value
    end

    @setting
  end
end

しかし、そこまでする必要はありません。ここにトリックがあります。

module Cloakable
  def cloaking(value = (nothing_provided = true))
    return @cloaking if nothing_provided
      
    @cloaking = value
 end
end

Rubyは、デフォルト引数式は引数が省略された場合にのみ評価します。そして、その式には代入を含めることができます。

引数なしでsettingを呼び出すと、nothing_provided = trueが実行されます。代入もtrueと評価され、それがvalueのデフォルト値になります(ただし、このブランチでは使用しません)。

引数を指定した場合、たとえfalseやnilであっても、Rubyはデフォルト式をスキップします。nothing_providedはローカル変数として認識されますが、代入が実行されなかったため、その値はnilになります。

これで、4つの呼び出しすべてを区別できるようになります。

  cloaking(true)  # trueを格納する。
  cloaking(false) # falseを格納する。
  cloaking(nil)   # nilを格納する。
  cloaking        # 格納された値を返す。

このテクニックを最初に気づいたのは、Railsのindex_withの実装でした。

  def index_with(default = (no_default = true))
    if block_given?
      to_h { |elem| [elem, yield(elem)] } # rubocop:disable Rails/IndexWith
    elsif no_default
      to_enum(:index_with) { size if respond_to?(:size) }
    else
      to_h { |elem| [elem, default] } # rubocop:disable Rails/IndexWith
    end
  end

Rubyは、「値が供給された」場合と「何も供給されなかった」場合が、メソッドの契約の異なる部分であるということを区別するための便利な方法を提供してくれます。時には、何もしないことが、たとえそれについて少し議論する必要があったとしても、最も賢明なことなのです。

追加情報

この機能は元々byrootによってRails PR #44611で導入され、このパターンを使用する時期について追加のガイダンスを提供しました。

[画像: byroot: はい、Rubyコアで多く使用されています(CからRubyに移行したメソッドの場合、Cでは可変引数を効率的にチェックするのが容易なため)。セマンティック値は、引数が別のメソッドに渡されたり、ivarに格納されたりする場合に利点がありますが、それ以外の場合は、このパターンはやや優れています。]


エンジニアリングチームがRubyをどのように活用しているかについて、さらに詳しい洞察を得るには、Aha!エンジニアリングブログをお読みください。