Ruby のパターンマッチを利用して任意のメソッドが定義されているかどうかを判定する

と、いうのが bugs.ruby に来ていたので。

class Runner
  def run
  end

  def stop
  end
end

runner = Runner.new
case runner
in .run & .stop
  :reachable
in .start & .stop
  :unreachable
end

ほしい気持ちはわかるんだけど流石に上記の提案だと情報が欠落しすぎているのとそもそもパターンマッチでやるべきこと?と思ってしまい個人的にはいまいち。
なんかもっといい感じの構文だといいとは思うんですが…。

と、言うことで既存のパターンマッチで出来ないかやってみました。

using Module.new {
  refine Object do
    # パターンマッチ内部では #deconstruct_keys を暗黙的に呼び出してそれで判定を行っている
    # そこで deconstruct_keys を経由してメソッド情報を取得することでパターンマッチで利用できるようにする
    # { メソッド名: 値... } となるような Hash を返す
    def deconstruct_keys(keys)
      keys.select { |name| respond_to?(name) }.to_h { |key| [key, send(key)] }
    end
  end
}

def check(obj)
  case obj
  in { run: _, stop: _ }
    :reachable
  in { start: _, stop: _ }
    :unreachable
  else
    :none
  end
end


class Runner
  def run
  end

  def stop
  end
end

runner = Runner.new
p check(runner)
# => :reachable


class Runner2
  def start
  end

  def stop
  end
end

runner2 = Runner2.new
p check(runner2)
# => :reachable

Ruby のパターンマッチでは暗黙的に #deconstruct_keys (や #deconstruct)が呼び出され、その戻り値を参照してパターンマッチを評価します。
上記の実装では { メソッド名: 値... } というような Hash をパターンマッチで使用できるようにすることで

  case obj
  in { run: _, stop: _ }
    :reachable
  in { start: _, stop: _ }
    :unreachable
  else
    :none
  end

というようなパターンマッチをかけるようにしています。
これならメソッドが定義されているかどうかを判定することも出来ますし『任意のメソッドの値』をキャプチャすることも出来ます。

  # obj.run の値をキャプチャする
  case obj
  in { run: status, stop: _ }
    p "status is #{status}"
    :reachable
  in { start: status, stop: _ }
    p "status is #{status}"
    :unreachable
  else
    :none
  end

これ、かなり汎用性が高そうなので普通にほしい。ってか、すでに機能としてありそう。

参照