Ruby: シングルトンオブジェクトをデフォルト引数として使う(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Ruby: シングルトンオブジェクトをデフォルト引数として使う(翻訳)

オプション引数を1つ取るメソッドを定義したくなることがときどきあるかと思いますが、プログラマーがこのメソッドにnilを渡す可能性がないわけではありません。かつコードの側では、値が提供されない(つまりデフォルト値にする)場合とnilの場合を区別する必要があるとします。どうやったらこれをうまくさばけるでしょうか?

こういう場合によく使われるのは、nilやその他の空値やゼロ値をデフォルト値としてメソッドを定義するというものです。0や文字列、空の配列などの場合であれば、この方法は理にかなっています。

class Foo
  def bar(one, two: nil)
    # ...
  end
end

しかし、nilと、値が指定されていない場合を区別する必要がある場合はどうしたらよいのでしょうか。具体的には、次の2つを区別する方法です。

foo.bar(:something, two: nil)
foo.bar(:something)

こんな場合に使えるのは、単一かつ一意のオブジェクトを定義して、それをデフォルトに用いる方法です。そして、渡された引数がnilかどうかをチェックするのではなく、シングルトンオブジェクトであるかどうかをチェックします。

class Foo
  NOT_PROVIDED = Object.new

  def bar(one, two: NOT_PROVIDED)
    puts one.inspect
    if two == NOT_PROVIDED
      puts "not provided"
    else
      puts two.inspect
    end
  end

  private_constant :NOT_PROVIDED
end

private_constantは無理して使う必要はありませんが、私はこのメソッドを引数に使えることと、privateクラスでも同じようなことができることをRuby開発者に思い出していただきたいのです。

Foo.new.bar(1)
1
not provided

Foo.new.bar(1, two: 2)
1
2

:not_providedのようにシンボルを使ってもよいですし、数値などRuby内部で一意であるものなら何でも構いません。しかし後述のassert_changesのような一般的なメソッドでは、引数に有効なオブジェクトが渡される可能性もあります。最善の方法は、引数として受け渡せない一意のオブジェクトを使うことです。

以下は、Railsでこれを用いてassert_changesを実装する方法です。

assert_changes :@object, from: nil, to: :foo do
  @object = :foo
end

assert_changes -> { object.counter }, from: 0, to: 1 do
  object.increment
end
UNTRACKED = Object.new
def assert_changes(expression, message = nil, from: UNTRACKED, to: UNTRACKED, &block)
  exp = if expression.respond_to?(:call)
    expression
  else
   -> { eval(expression.to_s, block.binding) }
  end

  before = exp.call
  retval = yield

  unless from == UNTRACKED
    error = "#{expression.inspect} isn't #{from.inspect}"
    error = "#{message}.\n#{error}" if message
    assert from === before, error
  end

  after = exp.call

  if to == UNTRACKED
    error = "#{expression.inspect} didn't changed"
    error = "#{message}.\n#{error}" if message
    assert_not_equal before, after, error
  else
    error = "#{expression.inspect} didn't change to #{to}"
    error = "#{message}.\n#{error}" if message
    assert to === after, error
  end

  retval
end

私はどうやら、RSpecのこのアプローチが好みです。

expect do
  object.increment
end.to change{ object.counter }.from(0).to(1)

しかしその一方で、UNTRACKEDオブジェクトを用いるassert_changes実装もありだと思います。

しかしこれは、論理値(boolean)引数にどこか似ています。これは、2つの異なるオブジェクトで定義されるべきフラグとしてよく用いられます。そこで、foo(1, true)foo(1, false)よりも、単にfoo(1)bar(1)とする方がよいとされることが多く、私も普通はこのガイドラインに従います。ただしassert_changesの場合であれば、名前付き引数とシングルトンオブジェクトの組み合わせはよいのではないかと思います。

お知らせ: もっと知りたい方へ

本記事を気に入っていただけた方は、Arkencyのニュースレターの購読をお願いします。開発者を悪い意味で驚かせないRailsアプリを構築するための弊社のノウハウを毎日お送りします。

よろしければ以下もどうぞ(訳注: いずれも英語記事です)。

弊社の最新刊『Domain-Driven Rails』もぜひどうぞ。特に、巨大で複雑なRailsアプリを扱ってる方に有用です。

ツイートより

関連記事

Rails: モデルの外では名前付きスコープだけを使おう(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