Rubyでand/orのようなものを実装する方法

Rubyでは再定義できる演算子と再定義できない演算子があります。
再定義できる演算子はメソッドとして実装されているのでオーバーライドできます。
再定義できない演算子は制御構造なのでオーバーライドできません。

参考: https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html

この記事では`and`と`or`のような動作をするメソッドを実装してみようと思います。


メソッドとして実装してみる
----

まずは普通にメソッドとして書いてみます。

```and_or1.rb
module AndOr
  refine(Object) do
    def and(o)
      if self
        o
      else
        self
      end
    end

    def or(o)
      if self
        self
      else
        o
      end
    end
  end
end

return unless $0 == __FILE__

require 'test/unit'

class TestAndOr < Test::Unit::TestCase
  using AndOr

  def test_return_value
    assert { (true  and nil  ) == true  .and(nil  ) }
    assert { (true  and false) == true  .and(false) }
    assert { (true  and true ) == true  .and(true ) }

    assert { (true  or  nil  ) == true  .or(nil   ) }
    assert { (true  or  false) == true  .or(false ) }
    assert { (true  or  true ) == true  .or(true  ) }

    assert { (false and nil  ) == false .and(nil  ) }
    assert { (false and false) == false .and(false) }
    assert { (false and true ) == false .and(true ) }

    assert { (false or  nil  ) == false .or(nil   ) }
    assert { (false or  false) == false .or(false ) }
    assert { (false or  true ) == false .or(true  ) }

    assert { (nil   and nil  ) == nil   .and(nil  ) }
    assert { (nil   and false) == nil   .and(false) }
    assert { (nil   and true ) == nil   .and(true ) }

    assert { (nil   or  nil  ) == nil   .or(nil   ) }
    assert { (nil   or  false) == nil   .or(false ) }
    assert { (nil   or  true ) == nil   .or(true  ) }
  end
end
```

実行します。

```console
% ruby and_or1.rb
Loaded suite and_or1
Started
.

Finished in 0.017493 seconds.
-----------------------------------------------------------------------------------
1 tests, 18 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
-----------------------------------------------------------------------------------
57.17 tests/s, 1028.98 assertions/s
```

はい。

短絡評価
----

`and`や`or`は短絡評価です。

`and`の説明をるりまから引用

> 左辺を評価し、結果が偽であった場合はその値(つまり nil か false) を返します。左辺の評価結果が真であった場合には 右辺を評価しその結果を返します。 and は同じ働きをする優先順位の低い演算子です。
https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html#and

`or`の説明をるりまから引用

> 左辺を評価し、結果が真であった場合にはその値を返します。 左辺の評価結果が偽であった場合には右辺を評価し その評価結果を返します。 or は同じ働きをする優先順位の低い演算子です。
https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html#or

以下のようなプログラムでは`raise`が評価されることはないため、例外は発生しません。

```short-circuit.rb
true or raise
false and raise
```

短絡評価のテストを追加してみる
----

先程の実装に短絡評価のテストを追加してみます。

```and_or2.rb
module AndOr
  refine(Object) do
    def and(o)
      if self
        o
      else
        self
      end
    end

    def or(o)
      if self
        self
      else
        o
      end
    end
  end
end

return unless $0 == __FILE__

require 'test/unit'

class TestAndOr < Test::Unit::TestCase
  using AndOr

  def test_return_value
    assert { (true  and nil  ) == true  .and(nil  ) }
    assert { (true  and false) == true  .and(false) }
    assert { (true  and true ) == true  .and(true ) }

    assert { (true  or  nil  ) == true  .or(nil   ) }
    assert { (true  or  false) == true  .or(false ) }
    assert { (true  or  true ) == true  .or(true  ) }

    assert { (false and nil  ) == false .and(nil  ) }
    assert { (false and false) == false .and(false) }
    assert { (false and true ) == false .and(true ) }

    assert { (false or  nil  ) == false .or(nil   ) }
    assert { (false or  false) == false .or(false ) }
    assert { (false or  true ) == false .or(true  ) }

    assert { (nil   and nil  ) == nil   .and(nil  ) }
    assert { (nil   and false) == nil   .and(false) }
    assert { (nil   and true ) == nil   .and(true ) }

    assert { (nil   or  nil  ) == nil   .or(nil   ) }
    assert { (nil   or  false) == nil   .or(false ) }
    assert { (nil   or  true ) == nil   .or(true  ) }
  end

  def test_shortcircuit
    assert_nothing_raised { (true  or  NIL()  ) == true  .or(NIL()   ) }
    assert_nothing_raised { (true  or  FALSE()) == true  .or(FALSE() ) }
    assert_nothing_raised { (true  or  TRUE() ) == true  .or(TRUE()  ) }

    assert_nothing_raised { (false and NIL()  ) == false .and(NIL()  ) }
    assert_nothing_raised { (false and FALSE()) == false .and(FALSE()) }
    assert_nothing_raised { (false and TRUE() ) == false .and(TRUE() ) }

    assert_nothing_raised { (nil   and NIL()  ) == nil   .and(NIL()  ) }
    assert_nothing_raised { (nil   and FALSE()) == nil   .and(FALSE()) }
    assert_nothing_raised { (nil   and TRUE() ) == nil   .and(TRUE() ) }
  end

  private

  def NIL
    raise 'nil should not be called'
  end

  def TRUE
    raise 'true should not be called'
  end

  def FALSE
    raise 'false should not be called'
  end
end
```

