Ruby における関数オブジェクトとブロック引数とは…?

Ruby 2.6 で追加される Proc#>>Symbol も渡したいよねー内部で #to_proc も呼び出してほしいよねーと考えた時の覚書。

現在

  • Proc#>> には #call が定義されているオブジェクトを渡せる

なにをしたい

  • Proc#>>#to_proc が定義されているオブジェクトも渡したい
  • method(:hoge) >> foo.to_procmethod(:hoge) >> foo と書きたい
  • Proc#>>Symbol も渡したい
  • #to_proc を Refinements で定義されている場合にも渡したい
class Array
  # to_proc を Refinements で定義したい…
  def to_proc
    proc { |*args|
      self.map { |it| it.to_proc.call(*args) }
    }
  end
end

User = Struct.new(:id, :name, :age)

users = [
  User.new(1, "Homu", 14),
  User.new(2, "Mami", 15),
  User.new(3, "Mado", 14),
]

users.map &method(:pp) >> [:name, :age]

メモ

#call について

  • Ruby では #call が生えているオブジェクトは関数オブジェクトとして扱われる
    • ProcMethod など
  • meth(&block) のようにブロック引数で受け取って block.call のように呼び出す

#to_proc について

  • #to_proc は『関数オブジェクト』を返すメソッド
  • #to_proc はブロック引数のシンタックスシュガーとして使われる
    • &hoge の時に hoge.to_proc が呼ばれる
  • #to_proc が定義されているだけでは『関数オブジェクト』と呼べない
  • ブロック引数で渡したいと気に #to_proc を定義する

Proc#>> について

  • Proc#>> は関数オブジェクトを受け取る
    • 内部で受け取ったオブジェクトの #call を呼び出している
  • なので内部で #to_proc を呼び出すような挙動は一貫性がない
    • #to_proc が定義されているだけのオブジェクトは『関数オブジェクト』ではない
  • Proc#>>#to_proc が定義されているオブジェクトを受け取りたい場合は Proc#>>(a) ではなくて Proc#>>(&block) で受け取るべき
    • 結果的に #to_proc が定義されているオブジェクトを渡すことが出来る
  • しかし method(:hoge).<< do ... end みたいに『ブロック構文』を使うような使い方は想定していない(と思うので) Proc#>>(&block) を定義するのはおかしい
    • #to_proc を呼び出したいだけのために &block 引数を定義するのはおかしい
  • Proc#(a, &block) みたいに定義することで #call#to_proc の両方を受け取ることが出来る
    • ただし、 a&block の両方を渡した場合にどうするのか、という問題は残る

まとめ

任意の関数で、

  • 関数オブジェクトを受け取りたい
  • ブロック引数で受け取りたい
    • ブロック引数で受け取るという前提で meth(&block) 渡しが出来る

を切り分ける必要がある。 今回の Proc#>> は『ブロック引数』で受け取るのではなくて『関数オブジェクト』を受け取るので #to_proc を渡すのは難しそう Proc#>> のように『関数オブジェクト』を期待するメソッドに対しては #call を定義したオブジェクトを渡すべき

まとめ2

  • Proc#>>Symbol を渡したいのであれば
    • Symbol#call を定義すべき
  • Proc#>>#to_proc が定義されているオブジェクトを渡したいのであれば
    • Proc#>>(&block) のようにブロック引数で受け取るべき
  • #to_proc を呼び出すシンタックスシュガーがほしい…
    • 例えば ~hogehoge.to_procシンタックスシュガーであれば(~ は仮
    • method(:foo) >> :hoge.to_procmethod(:foo) >> ~:hoge とかけたり when :even?.to_procwhen ~:even? とかけたりする

そもそも…

次のような構文はシンタックスエラーになる…

class X
    def << &block
    end
end

# syntax error, unexpected &
X.new << &:hoge

備考

  • 関数オブジェクト:#call が定義されているオブジェクト
  • ブロッカブルオブジェクト:#to_proc が定義されているオブジェクト
  • 両対応する場合、どうするのがよいか

コード例

Proc#>> に渡したいのであれば以下のように #to_proc を定義するのではなくて

class Array
  def to_proc
    proc { |*args|
      self.map { |it| it.to_proc.call(*args) }
    }
  end
end

User = Struct.new(:id, :name, :age)

users = [
  User.new(1, "Homu", 14),
  User.new(2, "Mami", 15),
  User.new(3, "Mado", 14),
]

users.map &method(:pp) >> [:name, :age]

以下のように #call を定義するべき

require "pp"

class Array
  def call *args
    self.map { |it| it.to_proc.call(*args) }
  end
end

User = Struct.new(:id, :name, :age)

users = [
  User.new(1, "Homu", 14),
  User.new(2, "Mami", 15),
  User.new(3, "Mado", 14),
]

users.map &(method(:pp) << [:name, :age])

【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 しってるんだぜー』アピールに使用してください。