【Ruby Advent Calendar 2018】あなたのしらない Refinements の世界【3日目】

Ruby Advent Calendar 2018 3日目の記事になります。
なんとか日付が変わる前に書けました…。
何を書こうか迷ったんですが、この記事では今年 Ruby にパッチを投げまくったディープな Refinements の世界について書いてみようかと思います。
そもそも皆さん Refinements は使っていますか?どんな機能か知っていますか?
まずは Refinements についておさらいしてみましょう。

クラス拡張について

Ruby ではオープンクラスに対してメソッドを拡張する事が出来ます。

# Integer クラスに対して後からメソッドを追加する事が出来る
class Integer
  def twice
    self + self
  end
end

# 追加したメソッドは既存のオブジェクトから呼び出すことが出来るようになる
p 42.twice
# => 84


# クラスではなくてモジュールに対してメソッドを追加する事も出来る
module Enumerable
  def filter &block
    select &block
  end
end

# Enumerable が mixin されているクラスのオブジェクトで使用することが出来る
p [1, 2, 3, 4, 5].filter(&:odd?)
# => [1, 3, 5]

p ({ a: 1, b: 2, c: 3, d: 4, e: 5 }.filter { |k, v| v.even? })
# => {:b=>2, :d=>4}

このように Ruby では後からクラスに対して自由にメソッドを追加する事が出来ます。
この機能の事を Ruby では『クラス拡張』や『モンキーパッチ』などと呼びます。

このような機能があることで Ruby ではユーザが自由にメソッドの追加したり挙動を書き換えたりフックしたりして闇の深いコードを書くことが出来ます。
Rails 界隈でよく見かける #present?#blank? などと言ったメソッドも Rails が独自にメソッドを定義しています。
こんな感じでユーザは便利なメソッドを様々なケースで利用する事が出来ます。

クラス拡張の欠点

クラス拡張は Ruby の醍醐味でもあるんですが、その自由さ故に欠点もあります。
例えば、次のように #blank? メソッドを追加する事が出来ます。

class Object
  # nil? の変わりに blank? という名前を使いたいなー
  def blank?
    nil?
  end
end

p nil.blank?   # => true
p false.blank? # => false
p 0.blank?     # => false
p [].blank?    # => false

これ単体では問題ありませんが、Rails を使っているコードであれば大問題です。
先程上げたように Rails ではすでに #blank? という名前のメソッドを定義しており、このメソッドと名前が競合してしまいます。
ですので、ユーザが定義した #blank?Rails が定義した #blank? も意図して動作しない可能性があります。

このようにクラス拡張は『全てのコードに対して』反映されてしまうので上記のように『うっかり』既存のメソッドと名前がかぶってしまう可能性があります。
上記のように自分でクラス拡張したメソッドであればまだ注意する事が出来るんですが、ライブラリの中でクラス拡張が定義されている場合、なかなか気づくことが出来ません。
どうしても上記のようなメソッドを使いたいのであればモジュール関数で定義することで安全に利用できます。

module Ex
  module_function
  def blank? a
    a.nil?
  end
end

p Ex.blank? nil   # => true
p Ex.blank? false # => false
p Ex.blank? 0     # => false
p Ex.blank? []    # => false

うーん、オブジェクト指向言語である Ruby でこのようなコードは書きたくないですね…。

このようにクラス拡張はユーザが自由にメソッドを定義出来るんですが、その反面『誰でもメソッドを定義出来てしまう』という諸刃の剣でもあります。
そのためクラス拡張を行う場合は、

  • そのメソッドが引き起こす副作用
  • そのメソッドが影響する範囲

を意識して利用する必要があります。
特に gem 化するようなコードであれば『そのライブラリを使用する全てのコード』に影響してしまうため細心の注意を払う必要があります。

しかし、安心してください。
これらの問題は Refinements を使用することで解決することが出来ます。

Refinements、Refinements を使う

Refinements とは一言で言ってしまうと『特定のコンテキストでのみクラス拡張を反映させる事が出来る機能』になります。
Refinements は Ruby 2.0 で実験的に導入され、Ruby 2.1 から本格的に導入された機能になります。
ですので Ruby 2.1 以降であれば誰でも自由に利用する事が出来ます。
と、いうわけで実際にコードを例に上げて動作を見ていきましょう。

refine を使用してクラス拡張を定義する

Refinements ではまず最初に『モジュール + refine』という形でクラス拡張を定義します。
先程例に上げた

class Object
  def blank?
    nil?
  end
end

を Refinements で定義し直すと

# クラス拡張を定義するモジュールを定義する
module ExBlank
  # refine に拡張したいクラスを渡す
  # do 〜 end 内でクラス拡張したいメソッドを定義する
  refine Object do
    def blank?
      nil?
    end
  end
end

このような形になります。
refineclass などの構文とは違い単なるメソッドなので do を定義することに注意しましょう。
ちなみに1つのモジュール内で何回も refine でメソッドを定義することが出来ます。

# Rails ライクな blank? を定義する場合
module ExBlank
  # 複数のクラスに対して拡張も出来る
  refine Object do
    def blank?
      respond_to?(:empty?) ? !!empty? : !self
    end
  end

  refine NilClass do
    def blank?
      true
    end
  end

  refine FalseClass do
    def blank?
      true
    end
  end

  refine TrueClass do
    def blank?
      false
    end
  end
end

refine したクラス拡張を反映させる

refine で定義したメソッドはそのままでは使用する事は出来ません。

module ExBlank
  refine Object do
    def blank?
      nil?
    end
  end
end

# 定義したメソッド呼び出してもエラーになる
# Error: undefined method `blank?' for nil:NilClass (NoMethodError)
p nil.blank?

逆にいえば『refine で定義しただけ』ではクラス拡張は反映されません。
ここが通常のクラス拡張と大きく異なる点です。

では、実際に反映させるためにどうするのかと言うと using を使用します。

module ExBlank
  refine Object do
    def blank?
      nil?
    end
  end
end

# refine を定義したモジュールを using に渡すことで、以降の行から反映される
using ExBlank

p nil.blank?
# => true

この using というのが肝で refine で定義したクラス拡張は using を使用して『特定のコンテキストでのみ』クラス拡張を反映させます。
と、言われてもピンと来ないと思うのでもう少し解説します。
using が使用できる場所は限られており、

  • トップレベ
  • クラス(モジュール)内

になります。
まず、トップレベルの場合は『using したファイル内全体』に対して反映されます。

module ExBlank
  refine Object do
    def blank?
      nil?
    end
  end
end

# NG: ここではまだ呼べないよー
p nil.blank?

using ExBlank

# OK: using 後から呼べるようになるよー
p nil.blank?


# クラス内からも呼び出せる
class X
  def meth a
    a.blank?
  end
end

# OK
p X.new.meth 0

このように using したファイル全体でのみクラス拡張が反映されます。
ただし、using した以降の行で反映されることに注意してください。

次にクラス内で using を使用した場合は『そのクラス内でのみ』クラス拡張が反映されます。

module ExBlank
  refine Object do
    def blank?
      nil?
    end
  end
end

class X
  # クラス内でのみ影響する
  using ExBlank

  # メソッド内で呼び出せる
  def meth a
    a.blank?
  end
  
  # OK: ここでも呼び出せる
  p [].blank?
end

# OK
p X.new.meth 0

# NG: クラス外では呼び出せない
p 0.blank?