はい。

```console
% ruby and_or2.rb
Loaded suite and_or2
Started
.F
===================================================================================
Failure:
  Exception raised:
  RuntimeError(<nil should not be called>)
test_shortcircuit(TestAndOr)
and_or2.rb:55:in `test_shortcircuit'
     52:   end
     53:
     54:   def test_shortcircuit
  => 55:     assert_nothing_raised { (true  or  NIL()  ) == true  .or(NIL()   ) }
     56:     assert_nothing_raised { (true  or  FALSE()) == true  .or(FALSE() ) }
     57:     assert_nothing_raised { (true  or  TRUE() ) == true  .or(TRUE()  ) }
     58:
===================================================================================


Finished in 0.032972 seconds.
-----------------------------------------------------------------------------------
2 tests, 19 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
50% passed
-----------------------------------------------------------------------------------
60.66 tests/s, 576.25 assertions/s
```

メソッドの引数は正格な評価がされるようで、メソッドの実行前に評価されてしまいます。

ブロックを使って評価されるのを回避する
----

ブロックを使うと、ブロックを実行するまでブロックの中身が評価されるのを遅延させられます。

```and_or3.rb
module AndOr
  refine(Object) do
    def and
      if self
        yield
      else
        self
      end
    end

    def or
      if self
        self
      else
        yield
      end
    end
  end
end

return unless $0 == __FILE__

require 'test/unit'

class TestAndOr < Test::Unit::TestCase
  using AndOr

  def test_return_value
    assert { (true  and nil  ) == true  .and { nil   } }
    assert { (true  and false) == true  .and { false } }
    assert { (true  and true ) == true  .and { true  } }

    assert { (true  or  nil  ) == true  .or  { nil   } }
    assert { (true  or  false) == true  .or  { false } }
    assert { (true  or  true ) == true  .or  { true  } }

    assert { (false and nil  ) == false .and { nil   } }
    assert { (false and false) == false .and { false } }
    assert { (false and true ) == false .and { true  } }

    assert { (false or  nil  ) == false .or  { nil   } }
    assert { (false or  false) == false .or  { false } }
    assert { (false or  true ) == false .or  { true  } }

    assert { (nil   and nil  ) == nil   .and { nil   } }
    assert { (nil   and false) == nil   .and { false } }
    assert { (nil   and true ) == nil   .and { true  } }

    assert { (nil   or  nil  ) == nil   .or  { nil   } }
    assert { (nil   or  false) == nil   .or  { false } }
    assert { (nil   or  true ) == nil   .or  { true  } }
  end

  def test_shortcircuit
    assert_nothing_raised { (true  or  NIL()  ) == true  .or { NIL()   } }
    assert_nothing_raised { (true  or  FALSE()) == true  .or { FALSE() } }
    assert_nothing_raised { (true  or  TRUE() ) == true  .or { TRUE()  } }

    assert_nothing_raised { (false and NIL()  ) == false .and { NIL()   } }
    assert_nothing_raised { (false and FALSE()) == false .and { FALSE() } }
    assert_nothing_raised { (false and TRUE() ) == false .and { TRUE()  } }

    assert_nothing_raised { (nil   and NIL()  ) == nil   .and { NIL()   } }
    assert_nothing_raised { (nil   and FALSE()) == nil   .and { FALSE() } }
    assert_nothing_raised { (nil   and TRUE() ) == nil   .and { TRUE()  } }
  end

  private

  def NIL
    raise 'nil should not be called'
  end

  def TRUE
    raise 'true should not be called'
  end

  def FALSE
    raise 'false should not be called'
  end
end
```

はい。

```console
% ruby and_or3.rb
Loaded suite and_or3
Started
..

Finished in 0.019147 seconds.
-----------------------------------------------------------------------------------
2 tests, 27 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
-----------------------------------------------------------------------------------
104.46 tests/s, 1410.14 assertions/s
```

ブロックは評価のタイミングをずらしたいとき便利
----

`and`/`or`の短絡評価のように、特定の条件によって評価したりしなかったりする動作を実装するとき、ブロックを使うと評価のタイミングをずらせて便利です。