Ruby でワンライナーでズンドコキヨシ
元ネタ
Javaの講義、試験が「自作関数を作り記述しなさい」って問題だったから
— てくも (@kumiromilk) 2016年3月9日
「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ!」って出力した後終了って関数作ったら満点で単位貰ってた
元ネタが2年以上前でめちゃくちゃ今更なんですが、やってみた。
脳内で考えたコードがワンライナーでそのまま動作したのでちょっとうれしかった
Ruby でズンドコキヨシ
puts (1..).lazy.map { %w(ズン ドコ).sample }.each_cons(5).map(&method(:p)).find(&%w(ズン ズン ズン ズン ドコ).method(:==)).join puts "キ・ヨ・シ!" # => # ["ズン", "ドコ", "ズン", "ドコ", "ドコ"] # ["ドコ", "ズン", "ドコ", "ドコ", "ズン"] # ["ズン", "ドコ", "ドコ", "ズン", "ズン"] # ["ドコ", "ドコ", "ズン", "ズン", "ドコ"] # ["ドコ", "ズン", "ズン", "ドコ", "ズン"] # ["ズン", "ズン", "ドコ", "ズン", "ズン"] # ["ズン", "ドコ", "ズン", "ズン", "ドコ"] # ["ドコ", "ズン", "ズン", "ドコ", "ズン"] # ["ズン", "ズン", "ドコ", "ズン", "ズン"] # ["ズン", "ドコ", "ズン", "ズン", "ドコ"] # ["ドコ", "ズン", "ズン", "ドコ", "ドコ"] # ["ズン", "ズン", "ドコ", "ドコ", "ズン"] # ["ズン", "ドコ", "ドコ", "ズン", "ズン"] # ["ドコ", "ドコ", "ズン", "ズン", "ズン"] # ["ドコ", "ズン", "ズン", "ズン", "ズン"] # ["ズン", "ズン", "ズン", "ズン", "ドコ"] # ズンズンズンズンドコ # キ・ヨ・シ!
ワンライナーで書いているのでちょっと複雑かもしれませんが、やっていることは至極単純で
(1..) # Ruby 2.6 から入った (1..) で無限配列を生成 .lazy # 遅延処理するようにする .map { %w(ズン ドコ).sample } # ランダムな "ズン" "ドコ" な配列に変換 .each_cons(5) # 無限リストを5個ずつの配列に分割 .map(&method(:p)) # この時点で一旦出力 .find(&%w(ズン ズン ズン ズン ドコ).method(:==)) # ["ズン", "ズン", "ズン", "ズン", "ドコ"] の組み合わせを探す .join # 結果を結合
無限リスト + #lazy
を使って遅延処理しつつ、#each_cons(5)
で分割して ["ズン", "ズン", "ズン", "ズン", "ドコ"]
という並びがあれば終了する、っていう処理です。
やってることは割と力技。
ちなみに map(&%w(ズン ドコ).method(:sample))
と書きたかったんですが、#sample
が引数を受け取ってしまうのでダメでした。
Ruby 2.6 に投げたパッチが取り込まれた話
先日 Ruby 2.6 がリリースされましたが、それにわたしが投げたパッチが4つ含まれています。
と、いうことで具体的にどんなパッチを投げてどんな機能が取り込まれたのかを簡単に書いてみようかと
Refinements で定義した #to_proc が &hoge 時に呼ばれないのを緩和する提案
表題そのままなんですが、次のように Refinements で to_proc
を定義した場合、&hoge
で to_proc
が呼ばれなかったのを呼ばれるようにしたパッチです。
using Module.new { # Refinements で to_proc を定義 refine String do def to_proc proc { |it| it.send self } end end } p "upcase".to_proc.call "homu" # => HOMU # "upcase".to_proc が呼ばれるようになった p %w(homu mami mado).map &"upcase" # => ["HOMU", "MAMI", "MADO"]
このパッチを投げたのは Ruby 2.5 のリリース直前で、ドサクサに紛れて取り込まれないかなーという気持ちで投げました。
まあ流石に直前だと無理だったので Ruby 2.5 には入りませんでしたが、パッチ自体は特に問題なく取り込まれて Ruby 2.6 で正式に使えるようになりました。
ここから怒涛の Refinements パッチが始まります。
Proposal: Enable refinements to #public_send
先程の to_proc
のパッチが取り込まれてからだいぶ時間が経って、11月取り込まれたパッチになります。
前回からだいぶ間が空いているんですが、この間にもいくつかパッチを投げており…。
matz に『日本語よりも英語で上げたほうがいいよー』とツッコまれたのでこのあたりからがんばって英語でパッチを投げるようにしました。
パッチも diff
ファイルを直接添付していたんですが、CI を回したかったので github 経由で pull request を投げるようにしています。
パッチ自体は以下のように『public_send
でも Refinements を有効にする』と言うような内容です。
using Module.new { # Refinements でメソッドを定義 refine String do def twice self + self end end } # public_send 経由でもメソッドが呼ばれるようになった p "homu".public_send(:twice) # => "hogehoge"
まあ #send
も Refinements が有効になっているから #public_send
も有効になってもいいよねーという気持ちで提案しました。
提案したものの(Refinements 的な意味で)議論されて取り込まれるまでに時間がかかるかなー、と心配していたんですが割とあっさり入ったのでちょっと面食らいました。
感触としては Ruby の方針としてはこのあたりはそこまで厳密に Refinements を制限する、という感じではなさそうな感じなんですかね。
Proposal: Enable refinements to #respond_to?
これも先程の #public_send
と同じで『#public_send
で Refinements を有効にする』というパッチになります。
using Module.new { # Refinements でメソッドを定義 refine String do def twice self + self end end } p "homu".respond_to?(:twice) # => true
こちらも #public_send
と同じタイミングで取り込まれました。
Ruby で定義したメソッドに
&:hogeを渡しても refinements が有効にならない
これは今回投げた中では一番大変なパッチでした。
このパッチを投げた経緯はこちらを参照してもらうと詳しく書いてあります。
パッチ自体は9月ぐらいに書いてすぐに投げたんですが、その後特に進展がなく、先月頭ぐらいにバグ認定されて、今月の頭に取り込まれたんですが、テストで落ちてたみたいで一旦 revert されて…という経緯があります。
Ruby 2.6 に絶対に入れたかったのでテストで落ちていた箇所を直してもう一度パッチを投げたんですが、そしたら別の場所でテストが失敗してしまったらしくて…。
けど、原因を調べてみたら実際はこのパッチとは全然関係ない箇所で、しょうがないのでこっちで原因を調べてみたら実は Ruby のバグを踏んでいてそれを報告して…。
というのがここ1〜2週間のハイライトになります。
まさか trunk で Ruby のバグを踏んでいるとは思わなくて実はかなり悩んでいました。
まあ結果的にこの不具合が修正されたので Ruby 2.6 ではいい感じに Refinements が使えるようになると思います。
using Module.new { # Refinements でメソッドを定義 refine String do def twice self + self end end } def meth &block block.call "homu" end # Ruby で定義したメソッドのブロック引数でも有効になった p meth &:twice
これが今まで動かなかったんですよねー。
何が嬉しいかって ActiveRecord のリレーションで定義されている map
のブロック引数とかでも Refinements が有効になったのが大きいかなーと思います。
ってか、それで使えなくて困っていたのでパッチ書いて投げました。
所感
と、言う感じで Ruby 2.6 で取り込まれたパッチをいくつか紹介しました。
改めて見ると全部 Refinements のパッチですね、これ。
Refinements はもっと使い勝手よくしたいんですよねえ。
ちなみに今回取り込まれたパッチは4つでしたが、実際にはこの倍近くのパッチを投げています。
本当は #method
と #method_missing
で Refinements が有効になるパッチも投げていたんですが、残念ながら Ruby 2.6 には間に合いませんでした。
このあたりが Ruby 2.7 で取り込まれるとだいぶ Refinements が使いやすくなるかなーと思います。
Ruby 2.7 で入るといいですねえ。
そろそろ Ruby のパッチを書くのにも慣れてきたので、何か思いついたらまたパッチを投げていきたいですねー。
パッチを書くよりも取り込まれるまでが大変ですが…。
今日は Ruby 2.6 がリリースされる日!!
今日は年に一度の Ruby がリリースされる日ですね!!!
クリスマス?知らないですねえ…。
と、言うことで Ruby 2.6 が無事にリリースされました。やったー!
そんなわけで気になった機能をいくつか上げてみようかと思います。
ちなみに Ruby 2.6 にパッチを10個ぐらい投げた結果 4つ取り込まれました。わーい。
Kernel#then
が Kernel#yield_self
のエイリアスとして追加
Kernel#yield_self
のエイリアスとして Kernel#then
が新しく追加されました。
yield_self
が then
と短く書けるやったー、と思う一方 then
って名前はどうなんですかね…という思いもあり…。
まあ文字数が半分以下になるのは喜ばしいこと。
Enumerable#filter
Enumerable#filter!
が追加
Enumerable#select
のエイリアスとして Enumerable#filter
が追加されました。
これで同機能の Enumerable
の絞り込みメソッドが
Enumerable#select
Enumerable#find_all
Enumerable#filter
の3つに増えました。
知らない人から見るとますます混乱しそうな気もしますが…。
Ruby をやり始めた頃は『なんで filter
がないんだよ』と思っていた思い出。
Proc#<<
、Proc#>>
が追加
Proc
にはいわゆる関数合成を行う演算子が新しく追加されました。
f = proc{|x| x + 2} g = proc{|x| x * 3} p (f << g).call(3) # -> 11; identical to f(g(3)) p (f >> g).call(3) # -> 15; identical to g(f(3))
f << g
は f(g())
になり、f >> g
は g(f())
になります。
また、Method
にも同等の演算子が追加されています。
f = proc{|x| x + 2} g = proc{|x| x * 3} p (1.method(:+) << 2.method(:*)).call(3) # => 7 (1 + (2 * 3)) p (1.method(:+) >> 2.method(:*)).call(3) # => 8 ((1 + 3) * 2) # Proc と組み合わせたり p (1.method(:+) << g).call(3) # => 10 ((3 * 3) + 1) p (f >> 2.method(:+)).call(3) # => 7 ((3 + 2) + 2)
Proc#<<
、Proc#>>
ともに『#call
が定義されている』オブジェクトを渡すことが出来ます。
class G def call x x * 3 end end f = proc{|x| x + 2} g = G.new p (f << g).call(3) # => 11 f(g(3))
所感
と、いう感じで少しですが Ruby 2.6 の機能を取り上げてみました。
他には終端なし Range が追加されたり #to_h
にブロック引数が追加されたりなどなど。
気になる方はリリースノートを見てみるといいともいます。
さて、Ruby 2.6 がリリースされたということは Ruby 2.7 の開発が始まったことになります。
Ruby 2.6 ではパッチを10個投げた結果 4つ取り込まれました。
Ruby 2.7 でもまだまだ追加したい機能がたくさんあるので、Ruby 2.7 ではもう少しがんばってパッチを投げていきたいと思います。
あと直せる範囲であればバグ修正とかも手伝えるといいですねえ…。
そんな感じで来年も Ruby 2.7 を楽しみにしています!!
Ruby における関数オブジェクトとブロック引数とは…?
Ruby 2.6 で追加される Proc#>>
に Symbol
も渡したいよねー内部で #to_proc
も呼び出してほしいよねーと考えた時の覚書。
現在
Proc#>>
には#call
が定義されているオブジェクトを渡せる
なにをしたい
Proc#>>
に#to_proc
が定義されているオブジェクトも渡したいmethod(:hoge) >> foo.to_proc
をmethod(: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
が生えているオブジェクトは関数オブジェクトとして扱われるProc
やMethod
など
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
を呼び出すシンタックスシュガーがほしい…- 例えば
~hoge
がhoge.to_proc
のシンタックスシュガーであれば(~
は仮 method(:foo) >> :hoge.to_proc
がmethod(:foo) >> ~:hoge
とかけたりwhen :even?.to_proc
をwhen ~: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
このような形になります。
refine
は class
などの構文とは違い単なるメソッドなので 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 化しておらずこのままでは Rails の enum
と競合してしまいますね…。
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"
しかし、残念ながら Ruby の private
はガバガバなので、例えば継承した場合、継承先で意図しないで参照されてしまう(してしまう)可能性があります。
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"
そもそも Ruby の private
はこういう意図で使用するものではないのでしょうがないんですが、それでもやっぱり参照してほしくない、参照されたくないメソッドというのは出てくると思います。
そういうときは 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 化しておくことで利用者の安心感を得る
- gem では
- 雑にメソッドを生やす
- その場でメソッドを生やして可読性を 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? :twice
が false
を返すのに 42.twice
は呼び出し可能な不思議!!
#send
は Refinements が反映されているけど #public_send
は動かない!!
ないよ、メソッドないよぉ!!!!!
と、いう感じで標準メソッドでも Refinements が考慮されていないメソッドはかなりあります。
むしろ対応しているメソッドを上げたほうが早い
method_missing
や const_missing
、method_added
が呼ばれない…
もうやめたげてぇ!となりますがまだまだ続きます。
method_missing
や const_missing
、 method_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_s
も Ruby 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 が有効になっています。
&
引数のto_proc
を Refinements で呼び出せる#public_send
で Refinements が反映される#respond_to?
で Refinements が反映される
また、以下のパッチも投げられているのでもしかしたら Ruby 2.6 に入るかも…?(入らないかも…
- Feature #15374: Proposal: Enable refinements to
#method_missing
- Ruby trunk - Ruby Issue Tracking System - Feature #15373: Proposal: Enable refinements to
#method
and#instance_method
- Ruby trunk - Ruby Issue Tracking System
今後の Refinements には期待できそうですね!!!
あるよ、メソッドあるよぉ!!!!!
ちなみに上のパッチは全部わたしが書いて投げましたてへぺろ
まとめ
と、言う感じで Refinements に関して長々と書いてみました。
Refinements って言うと使う人が限られると思っている人も多いと思いますが、全然そんなことはなくて実際に使ってみるとものすごく使い勝手がよく(そしてとても悪く)て色んな所で Refinements を使いたいと思うようになります。
また、Refinements がいくらクソと言ってもRuby という言語の性質上、クラス拡張の範囲を制限する必要はどうしても必要になって来ると思います。
と、いうか各々が好き勝手クラス拡張しまくってるとそれこそ破綻していき Ruby オワタになってしまう…。
よくわからないクラス拡張に刺されないように gem を作る人は必ず Refinements を使いましょう。
そうでない人も Refinements を使って恐れることなくクラス拡張を使っていきましょう。
クラス拡張は Ruby をいう言語を書く上での醍醐味なので使わないともったいないなあ、というのがわたしの意見です。
Ruby でクラス拡張しないなんて C++ で C言語書いているようなものですよ!!C++20 早く!!!!
さて、Refinements 関連で書き足りないことがたくさんあるんですが、そろそろ時間なのでこの辺にしておきます。
もっと闇の深い Refinements はまた次の機会にでも…。
Refinements 便利なのでみんな使おう!!!
そしてどんどんよくしていこう!!!
来年もどんどんパッチ投げていくぞ!!!!
こういうのは 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
の組み合わせはありだと思いませんか?