このように Refinements は、

  • モジュール + refine でクラス拡張を定義する
  • クラス拡張したメソッドは using モジュール 反映される

と言うように役割が分担し、クラス拡張を制御します

ただし、『using 外のメソッド内ではクラス拡張したメソッドは呼び出せないこと』には注意してください。

module ExBlank
  refine Object do
    def blank?
      nil?
    end
  end
end

def meth a
  a.blank?
end

using ExBlank

# NG: メソッド内では using は反映されない
meth 0

おそらくこれが Refinements の一番のハマりポイントだと思います。
ちなみに refine したモジュールはそのファイル内で定義されているある必要はありません。
ですので、

  • 別のファイルで refine でクラス拡張を定義する
  • 必要に応じて using する

と、いうのが Refimenets の基本的な使い方になります。

まとめ 1

  • Ruby では自由にクラスに対してメソッドを追加する事が出来る
  • しかし、影響範囲が広いので何も考えずに追加してしまうと競合する可能性がある
  • Refinements を使用する事でクラス拡張の影響範囲を制限することが出来る
  • ただし、using がどの範囲まで適用されるのかは意識する必要がある

次は実際にどのような場面で Refiments を利用しているのか解説していきたいと思います。

gem で安全にクラス拡張する

『クラス拡張を伴うライブラリをつくりたい!!』と思うことは多いと思います。
実際に rubygems では『クラス拡張だけ』を行うライブラリがたくさんあります。
例えば、わたしも Binding#expand メソッドを追加する gem-binding-expand という自作ライブラリをつくって遊んだりしています。
この『クラス拡張を伴うライブラリ』ですが、当然ただ単にクラス拡張を行ってしまうと先程上げた問題のように名前が競合してしまう可能性があります。
#expand なんてめっちゃ一般的な名前なのでもしかしたらすでに誰かが使っている可能性がありますよね…。
そこで、Refinements を利用して、

  • ライブラリ側は refine したモジュール・メソッドだけ定義する
  • 利用する場合は using を使い必要に応じて有効化する

というような使い方を考えてみましょう。
先程上げた gem-binding-expand もそのような構造になっており、以下のような使い方になります。

require "binding/expand"

# using したら Binding#expand というメソッドが使用できるようになる
using Binding::Expand

def meth
  "meth"
end

hoge = 42
foo = "homu"
bar = [1, 2, 3]

p binding.expand(:hoge, :foo, :bar, :meth)
# => { hoge: hoge, foo: foo, bar: bar, meth: meth }

これによりライブラリ側は名前の競合を考える必要がなく、また利用者側も require しただけでは即座に反映されず、更に適用範囲を制御することが出来ます。
この『using しない限りは問題ない』というのが利用者の安心感に繋がっていきます。
逆に何も考えずにクラス拡張を行っているようなライブラリは何が起こるのかわからないので使いたくないですよね?
『まーこのメソッド名は競合しないだろー』と慢心せずにクラス拡張を行う場合は必ず、必ず!! Refinements を利用しましょう。
ちなみに失敗例を上げると以前 gem-cstyle_enum というライブラリを作ったんですが、Refinements 化しておらずこのままでは Railsenum と競合してしまいますね…。

require "cstyle_enum"

class Color
  # クラスメソッドとして enum が定義されるが
  # Rails も同じ名前のメソッドが生えており…
  Colors = enum {
    RED
    GREEN = 3
    BLUE
  }
end

Color::RED      # => 0
Color::GREEN    # => 3
Color::BLUE     # => 4
Color::Colors   # => {:RED=>0, :GREEN=>3, :BLUE=>4}

このように単体では問題なくても他のライブラリやフレームワークなどを組み合わせる場合に競合してしまう可能性が微粒子レベルで存在するので『このメソッド名は競合しないだろー』と思わずにクラス拡張を行う場合は必ず、必ず! Refinements を利用しましょう。

その場で雑にメソッドを生やす

今度はもう少し実用的な使い方を考えてみましょう。
次のような User クラスがあるとします。

class User < Struct.new(:name, :age)
  def self.all
    @@all ||= []
  end

  def initialize(*args)
    super
    User.all << self
  end
end

このクラスの一覧を出力するメソッドを別で定義すると以下のような感じになると思います。

# 雑に全ての User を文字列に変換する
def view_user_all
  User.all.map { |user| "#{user.last_name} : #{user.last_name} (#{user.age})" }
end

User.new("ほむら", "明美", 14)
User.new("まどか", "鹿目", 14)
User.new("まみ", "", 15)
p view_user_all
# => "明美 ほむら (14)\n鹿目 まどか (14)\n巴 まみ (15)"

まあ Rails などを使っていると Model を View が分かれていることはよくあることだと思います。 上記のコードでも問題はないんですが、Refinements を使用するともう少しすっきりとした実装になります。

# 文字列に変換するメソッドを Refinements で定義しておく
module ExView
  refine User do
    def to_s
      "#{last_name} #{first_name} (#{age})"
    end
  end

  # 拡張しづらい標準クラスに対しても安全に一時的に挙動を変えることが出来る
  refine Array do
    def to_s
      self.join("\n")
    end
  end
end
using UserView

def view_user_all
  # すっきり
  "#{User.all.map { |user| "#{user}" }}"
end

User.new("ほむら", "明美", 14)
User.new("まどか", "鹿目", 14)
User.new("まみ", "", 15)
p view_user_all
# => "明美 ほむら (14)\n鹿目 まどか (14)\n巴 まみ (15)"

Refinements の部分がやや冗長に見えますが呼び出す側はすっきりしていますね。 User#full_name みたいなメソッドを追加しておくと更に汎用的になるかもしれないですね。
また Array のように『メソッドを生やすと可読性が上がるけど余計なメソッドは生やしたくない…』みたいなクラスに対しては Refinements がかなり有効になります。
このように Refinements を使用することで安全かつ最小限の副作用でメソッドを追加する事が出来ます。 これは追加したメソッドを使用する箇所が増えれば増えるほどより強力により可読性向上につながります。

メソッドを隠蔽する

さてさて、もう少しコアな Refinements の使い方を紹介してみましょう。
次のように他から参照されたくないメソッドを private にすることはよくあると思います。

class User
  def initialize name
    @__name__ = name
  end

  def to_s
    "name is #{name}"
  end

  # 直接値が参照されないように private にしておく
  private def name
    @__name__
  end
end

homu = User.new "homu"

# to_s を介してのみ参照出来る
p homu.to_s
# => "name is homu"

しかし、残念ながら Rubyprivate はガバガバなので、例えば継承した場合、継承先で意図しないで参照されてしまう(してしまう)可能性があります。

class User
  def initialize name
    @__name__ = name
  end

  def to_s
    "name is #{name}"
  end

  private def name
    @__name__
  end
end

class UserEx < User
  def meth
    # スーパークラスの private メソッドを普通に参照できてしまう
    name
  end
end

homu = UserEx.new "homu"
p homu.meth
# => "homu"

そもそも Rubyprivate はこういう意図で使用するものではないのでしょうがないんですが、それでもやっぱり参照してほしくない、参照されたくないメソッドというのは出てくると思います。
そういうときは Refinements を使用する事で完全にメソッドを隠蔽することが出来ます。

class User
  # Refinements で隠蔽したいメソッドを定義する
  # また Module.new で動的にモジュールを定義することで
  # あとから using することも防ぐ事が出来る
  using Module.new {
    refine User do
      def name
        @__name__
      end
    end
  }

  def initialize name
    @__name__ = name
  end

  def to_s
    "name is #{name}"
  end
