RubyのMethodオブジェクトをJavaScriptのfunctionと比較する

これ作ってる時に知ったこととその後思ったこと。

RubyのMethodオブジェクトって知ってる?

こういうことができるもの。

class Foo
  def initialize(external_method)
    @external_method = external_method
    @foo = "foo"
  end
  attr_reader :external_method
end

class Bar
  def initialize
    @bar = "bar"
  end

  def bars_method
    @bar
  end
end

p Foo.new(Bar.new.method(:bars_method)).external_method.call
# => "bar"
  1. Object#method メソッドに Symbol か String で名前を与えるとそのメソッドの実体が Method オブジェクトとして取得できる
  2. Method オブジェクトは Proc のように call できる
  3. Method オブジェクトはレシーバも持っているので、Method オブジェクト側のコンテキストを利用できる(呼び出す側のコンテキストは引数で与えれば両方利用できる)

これ結構便利で、

オブジェクト丸ごとを他のオブジェクトに渡さず、メソッドだけ渡してそのメソッドを呼び出せますよ、の意思表示として使える1

Ruby だと duck type という考え方で、respond_to? 任せで、メソッドがあればそれを呼ぶスタイルがポピュラーだと思う。その場合、メソッドを渡すわけではなく、丸ごとのオブジェクトを渡すスタイルになるので、Method オブジェクトの登場機会はあんまりないんじゃないかと思う。

ワタクシ25年以上 Ruby 使ってますが、初めて使ったもんね。

cf.

JavaScriptってもっと楽にfunction投げられるよね

class Foo {
  constructor (externalMethod) {
    this.externalMethod = externalMethod
  }
}

class Bar {
  constructor () {
    this.bar = 'bar'
  }

  barsMethod () {
    return this.bar
  }
}

const bar = new Bar()
const foo = new Foo(bar.barsMethod)
console.log(foo.externalMethod.call(bar))
// => "bar"
  1. JavaScript は () が function call の役割を担っていて、() を付けない場合はfunction の reference を取得できる
  2. function の reference に対して call で実行する2
  3. その際、call に this として解釈するオブジェクトを与える

形になる。確かにメソッドを与える部分は簡単に書ける。ただ問題は 3 で、昔からずっと話題になる JavaScript の this の謎の一端だと思う。Ruby の Method オブジェクトと同じ挙動にするには、reference を取得する function の属しているインスタンスを call に渡してあげないといけない。ここで異なるオブジェクトを渡すと参照先が変わって undefined が返ってきたり TypeError になったりする。3

これが呼び出し側で変わると困る場合は

bar.barsMethod.bind(bar)

のように予め bind 済みの function を与えることもできるが、いずれにせよ、使うということを考えると注意して一手間掛けないといけないという意味では大差ない。

完全に脱線だけど、これをきらってそもそも this の binding がない arrow function を好む人たちが増えてるというのは、まぁ分からんではない。分からんではないけど、function だけでカタマリを表現するの、むしろ複雑さを隠蔽する言語の進化の逆を行っているようで、そりゃどうなんだろうなぁと思う。4

逆にRubyでJavaScriptのfunctionと同じことができないか?

結論から言うと同じように実行時に切り替えることはできないが、目的に応じて定義方法を変えておくことはできる

Ruby のメソッドは

  • 使いたいレシーバの中に取り込んで使う → Module の mixin
  • メソッドのレシーバを維持して使う → Method オブジェクト

という形で定義の時点で利用方法を考えておかないといけない。

Ruby では function じゃなくてメソッドであり、レシーバや周辺のコンテキスト込みの存在 なので、原則的には定義の段階で this ( Ruby では self ) が決まっている。

というか、改めて考えると実行時に this を動的に切り替えられる JavaScript やべーな。そりゃ混乱もするわ。

で、Methodオブジェクトだけど

使い方が分かったからと言ってそんなに使う機会は多くないかなーとは思う。まぁオブジェクトそのものを丸投げして「お前自身がどのメソッドを呼ぶか決めろ」とするよりは利用側にある程度制約を加えることができてるように見えるし、それを破ろうとしているかどうかは確認しやすいかな、くらい。

あと stub/mock が簡単そうではある。複雑なオブジェクトそのものを与える方式よりは内部構造に依存せずに stub/mock を実現しやすいし、多少中身が変化しても stub/mock 自体に手を加える必要性は低そう。その辺を気にするようならいいのかもしれない。

今回自分が使ったのは block の受け渡しが多発するコードで、block を受け取ってその block をどういう文脈で call するか、というケースだったので便利だったけど、通常の値しか考えないならあんまり必要性ないんじゃないかと思う。そう、Ruby ではそもそもメソッドじゃなくて Proc を受け渡ししてるんだよね、普段から。callback としてメソッドを渡す、という発想ではなく、ナチュラルに Proc を渡してやりたいことはできているので、この Method オブジェクトをわざわざ使う機会は通常は少ないままだと思う。

上のコードで利用したのは分割された役割を中継する gateway 的な役割を果たすオブジェクトがまさに橋を掛ける処理だけで使っている。そういう、別々の I/O をこの一点でいい具合に繋いで、お互いは疎結合を守っている、そういうケースで便利なんだと思う。

逆にこれが達成されたフレームワーク上でコードを書いているだけなのであれば、ほぼ使わないだろうし、それでいいんだと思う。

  1. 実際には receiver からそのメソッドの所属する instance にアクセスできるのでなんでもできるけど 

  2. apply も使えるけど、深掘りしない 

  3. call しないで () を付けるだけでも実行はできるんだけど、この場合は this を変える方法がないので上のコードだと Foo の中にしかアクセスできない 

  4. それを言い始めたら JS の class はそもそも syntax sugar でしかないので、やっぱ function で表現してるだけか 

More