end

class UserEx < User
  def meth
    # using してないコンテキストなので参照できない!!
    name
  end
end

homu = UserEx.new "homu"

# Error: undefined local variable or method `name' for #<UserEx:0x000055998748ee48 @__name__="homu"> (NameError)
p homu.meth

このように Refinements を利用することで『そのクラス内でのみ』参照出来るメソッドを定義することが出来ます。
また、Module.new で動的にモジュールを定義することであとから using することを防ぐことも出来ます。
実際にここまで完全に隠蔽する必要があるケースは限られると思うんですが、いざ隠蔽したいケースが出てきた時はこういうテクニックを利用する事が出来ます。
homu.instance_exec { @__name__ } で丸見えなのはご愛嬌
わたしは mixin するモジュール内で『実装』と『UI』を分けたい時などに前者を隠蔽する目的で使用する事が稀によくあります。

まとめ2

Refinements は以下のようなケースで利用できるよ!!

  • 安全にクラス拡張する
    • gem では refine でクラス拡張する!!!
    • Refinements 化しておくことで利用者の安心感を得る
  • 雑にメソッドを生やす
    • その場でメソッドを生やして可読性を Up!
    • 草みたいにどんどん生やしていこう
  • そのメソッド、完全に隠蔽出来るよ!!
    • 妖怪メソッド隠し

ここまでは Refinements のポジティブな面を見てきたのですが、次からは闇の部分になります。

Refinements の落とし穴

ここまでで Refinements が以下に便利な機能なのかわかってきたと思います。
しかし、残念ながら万能に見える Refinements もそこまで万能ではありません。
むしろ実際に使おうと思うとクソだなと思うことの方が多いです、

適用されるコンテキストが限定的すぎる

まず、大前提なのですが Refinements で定義したメソッドが呼び出せるのは『using されたコンテキスト範囲』です。
逆にいえば『using していないコンテキスト』ではそのメソッドを呼び出すことは出来ません。
どういうことかというと、次のような呼び出しはエラーになります。

def meth a
  a.twice
end

using Module.new {
  refine Object do
    def twice
      self + self
    end
  end
}

# OK: ここでは呼び出せる
10.twice

# NG: ここでは呼び出せない
meth 10

まあこれはコード見たらそれはそうって感じがしますね。

では、次のようなコードで考えてみましょう。
#grep は内部で #=== を使用して比較を行っています。
ですので、次のように #=== を定義する事で挙動を変更する事が出来ます。

class String
  def === other
    self == other.to_s
  end
end

p "homu" === :homu
# => true

# #=== を拡張する事で #grep でも String === Symbol 出来るようになる
p [:homu, :mami, :mado].grep "homu"
# => [:homu]

しかし、ここまで読んでいた方であればこんなコードはすぐに Refinements 化したくなりますよね。
と、言うわけで素直に Refinements 化してみますが…。

# Refinements でこのコンテキストでのみ動作するようにする
using Module.new {
  refine String do
    def === other
      self == other.to_s
    end
  end
}

p "homu" === :homu
# => true

# 先程であれば [:homu] が返ってきたが [] が返ってくる…
p [:homu, :mami, :mado].grep "homu"
# => []

このように何故か #=== が適用されなくなってしまいました。 ナンデナンデ!
なぜ、このようになってしまうのかというと(当然ではあるんですが)『#grep のメソッド内では using が反映されないから』です。

そうです、お察しの通り Refinements はダックタイピングにめちゃくちゃ弱いんです… Ruby なのに…どうして…どうして…。
using したコンテキストでメソッドを呼び出す場合には問題がないんですが『メソッド内で呼ばれる事を期待するメソッド』に Refinements は全く役に立ちません。
どうしたもんですかね、これ。

Ruby の標準メソッドでも対応していないよ!!

Refinements の受難はまだ続きます。
Ruby ではしばしばメソッド名で処理を行ったりする事があります。

# メソッドを動的に呼び出したり
p -42.send(:abs)         # => 42
p -42.public_send(:abs)  # => 42

# メソッドが呼び出せるか確認したり
p 42.respond_to? :abs    # true
p 42.respond_to? :hoge   # false

# メソッドをオブジェクト化したり
pow = 2.method(:pow)
p pow.call 3
# => 8

# メソッド名一覧を取得したり
p 2.methods
# [:-@, :**, :<=>, :upto, :<<, :<=, :>=, :==, :chr, :===, :>>, :[], :%, :&, :inspect, :+, :ord, :-, :/, :*, :size, :succ, :<, :>, :to_int, :coerce, :divmod, :to_s, :to_i, :fdiv, :modulo, :remainder, :abs, :magnitude, :integer?, :numerator, :denominator, :to_r, :floor, :ceil, :round, :truncate, :gcd, :to_f, :^, :odd?, :even?, :allbits?, :anybits?, :nobits?, :downto, :times, :pred, :pow, :bit_length, :digits, :rationalize, :lcm, :gcdlcm, :next, :div, :|, :~, :+@, :eql?, :singleton_method_added, :i, :real?, :zero?, :nonzero?, :finite?, :infinite?, :step, :positive?, :negative?, :arg, :rectangular, :rect, :real, :imag, :abs2, :imaginary, :angle, :phase, :conjugate, :to_c, :polar, :conj, :clone, :dup, :quo, :between?, :clamp, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :instance_of?, :kind_of?, :is_a?, :tap, :instance_variable_get, :public_methods, :instance_variables, :method, :public_method, :singleton_method, :define_singleton_method, :public_send, :extend, :to_enum, :enum_for, :pp, :=~, :!~, :respond_to?, :freeze, :object_id, :send, :display, :nil?, :hash, :class, :singleton_class, :itself, :yield_self, :taint, :untaint, :tainted?, :untrusted?, :untrust, :frozen?, :trust, :singleton_methods, :methods, :private_methods, :protected_methods, :!, :equal?, :instance_eval, :instance_exec, :!=, :__id__, :__send__]

このあたりは普通に Ruby を使っていてもしばしば使うことがあると思います。
では、このようなメソッドに対して Refinements で定義したメソッドを渡してみると…。

using Module.new {
  refine Object do
    def twice
      self + self
    end
  end
}

# OK
p 42.send(:twice)
# => 84

# NG: undefined method `twice' for 42:Integer (NoMethodError)
42.public_send(:twice)

# NG: false が返ってくる…。  
p 42.respond_to? :twice    # false

# NG: undefined method `twice' for class `Integer' (NameError)
2.method(:twice)

# NG: 含まれていない…
p 2.methods.include? :twice

なんと #send 以外全部 Refinements が反映されていません!!!
ほんまか???Ruby 大丈夫?????バグってない?????
42.respond_to? :twicefalse を返すのに 42.twice は呼び出し可能な不思議!!
#send は Refinements が反映されているけど #public_send は動かない!!
ないよ、メソッドないよぉ!!!!!

と、いう感じで標準メソッドでも Refinements が考慮されていないメソッドはかなりあります。
むしろ対応しているメソッドを上げたほうが早い

method_missingconst_missingmethod_added が呼ばれない…

もうやめたげてぇ!となりますがまだまだ続きます。
method_missingconst_missingmethod_added といえば Ruby の黒魔術には欠かせませんね。
これらのメソッドはあまりにも闇が深すぎるのでなるべく副作用を抑えるべく Refinements で利用したいのですが…。

# Hash に対してキーの名前でメソッド参照したいが…
using Module.new {
    refine Hash do
        def method_missing(name)
            self[name.to_sym] || self[name.to_s]
        end
    end
}

hash = { name: "homu", "age" => 14 }
# NG: undefined method `name' for {:name=>"homu", "age"=>14}:Hash (NoMethodError)
pp hash.name

残念ながら利用することは出来ません…。
こういう時こそ Refinements を利用したいのに恥ずかしくないの???

それでも希望はある

ここまで見て『なんだよ Refinements 残念だなあ』と思った方も多いと思います。
しかし、安心してください。
最初はガチガチだった Refinements もだんだん緩和されてきています。

例えば、先程上げた #send も実は Refinements が実装された当初は動作しませんでしたが Ruby 2.4 で緩和されました。
また、&:hoge のような Symbol#to_proc 呼び出しも同様に Ruby 2.4 から呼び出せるようになり、文字列の式展開(#{こういうの})で呼び出される #to_sRuby 2.5 から Refinements が適用されるようになっています。

using Module.new {
  refine Integer do
    def twice
      self + self
    end

    def to_s
      "call to to_s"
    end
  end
}

# 以下は問題なく動作する
p 42.send(:twice)
# => 84

p (1..10).map(&:twice)
# => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

p "#{42}"
# => "call to to_s"

なんだよ、Refinements やるじゃん。

更に Ruby 2.6 では…

更にもうすぐリリースされる Ruby 2.6 では以下のメソッドで Refinements が有効になっています。

また、以下のパッチも投げられているのでもしかしたら Ruby 2.6 に入るかも…?(入らないかも…

今後の Refinements には期待できそうですね!!!
あるよ、メソッドあるよぉ!!!!!
ちなみに上のパッチは全部わたしが書いて投げましたてへぺろ

まとめ

と、言う感じで Refinements に関して長々と書いてみました。
Refinements って言うと使う人が限られると思っている人も多いと思いますが、全然そんなことはなくて実際に使ってみるとものすごく使い勝手がよく(そしてとても悪く)て色んな所で Refinements を使いたいと思うようになります。
また、Refinements がいくらクソと言ってもRuby という言語の性質上、クラス拡張の範囲を制限する必要はどうしても必要になって来ると思います。
と、いうか各々が好き勝手クラス拡張しまくってるとそれこそ破綻していき Ruby オワタになってしまう…。

よくわからないクラス拡張に刺されないように gem を作る人は必ず Refinements を使いましょう。
そうでない人も Refinements を使って恐れることなくクラス拡張を使っていきましょう。
クラス拡張は Ruby をいう言語を書く上での醍醐味なので使わないともったいないなあ、というのがわたしの意見です。
Ruby でクラス拡張しないなんて C++C言語書いているようなものですよ!!C++20 早く!!!!
さて、Refinements 関連で書き足りないことがたくさんあるんですが、そろそろ時間なのでこの辺にしておきます。
もっと闇の深い Refinements はまた次の機会にでも…。
Refinements 便利なのでみんな使おう!!!
そしてどんどんよくしていこう!!!
来年もどんどんパッチ投げていくぞ!!!!

こういうのは Refinements に向かない

最後に覚え書き程度に書いておきます。

  • 毎ファイルで using するのがめんどくさい
    • Rails みたいなフレームワークを使うとファイル数が無限に増えてくるので汎用的なメソッドの場合、ファイル毎に using するのがかなり手間
    • もうちょっと広い範囲で using を制御したい
  • デバッグとかで使うなら素直にモンキーパッチでいいと思う
    • コンテキストが限られているという前提。 .pryrc で定義するとか
    • 実際デバッグ用のライブラリを Refinements 化してみたけどデバッグする度に using するのがクッソだるい

では、よい Refinements ライフを。

Ruby の Hash をソートする時の注意

Hash をソートしたい場合、Hash#sort でソートすることが出来るんですが、このメソッドは [key, value] の配列に変換してからソートが行われます。
なので Hash#sort の戻り値も配列になります。

hash = { b: "homu", c: "mami", a: "mado" }

# #sort の戻り値は [key, value] の配列になる
pp hash.sort
# => [[:a, "mado"], [:b, "homu"], [:c, "mami"]]

この時に結果を Hash 化したい場合は、#to_h を呼ぶことで Hash になります。

hash = { b: "homu", c: "mami", a: "mado" }

# #to_h で [key, value] の配列を Hash 化
pp hash.sort.to_h
# => {:a=>"mado", :b=>"homu", :c=>"mami"}

ちなみに value でソートしたい場合は、配列と同様に #sort_by が利用できます。

hash = { c: "mami", b: "homu", a: "mado", d: "an" }

# #sort_by で value を基準としたソート
pp hash.sort_by { |key, value| value }.to_h
# => {:d=>"an", :b=>"homu", :a=>"mado", :c=>"mami"}

# こうもかけるはず
pp hash.sort_by(&:last).to_h
# => {:d=>"an", :b=>"homu", :a=>"mado", :c=>"mami"}

知ってると便利

Ruby で yield_self のめっちゃ便利な使い道

Ruby 2.5 で追加された #yield_self という地味に便利なメソッドがあります。
これは『レシーバを引数としてブロックを呼び出す』というメソッドになります。
既存のメソッドとして #tap と似ていますが、#tap とは異なり『ブロックの戻り値を #yield_self の戻り値として』返します。

# ブロックの引数で yield_self のレシーバを受け取る
# #tap とは違いブロックの戻り値が yield_self の戻り値となる
p [1, 2, 3].yield_self { |it| it + [4, 5, 6] }
# => [1, 2, 3, 4, 5, 6]

どういう時に便利なの

例えば、次のように『hash でキーの値が存在すれば任意の処理を行う』というようなコードはよく書くと思います。

def meth opt
    # 存在するキーの値だけ result に突っ込んでいく
    result = []
    result << opt[:hoge] if opt[:hoge]
    result << opt[:foo]  if opt[:foo]
    result << opt[:bar]  if opt[:bar]
    result
end

pp meth(hoge: 1, foo: 2)
# => [1, 2]

しかし、上記の場合では opt[:hoge] というのを2回書く必要がありちょっと手間ですね。
こういう場合、#yield_self を使うことでスッキリと書くことが出来ます。

def meth opt
    result = []
    opt[:hoge]&.yield_self { |it| result << it }
    opt[:foo]&.yield_self  { |it| result << it }
    opt[:bar]&.yield_self  { |it| result << it }
    result
end

pp meth(hoge: 1, foo: 2)
# => [1, 2]

こんな感じで &. 演算子と合わせることで『レシーバが nil の場合はブロックを評価しない』というような処理を行うことが出来ます。
更に、次のように書くことも出来ます。

def meth opt
    result = []
    opt[:hoge]&.yield_self(&result.method(:<<))
    opt[:foo]&.yield_self(&result.method(:<<))
    opt[:bar]&.yield_self(&result.method(:<<))
    result
end

pp meth(hoge: 1, foo: 2)
# => [1, 2]

#yield_self はメソッドチェーンしている時に便利、と言われていましたがこういう使い方の方が有効な気がしてきましたね!!

更に更に Ruby 2.6 では

#yield_self はめちゃくちゃ便利という事がわかったと思うんですが、メソッド名が長いという根本的な問題がありました。
しかし、この問題は Ruby 2.6 で解決しており、Ruby 2.6 では #yield_self の別名として #then が追加されました。
そう、半分以上も短くなったんです!! それを踏まえた上で先程のコードを #then で置き換えてみましょう。

def meth opt
    result = []
    opt[:hoge]&.then { |it| result << it }
    opt[:foo]&.then  { |it| result << it }
    opt[:bar]&.then  { |it| result << it }
    result
end

pp meth(hoge: 1, foo: 2)
# => [1, 2]

ね? &.then の組み合わせはありだと思いませんか?

Ruby 2.5 系で CSV.generate のバグ

Ruby 2.5 系で CSV.generate を使用しようとしたら意図しない動作をして、調べてみたらバグだったのでそのまとめ。
しかし、これ、結構クリティカルなバグだと思うんですけど、全然話題になってないのが不思議(当時は話題になっていたのかもしれないけど。

CSV.generate とは

以下のような感じで CSV 形式で文字列を構築する事が出来ます。

# Ruby 2.4 で実行
require "csv"
require "pp"

result = CSV.generate do |csv|
    csv << [1, 2, 3]
    csv << ["homu", "mami", "mado"]
end
pp result
# => "1,2,3\n" + "homu,mami,mado\n"

https://wandbox.org/permlink/jL6prEzNrYZ0g9m0

また CSV.generate の第一引数で『その CSV の先頭文字列』を指定する事が出来ます。

require "csv"
require "pp"

csv = CSV.generate("prefix") do |csv|
    csv << [1, 2, 3]
end
pp csv
# => "prefix1,2,3\n"

# 既存の csv に対して追記したり
csv = CSV.generate(csv) do |csv|
    csv << ["homu", "mami", "mado"]
end
pp csv
# => "1,2,3\n" + "homu,mami,mado\n"

https://wandbox.org/permlink/afzhyZti9dAAyukB

CSV.generate のバグ

で、件のバグなんですが、先ほど説明した『CSV.generate の第一引数で『その CSV の先頭文字列』を指定する』が Ruby 2.5 では動作しません。

# Ruby 2.5 で実行した場合
require "csv"

csv = CSV.generate("prefix") do |csv|
    csv << ["homu", "mami", "mado"]
end
pp csv
# => "homu,mami,mado\n"

https://wandbox.org/permlink/jWYqvmnnWIRdH2X7

上記のようなコードではすぐに『何かおかしい』とわかるんですが、例えば次のように『CSV データに BOM を追加する』みたいな事をしたい場合はほぼ気づきません。

require "csv"

# BOM 付き CSV データを生成したいが追加されない…
bom = "\uFEFF"
csv = CSV.generate(bom) do |csv|
    csv << ["homu", "mami", "mado"]
end

# 出力先によっては BOM がついているかどうかが視覚的にわからないのでバグを見つけるのがむずかしい…
pp csv
# => "homu,mami,mado\n"

わたしも上記のような事をやりたかったんですが、うまく動作しなくて調べてみたら既知のバグでした。
Ruby BOM CSV』でググるCSV.generate を使ったやり方を書いているブログとかが結構ヒットするんですが、このバグに気づいてない人もいるんじゃないかなあ…。

Ruby 2.6 では修正済み

このバグは Ruby 2.6 では修正済みとなっています。

Ruby 2.5 系での対処方法

幸いにも CSV.generateRuby で実装されているので次のようなモンキーパッチで対処する事が出来ます。

require "csv"

# CSV.generate の実装を上書き
class CSV
    def self.generate(str=nil, **options)
        # add a default empty String, if none was given
        if str
            str = StringIO.new(str)
            str.seek(0, IO::SEEK_END)
        else
            encoding = options[:encoding]
            str      = String.new
            str.force_encoding(encoding) if encoding
        end
        csv = new(str, options) # wrap
        yield csv               # yield for appending
        csv.string              # return final String
    end
end

# これで BOM が追加される
bom = "\uFEFF"
csv = CSV.generate(bom) do |csv|
    csv << ["homu", "mami", "mado"]
end
pp csv
# => "<feff>homu,mami,mado\n"

まとめ

Ruby 2.6 はよ〜

Ruby で nil が含まれている配列をソートする

さて、Ruby の配列でソートを行いたい場合、配列内に nil があるとうまくソートできないことがあります。

ary = ["homu", nil, "mado", nil, nil, "mami", "saya", nil]

# Error: comparison of String with nil failed (ArgumentError)
ary.sort

これは、『nil とその他の要素が比較できない』のでエラーになっています。

nil とそうでない要素を分けてソートする

こういう場合は Enumerable#partition を利用して『nil』と『それ以外』を分けてからソートするといい感じにできそうです。

ary = ["homu", nil, "mado", nil, nil, "mami", "saya", nil]

# #partition で nil とそうでない要素を分けてからソートする
p ary.partition(&:nil?).yield_self { |nils, ary|
    # nil は末尾に持っていく
    ary.sort + nils
}
# => ["homu", "mado", "mami", "saya", nil, nil, nil, nil]

これでだいぶスッキリとソートすることが出来ますね。
今回は #nil? を使って分けましたが、他にも present?blank?hoge? みたいなメソッドを利用してもいい感じにソートが制御できそうです。

参照

Kernel.#proc でブロックを渡さなかった時の挙動

さて、 Kernel.#proc といえば普通は『ブロックをオブジェクトとして扱う時』に使用します。

# ブロックをオブジェクト化する
plus = proc { |a, b| a + b }
# もしくは Proc.new でもどうようの挙動になる
# plus = proc { |a, b| a + b }

# #call でブロックを評価する
p plus.call 1, 2

細かい点は違いますが lambda {}-> {} でも同様に使用することが出来ます。
では、この Kernel.#proc に対してブロック引数を渡さなかった場合どうなるんでしょうか。

Kernel.#proc にブロック引数を渡さなかった場合

Kernel.#proc にブロック引数を渡さなかった場合、『そのメソッドのブロック引数』を返します。

def meth &block
    # meth のブロック引数を返す
    # block と同等
    proc
end

# meth のブロック引数がそのまま返ってくる
p meth { |a, b| a + b }.call 1, 2

一体どういう時に便利なのかわかりません…。
ブロック引数がない場合は nil を返すのかな?と思ったんですが、エラーになってしまうみたいですね。

def meth
    proc
end

# Error: `proc': tried to create Proc object without a block (ArgumentError)
meth

nil を返すのであれば次のように使えるかとも思ったんですが、ダメそう

def meth
    if proc.nil?
        "Not found block."
    end
    yield(1, 2)
end

p meth { |a, b| a + b }
p meth

飲み会の席などで『おれ Ruby しってるんだぜー』アピールに使用してください。

Ruby Hack Challeng で Refinements のバグを治そうとした話

前回参加した Ruby Hack Challeng からだいぶ時間が経ってしまったんですが、その時にやっていたことを覚書程度に書いておこうかと。
ちなみにバグと言っていますが、バグなのか微妙なところだったり。
あと結構フィーリングで書いているので実際の Ruby(の実装)と言ってることが異なるかもしれませんがまあなんか察してください。
なんかガッツリ書いたら長くなったので『兎に角問題だけ知りたいんじゃ!!』って人は bugs.ruby の issuesを参照してください。

Refinements とは

Refinements が実装されてだいぶ時間が経っているのでもう皆さんご存知だと思いますが、一応説明しておくと、『任意のスコープでのみクラス拡張を適用するため』の機能です。

module IntegerEx
    # Integer#twice を定義する
    refine Integer do
        def twice
            self + self
        end
    end
end

class X
    # X クラスのスコープ内でのみ Integer#twice が使用できる
    using IntegerEx

    def initialize value
        @value = value
    end

    def meth
        @value.twice
    end
end

x = X.new 42
p x.meth # => 84

# ここでは使えない
42.twice

Ruby の場合、クラス拡張を使う事で思わぬ副作用が発生する事があるので、それを制限するための機能になります。

Refinements の制限

さて、そんな Refinements ですが、実装された当初はいろいろと制限があり、例えば以下のようなコードは動きませんでした。

module IntegerEx
    # Integer#twice を定義する
    refine Integer do
        def twice
            self + self
        end
    end
end
using IntegerEx

# これは #twice を直接呼び出しているので OK
(1..3).map { |it| it.twice }

# これは Symbol#to_proc 内部で #twice を呼び出しているので NG
(1..3).map(&:twice)

この制限は Ruby 2.4 で緩和されて『Symbol#to_proc 内から Refinements されたメソッドが呼び出せる』ようになりました。

module IntegerEx
    # Integer#twice を定義する
    refine Integer do
        def twice
            self + self
        end
    end
end
using IntegerEx

# Ruby 2.4 以降では OK
(1..3).map(&:twice)

そう、なったはずだったんです…。

問題点

Ruby 2.4 の relese note では次のように明記されています。

Refinements is enabled at method by Symbol#to_proc. [Feature #9451] see: https://github.com/ruby/ruby/blob/v2_4_0/NEWS#changes-since-the-230-release

Symbol#to_proc が Refinements で適用されたかのように書かれていますね。
しかし、現実はこの実装は不完全であり、例えば次のように『ユーザが定義したメソッド』に対して同様に『Refinements したメソッドを &:hoge で渡す(呼び出す)』とエラーになります。

def meth &block
    # ここで Integer#twice が呼び出される
    block.call 42
end

module IntegerEx
    # Integer#twice を定義する
    refine Integer do
        def twice
            self + self
        end
    end
end
using IntegerEx

# Error: `meth': undefined method `twice' for 42:Integer (NoMethodError)
meth &:twice

meth &:twice の部分は問題ありませんが、#meth 内で block.call を呼び出そうとするとエラーになります。
これは次のように『 include Enumerable を mixin してイテレーションメソッドを呼び出したい』ような実装で問題になります。

class X
    include Enumerable

    def each &block
        (1..3).each &block
    end
end


module IntegerEx
    # Integer#twice を定義する
    refine Integer do
        def twice
            self + self
        end
    end
end
using IntegerEx

# こっちは当然 Integer#twice が呼ばれる
(1..3).map &:twice

# Error: `each': undefined method `twice' for 1:Integer (NoMethodError)
# こっちは X#each 経由で Symbol#to_proc を呼び出そうとしているのでエラーになる
X.new.map &:twice

実際、この問題は RailsActiveRecordイテレーションメソッドを使用してる時に気づきました。

# hoge が Refinements で定義されている場合、エラーになってしまう
User.all.map &:hoge

ちなみに Symbol#to_proc を直接呼び出した場合にもエラーになります。

module IntegerEx
    # Integer#twice を定義する
    refine Integer do
        def twice
            self + self
        end
    end
end
using IntegerEx

# 内部で 42.twice が呼ばれるが Refinements が反映されない
:twice.to_proc.call 42

これは流石に意図していない挙動かなーと思い修正しようと思いました。

CRuby 側の実装

今回の問題ですが、見るべき実装は二箇所あり、

  1. メソッドに &:twice を渡している部分
    • 内部的に Symbol#to_proc が呼び出される
  2. block.call が呼ばれる部分
    • つまり Symbol#to_proc で生成した Proc オブジェクトが評価される部分

になります。
おそらく問題となっているのは『&:twice を呼び出している部分』と『block.call が呼ばれる部分』でコンテキストが異なっており、参照する refinements 情報が違うのかなーという予想です。
ではでは、実際に CRuby の実装がどうなっているのかコードを見てみましょう。
上記の実装はどちらも vm_args.c で書かれています。

&:twice から proc オブジェクトを生成している箇所

さて、いきなりですが『&:twice から proc オブジェクトを生成しているコード』は以下の関数になります。

static VALUE
vm_caller_setup_arg_block(const rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
                          const struct rb_call_info *ci, rb_iseq_t *blockiseq, const int is_super)
{
    if (ci->flag & VM_CALL_ARGS_BLOCKARG) {
    VALUE block_code = *(--reg_cfp->sp);

    if (NIL_P(block_code)) {
            return VM_BLOCK_HANDLER_NONE;
        }
    else if (block_code == rb_block_param_proxy) {
            return VM_CF_BLOCK_HANDLER(reg_cfp);
        }
    else if (SYMBOL_P(block_code) && rb_method_basic_definition_p(rb_cSymbol, idTo_proc)) {
        const rb_cref_t *cref = vm_env_cref(reg_cfp->ep);
        if (cref && !NIL_P(cref->refinements)) {
        VALUE ref = cref->refinements;
        VALUE func = rb_hash_lookup(ref, block_code);
        if (NIL_P(func)) {
            /* TODO: limit cached funcs */
            func = rb_func_proc_new(refine_sym_proc_call, block_code);
            rb_hash_aset(ref, block_code, func);
        }
        block_code = func;
        }
            return block_code;
        }
        else {
            return vm_to_proc(block_code);
        }
    }
    else if (blockiseq != NULL) { /* likely */
    struct rb_captured_block *captured = VM_CFP_TO_CAPTURED_BLOCK(reg_cfp);
    captured->code.iseq = blockiseq;
        return VM_BH_FROM_ISEQ_BLOCK(captured);
    }
    else {
    if (is_super) {
            return GET_BLOCK_HANDLER();
        }
        else {
            return VM_BLOCK_HANDLER_NONE;
        }
    }
}

https://github.com/ruby/ruby/blob/1aef602d5a9398ff362de212ae402ffcd8ff76ae/vm_args.c#L872

この関数は『メソッドの引数まわりの処理』が実装されていますが、今回注目するべき箇所は上記の

else if (SYMBOL_P(block_code) && rb_method_basic_definition_p(rb_cSymbol, idTo_proc)) {
    const rb_cref_t *cref = vm_env_cref(reg_cfp->ep);
    if (cref && !NIL_P(cref->refinements)) {
    VALUE ref = cref->refinements;
    VALUE func = rb_hash_lookup(ref, block_code);
    if (NIL_P(func)) {
        /* TODO: limit cached funcs */
        func = rb_func_proc_new(refine_sym_proc_call, block_code);
        rb_hash_aset(ref, block_code, func);
    }
    block_code = func;
    }
        return block_code;
    }
    else {
        return vm_to_proc(block_code);
    }
}

の部分になります。
ここの

rb_func_proc_new(refine_sym_proc_call, block_code)

というコードが『&:twice から Proc オブジェクトを生成している部分』になります。
rb_func_proc_new() では #call 時に呼ばれるコールバック関数として refine_sym_proc_call() の関数ポインタを渡しています。
block_code はそのコールバック関数に渡される引数になります。
Ruby で書くとこんな感じでしょうか。

block_code = :twice
func = proc { refine_sym_proc_call(block_code) }

さて、この箇所ではまだ Refinements に関する処理は見当たりませんね。
cref->refinements は見なかったことに

block.call が呼ばれる部分

次に Ruby の『block.call が呼ばれる部分』、つまり refine_sym_proc_call() の中でどのように処理されているのか見てみましょう。
そろそろ Ruby なのか C言語の話なのかわからなくなってきましたね。
わたしもわかりません。

static VALUE
refine_sym_proc_call(RB_BLOCK_CALL_FUNC_ARGLIST(yielded_arg, callback_arg))
{
    VALUE obj;
    ID mid;
    const rb_callable_method_entry_t *me;
    rb_execution_context_t *ec;

    if (argc-- < 1) {
        rb_raise(rb_eArgError, "no receiver given");
    }
    obj = *argv++;
    mid = SYM2ID(callback_arg);
    me = rb_callable_method_entry_with_refinements(CLASS_OF(obj), mid, NULL);
    ec = GET_EC();
    if (!NIL_P(blockarg)) {
        vm_passed_block_handler_set(ec, blockarg);
    }
    if (!me) {
        return method_missing(obj, mid, argc, argv, MISSING_NOENTRY);
    }
    return rb_vm_call0(ec, obj, mid, argc, argv, me);
}

https://github.com/ruby/ruby/blob/1aef602d5a9398ff362de212ae402ffcd8ff76ae/vm_args.c#L847

refine_sym_proc_call() の実装はこんな感じになっています。
ここでも注目すべき点を上げると

me = rb_callable_method_entry_with_refinements(CLASS_OF(obj), mid, NULL);

この部分になります。
rb_callable_method_entry_with_refinements って名前からしてなんかすごく『refinements を適用させた呼び出し可能なメソッドオブジェクトを取得している』って感じですよね。
実際にこの rb_callable_method_entry_with_refinements() で『現在のコンテキストで Refinements を適用させた呼び出し可能なメソッドのオブジェクト』を取得しています。

Refinements を適用させている箇所を探す

いよいよ rb_callable_method_entry_with_refinements() で、どのようにして『Refinements を適用させているのか』というのを調べてみましょう。
rb_callable_method_entry_with_refinements() は次のような実装になっています。

MJIT_FUNC_EXPORTED const rb_callable_method_entry_t *
rb_callable_method_entry_with_refinements(VALUE klass, ID id, VALUE *defined_class_ptr)
{
    VALUE defined_class, *dcp = defined_class_ptr ? defined_class_ptr : &defined_class;
    const rb_method_entry_t *me = method_entry_resolve_refinement(klass, id, TRUE, dcp);
    return prepare_callable_method_entry(*dcp, id, me);
}

https://github.com/ruby/ruby/blob/1aef602d5a9398ff362de212ae402ffcd8ff76ae/vm_method.c#L892

どんどん潜っていきましょう。
次は method_entry_resolve_refinement() を見ます。

static const rb_method_entry_t *
method_entry_resolve_refinement(VALUE klass, ID id, int with_refinement, VALUE *defined_class_ptr)
{
    const rb_method_entry_t *me = method_entry_get(klass, id, defined_class_ptr);

    if (me) {
        if (me->def->type == VM_METHOD_TYPE_REFINED) {
            if (with_refinement) {
                const rb_cref_t *cref = rb_vm_cref();
                VALUE refinements = cref ? CREF_REFINEMENTS(cref) : Qnil;
                me = resolve_refined_method(refinements, me, defined_class_ptr);
            }
            else {
                me = resolve_refined_method(Qnil, me, defined_class_ptr);
            }

            if (UNDEFINED_METHOD_ENTRY_P(me)) me = NULL;
        }
    }

    return me;
}

https://github.com/ruby/ruby/blob/1aef602d5a9398ff362de212ae402ffcd8ff76ae/vm_method.c#L869

お、ここで興味深いコードが出てきましたね。

const rb_cref_t *cref = rb_vm_cref();
VALUE refinements = cref ? CREF_REFINEMENTS(cref) : Qnil;
me = resolve_refined_method(refinements, me, defined_class_ptr);

ここで refinements というオブジェクトを参照してメソッドを探査しています。
おそらくこの refinements っていうのが Refinements のコンテキスト情報を保持してそうですね。
と、言うことで rb_callable_method_entry_with_refinements() のコードに戻って『refinements を使用した実装』に変えてみましょう。

@@ -836,13 +836,17 @@ refine_sym_proc_call(RB_BLOCK_CALL_FUNC_ARGLIST(yielded_arg, callback_arg))
     ID mid;
     const rb_callable_method_entry_t *me;
     rb_execution_context_t *ec;
+    // ここで refinements を取得
+    const rb_cref_t *cref = rb_vm_cref();
+    VALUE refinements = cref->refinements;

     if (argc-- < 1) {
        rb_raise(rb_eArgError, "no receiver given");
     }
     obj = *argv++;
     mid = SYM2ID(callback_arg);
-    me = rb_callable_method_entry_with_refinements(CLASS_OF(obj), mid, NULL);
+    // refinements を渡して呼び出し可能なメソッドオブジェクトを取得する
+    me = rb_resolve_refined_method_callable(refinements, (const rb_callable_method_entry_t *)rb_method_entry(CLASS_OF(obj), mid));
     ec = GET_EC();
static VALUE
refine_sym_proc_call(RB_BLOCK_CALL_FUNC_ARGLIST(yielded_arg, callback_arg))
{
    VALUE obj;
    ID mid;
    const rb_callable_method_entry_t *me;
    rb_execution_context_t *ec;
    // ここで refinements を取得
    const rb_cref_t *cref = rb_vm_cref();
    VALUE refinements = cref->refinements;

    if (argc-- < 1) {
        rb_raise(rb_eArgError, "no receiver given");
    }
    obj = *argv++;
    mid = SYM2ID(callback_arg);
    // refinements を渡して呼び出し可能なメソッドオブジェクトを取得する
    me = rb_resolve_refined_method_callable(refinements, (const rb_callable_method_entry_t *)rb_method_entry(CLASS_OF(obj), mid));
    ec = GET_EC();
    if (!NIL_P(blockarg)) {
        vm_passed_block_handler_set(ec, blockarg);
    }
    if (!me) {
        return method_missing(obj, mid, argc, argv, MISSING_NOENTRY);
    }
    return rb_vm_call0(ec, obj, mid, argc, argv, me);
}

ここで resolve_refined_method() ではなくて rb_resolve_refined_method_callable() を使用しているのは実装上の都合です。
resolve_refined_method()rb_resolve_refined_method_callable() もだいたい似たような処理で両方共『refinements を指定して』処理することが出来ます。
上記のコードではまだ refine_sym_proc_call() のコンテキスト中、つまり『block.call を呼び出したコンテキストの refinements』を参照しています。

&:twice 時の refinements を参照するようにする

さて、では実際に vm_caller_setup_arg_block() が呼び出されたタイミング refinements をrefine_sym_proc_call()内で参照できるようにしてみましょう。 してみましょうと言っても実際 vm_caller_setup_arg_block() 時の refinements をどうやって refine_sym_proc_call() に渡せばいいんでしょうか。
今回は『rb_func_proc_new() の第二匹引数に block_coderefinements の配列を渡す』ようにしてみます。
Ruby のコードだとこんなイメージですね。

func = proc { refine_sym_proc_call([block_code, refinements]) }

これを実際に C言語で実装してみましょう。

static VALUE
refine_sym_proc_call(RB_BLOCK_CALL_FUNC_ARGLIST(yielded_arg, callback_arg))
{
    VALUE obj;
    ID mid;
    const rb_callable_method_entry_t *me;
    rb_execution_context_t *ec;
    // callback_arg は [block_code, refinements] の配列になっている
    // そこから block_code(Symbol) と refinements の情報を取り出す
    const VALUE symbol = RARRAY_AREF(callback_arg, 0);
    const VALUE refinements = RARRAY_AREF(callback_arg, 1);

    if (argc-- < 1) {
        rb_raise(rb_eArgError, "no receiver given");
    }
    obj = *argv++;
    mid = SYM2ID(symbol);
    me = rb_resolve_refined_method_callable(refinements, (const rb_callable_method_entry_t *)rb_method_entry(CLASS_OF(obj), mid));
    ec = GET_EC();
    if (!NIL_P(blockarg)) {
        vm_passed_block_handler_set(ec, blockarg);
    }
    if (!me) {
        return method_missing(obj, mid, argc, argv, MISSING_NOENTRY);
    }
    return rb_vm_call0(ec, obj, mid, argc, argv, me);
}

static void
vm_caller_setup_arg_block(const rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
                          struct rb_calling_info *calling, const struct rb_call_info *ci, rb_iseq_t *blockiseq, const int is_super)
{
    if (ci->flag & VM_CALL_ARGS_BLOCKARG) {
        VALUE block_code = *(--reg_cfp->sp);

        if (NIL_P(block_code)) {
            calling->block_handler = VM_BLOCK_HANDLER_NONE;
        }
        else if (block_code == rb_block_param_proxy) {
            calling->block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);
        }
        else if (SYMBOL_P(block_code) && rb_method_basic_definition_p(rb_cSymbol, idTo_proc)) {
            const rb_cref_t *cref = vm_env_cref(reg_cfp->ep);
            if (cref && !NIL_P(cref->refinements)) {
                VALUE ref = cref->refinements;
                // [block_code(Symbol), refinements] となるような配列を生成する
                VALUE callback_arg = rb_ary_new_from_args(2, block_code, ref);
                VALUE func = rb_hash_lookup(ref, block_code);
                if (NIL_P(func)) {
                    /* TODO: limit cached funcs */
                    // block_code(Symbol) と refinements を一緒にして refine_sym_proc_call() に渡す
                    func = rb_func_proc_new(refine_sym_proc_call, callback_arg);
                    rb_hash_aset(ref, block_code, func);
                }
                block_code = func;
            }
            calling->block_handler = block_code;
        }
        else {
            calling->block_handler = vm_to_proc(block_code);
        }
    }
    else if (blockiseq != NULL) { /* likely */
        struct rb_captured_block *captured = VM_CFP_TO_CAPTURED_BLOCK(reg_cfp);
        captured->code.iseq = blockiseq;
        calling->block_handler = VM_BH_FROM_ISEQ_BLOCK(captured);
    }
    else {
        if (is_super) {
            calling->block_handler = GET_BLOCK_HANDLER();
        }
        else {
            calling->block_handler = VM_BLOCK_HANDLER_NONE;
        }
    }
}

↑のコードは全部載せているのでちょっと長いですが、修正箇所自体はそんなになくて

@@ -836,13 +836,17 @@ refine_sym_proc_call(RB_BLOCK_CALL_FUNC_ARGLIST(yielded_arg, callback_arg))
     ID mid;
     const rb_callable_method_entry_t *me;
     rb_execution_context_t *ec;
+    // callback_arg は [block_code, refinements] の配列になっている
+    // そこから block_code(Symbol) と refinements の情報を取り出す
+    const VALUE symbol = RARRAY_AREF(callback_arg, 0);
+    const VALUE refinements = RARRAY_AREF(callback_arg, 1);

     if (argc-- < 1) {
        rb_raise(rb_eArgError, "no receiver given");
     }
     obj = *argv++;
-    mid = SYM2ID(callback_arg);
-    me = rb_callable_method_entry_with_refinements(CLASS_OF(obj), mid, NULL);
+    mid = SYM2ID(symbol);
+    me = rb_resolve_refined_method_callable(refinements, (const rb_callable_method_entry_t *)rb_method_entry(CLASS_OF(obj), mid));
     ec = GET_EC();
     if (!NIL_P(blockarg)) {
        vm_passed_block_handler_set(ec, blockarg);
@@ -870,10 +874,13 @@ vm_caller_setup_arg_block(const rb_execution_context_t *ec, rb_control_frame_t *
            const rb_cref_t *cref = vm_env_cref(reg_cfp->ep);
            if (cref && !NIL_P(cref->refinements)) {
                VALUE ref = cref->refinements;
+                // [block_code(Symbol), refinements] となるような配列を生成する
+               VALUE callback_arg = rb_ary_new_from_args(2, block_code, ref);
                VALUE func = rb_hash_lookup(ref, block_code);
                if (NIL_P(func)) {
                    /* TODO: limit cached funcs */
-                   func = rb_func_proc_new(refine_sym_proc_call, block_code);
+                    // block_code(Symbol) と refinements を一緒にして refine_sym_proc_call() に
渡す
+                   func = rb_func_proc_new(refine_sym_proc_call, callback_arg);
                    rb_hash_aset(ref, block_code, func);
                }
                block_code = func;

と、言う感じになります。
ポイントとしては、

VALUE callback_arg = rb_ary_new_from_args(2, block_code, ref);

で、[block, refinements] の配列を生成して、

const VALUE symbol = RARRAY_AREF(callback_arg, 0);
const VALUE refinements = RARRAY_AREF(callback_arg, 1);

で、refine_sym_proc_call() から refinements を参照できるような実装になっています。
本当にこれで動作するようになるの?と思うかと思いますが、上記の修正で最初に提示したコードは問題なく動作するようになります。
やっていることは本当に『&:twice 時の refinemetnsblock.call で参照する』というような修正になります。

まとめ

最終的に出来上がった修正パッチはこちらになります。
上記の修正パッチに加えて『rb_func_proc_new() の結果をキャッシュしている部分』も削除してあります。
今回難しかった点としては、

  • どこで refinements を参照しているのか探すこと
  • refinements をどうやって渡すのか

あたりですかね。
今回の記事だと割とサラッと書いているように感じますが、実際はかなり時間をかけて実装を調べたりしていました。
修正パッチを書くよりも C言語の中を読んでいることの方が圧倒的に多いのでそろそろ Visual Studio とかでいい感じにデバッグしたくなってきますね…。
CRuby って Visual Studio でビルド出来るようにできるんかな…。
あと難しかった点とはちょっと違うんですが、

  • cref->refinements の寿命がわからない
  • キャッシュ化を無効

のあたりはどうすればよくわからなかったですね…。
このあたりはもっと詳しい人に頼むしかなさそう。

ちなみに今回の修正コードを書いてて一番つらかったことは Ruby コミッタの人に今回の事を相談したらだいたい苦虫を噛み潰したような顔をされたことですね…。
まあしゃーないんだけどもみんなもっと Refinements 使おうぜ!という気持ちにはなりました。

追記

今回は &:twice の呼び出しに関して修正してみました Symbol#to_proc は今回とはまた別の箇所で実装されているのでそのままです。 こっちも別で修正する必要があります。