Ruby のパターンマッチを利用して任意のメソッドが定義されているかどうかを判定する
と、いうのが bugs.ruby に来ていたので。
class Runner def run end def stop end end runner = Runner.new case runner in .run & .stop :reachable in .start & .stop :unreachable end
ほしい気持ちはわかるんだけど流石に上記の提案だと情報が欠落しすぎているのとそもそもパターンマッチでやるべきこと?と思ってしまい個人的にはいまいち。
なんかもっといい感じの構文だといいとは思うんですが…。
と、言うことで既存のパターンマッチで出来ないかやってみました。
using Module.new { refine Object do # パターンマッチ内部では #deconstruct_keys を暗黙的に呼び出してそれで判定を行っている # そこで deconstruct_keys を経由してメソッド情報を取得することでパターンマッチで利用できるようにする # { メソッド名: 値... } となるような Hash を返す def deconstruct_keys(keys) keys.select { |name| respond_to?(name) }.to_h { |key| [key, send(key)] } end end } def check(obj) case obj in { run: _, stop: _ } :reachable in { start: _, stop: _ } :unreachable else :none end end class Runner def run end def stop end end runner = Runner.new p check(runner) # => :reachable class Runner2 def start end def stop end end runner2 = Runner2.new p check(runner2) # => :reachable
Ruby のパターンマッチでは暗黙的に #deconstruct_keys
(や #deconstruct
)が呼び出され、その戻り値を参照してパターンマッチを評価します。
上記の実装では { メソッド名: 値... }
というような Hash をパターンマッチで使用できるようにすることで
case obj in { run: _, stop: _ } :reachable in { start: _, stop: _ } :unreachable else :none end
というようなパターンマッチをかけるようにしています。
これならメソッドが定義されているかどうかを判定することも出来ますし『任意のメソッドの値』をキャプチャすることも出来ます。
# obj.run の値をキャプチャする case obj in { run: status, stop: _ } p "status is #{status}" :reachable in { start: status, stop: _ } p "status is #{status}" :unreachable else :none end
これ、かなり汎用性が高そうなので普通にほしい。ってか、すでに機能としてありそう。
参照
Ruby の trunk に(実験的に)パターンマッチ構文が入った!
Ruby の trunk にパータンマッチ構文が実験的に導入されました。
- Introduce pattern matching [EXPERIMENTAL] · ruby/ruby@9738f96 · GitHub
- Feature #14912: Introduce pattern matching syntax - Ruby trunk - Ruby Issue Tracking System
NOTE: 実験的に導入されたので今後仕様が変わる可能性があるので注意してください。
試してみた
と、言うことで早速試してみました。
case when
と構文が似ていますが『抽象的にマッチしつつ、マッチした値を変数にキャプチャすることが出来る』っていうあたりが違います。
簡単な使用例としてはこんな感じになります。
def func *x case x in [a] -a in [a, b] a + b in [a, b, c] a * b * c end end p func(1) # => -1 p func(2, 3) # => 5 p func(4, 5, 6) # => 120
case when
と似ていますがパターンマッチでは when
ではなくて in
というキーワードを使用してマッチする構文を記述します。
上記の場合は Array
にマッチしつつ、配列の要素を各変数でキャプチャしています。
無理やり if
文で書くとこんな感じですかね。
def func *x if Array === x && x.size == 1 a, = *x -a elsif Array === x && x.size == 2 a, b = *x a + b elsif Array === x && x.size == 3 a, b, c = *x a * b * c end end
パターンマッチを利用した FizzBuzz
これを利用すると FizzBuzz
を次のように書くことが出来ます。
def fizzbuzz a case [a % 5 == 0, a % 3 == 0] in [true, true] "FizzBuzz" in [_, true] "Fizz" in [true, _] "Buzz" else a end end # self.:fizzbuzz は method(:fizzbuzz) と等価 p (1..15).map &self.:fizzbuzz # => [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]
上記の場合はキャプチャした変数は使っていませんが『何でも受け取れる要素』として変数のキャプチャを利用しています。
だいぶすっきりと書くことが出来ますね。
余談ですが Ruby 2.7 では .:
演算子で method
メソッドを呼び出すことが出来ます。
in
に Hash
を渡す
in
には Hash
を記述することができ、次のような判定を行うことも出来ます。
def validation user case user in { name: /^[a-z]+$/, age: Integer } :ok else :ng end end p validation(name: "homu", age: 14) # => :ok p validation(name: "42", age: 14) # => :ng p validation(name: "mami", age: "14") # => :ng
これはかなり便利そう。
in User(name, age)
記法
in
には in User(name, age)
のような記法を記述することも出来ます。
これは例えば次のように利用できます。
class User attr_accessor :name, :age def initialize name, age self.name = name self.age = age end def deconstruct [name, age] end end def show user case user in User(name, age) p "User(name = #{name}, age=#{age})" in [name, age] p "name=#{name}, age=#{age}" end end homu = User.new("homu", 14) show(homu) # => "User(name = homu, age=14)" show(["mami", 15]) # => "name=mami, age=15"
in User(name, age)
と記述された場合、まず user.kind_of?(User)
で判定が行われ、マッチしていれば user.deconstruct
メソッドで値のキャプチャが行われます。
#deconstruct
メソッドは case when
でいう #===
のようなメソッドで、(多分)パターンマッチ時に内部で暗黙的に呼ばれるメソッドになります。
これを定義することで独自クラスに対してもパターンマッチを行うことが出来ます。
例えば Struct
で #deconstruct
を呼び出す事が出来るようになっているので次のように記述することも出来ます。
User = Struct.new(:name, :age) def show user case user in User(name, age) p "User(name = #{name}, age=#{age})" in [name, age] p "name=#{name}, age=#{age}" end end homu = User.new("homu", 14) show(homu) # => "User(name = homu, age=14)" show(["mami", 15]) # => "name=mami, age=15"
これなかなかに便利そう
所感
最初に case in
の構文を見たときは戸惑ったんですが、実際に使ってみると結構理にかなった構文になっている事がわかってきました。
変数に値をキャプチャすることが出来るのがかなりパターンマッチっぽくてよい。
あと Array
や Hash
などが使えるのはもちろんなんですが、 #deconstruct
を定義することでいろんなクラスでも応用出来るのでかなり便利そうですね。
今後どうなるのかわからない Ruby 2.7 や Ruby 3.0 で最終的にどうなるのかが楽しみです。
個人的には条件を付加しつつ、変数にキャプチャ出来るようになると嬉しいですねえ。
最近気づいた継承と mixin の違い
最近気づいたんですが、
- 継承したスーパークラスのクラスメソッドをサブクラスから呼べる
- mixin したモジュールのクラスメソッドは呼べない
っていう違いがあるんですね。
class Super def self.super_class_method "Super#super_class_method" end def super_instance_method "Super#super_instance_method" end end module M def self.module_class_method "M#module_class_method" end def module_instance_method "M#module_instance_method" end end class Sub < Super include M extend M # スーパークラスのクラスメソッドは呼べる pp super_class_method # mixin したモジュールのクラスメソッドは呼べない # pp module_class_method def sub_instance_method # インスタンスメソッドはどっちからでも呼べる pp super_instance_method pp module_instance_method end end Sub.new.sub_instance_method
あんまり Ruby で継承するってことはやらないのでスーパークラスのクラスメソッドをサブクラスから呼べるのは知らなかった。
Ruby の private セッターメソッドは self. をつけても呼び出すことが出来る
さて、 Ruby の private メソッドは通常レシーバを付けて呼び出す事が出来ません。
class User private def age 42 end end user = User.new # NG : private method `age' called for #<User:0x0000562111c42218> (NoMethodError) p user.age
これは self.
を付けても同じです。
class User def output # OK p age # NG : private method `age' called for #<User:0x00005606cceabb80> (NoMethodError) p self.age end private def age 42 end end user = User.new user.output
セッターメソッドの場合は self.
を付けてもメソッドを呼び出すことが出来る
例外的に #age=
のようなセッターメソッドの場合は private メソッドでも self.
を付けて呼び出すことが出来ます。
class User def set # これだとローカル変数に対して代入される age = 42 # OK : private メソッドだけど self. を付けて呼び出せる self.age = 42 end private def age= value @age end end user = User.new user.set
代入式の場合、レシーバを付けないとローカル変数が定義されてしまうのでこれだけ例外的に呼び出す事が出来るようになっているんですかね。
Ruby 2.7 で `Enumerable#tally` というメソッドが追加される
Ruby 2.7 で Enumerable#tally
というメソッドが追加されます。
あまり聞き慣れない単語のメソッドですが、これは『同じ要素の数を Hash で返す』というメソッドになります。
pp [1, 1, 2, 2, 2, 3, 3, 4].tally # => {1=>2, 2=>3, 3=>2, 4=>1} pp ["homu", "homu", "mami", "mado", "mado", "mado"].tally # => {"homu"=>2, "mami"=>1, "mado"=>3}
https://wandbox.org/permlink/3nAez0P6CDys749b
重複する要素をキーとして、その要素数を値とする Hash
を返します。
配列の要素の数を数えたいことは稀によくあるので、そういう場合に一発で取得できるのは便利そうですね。
また、機能としては Enumerable#group_by
と似ていますが、 #tally
はブロックを受け取らないことに注意してください。
ちなみに tally
という単語は、日本語で数を数える時に使う 正
の字と同じような意味合いがあるらしいです。
Ruby のトップレベルメソッドって結局なんなの
以下のような質問があったのでちょっと解説。
@pink_bangbi
— あつき (@atsuki09130) 2019年2月18日
僕はselfの概念がつかめておらず、バンビさんが以前書かれた記事で4つ目と5つ目(■)の項目の意味が理解できませんでした。 暇な時で構いませんので解説をいただきたいです。 pic.twitter.com/ZgX3UQ6bjj
トップレベルメソッドを理解するためには、以下の3つの Ruby の機能について理解する必要があります。
self
- トップレベル
Object
クラス
self
ってなに
まず、トップレベルメソッドを理解する前に『 self
とは何か』を知る必要があります。
self
とはメソッド内で『メソッドを呼び出したレシーバ』を参照するキーワードになります。
また、レシーバとは『メソッド呼び出しの左辺のオブジェクト』を指します。
class X def foo "foo" end def hoge # meth を呼び出したレシーバを参照する pp self # => #<X:0x00005627e8a8a050> # レシーバ、つまり x の foo メソッドを呼び出す pp self.foo # => "foo" end end x = X.new # メソッドを呼び出す際に左辺にあるオブジェクトの事を『レシーバ』という # この場合は x オブジェクトがレシーバに該当する x.hoge
また、レシーバをつけずにメソッドを呼び出した場合は暗黙的に『self
をレシーバとして』メソッドを呼び出します。
class X def foo "foo" end def hoge # レシーバがなければ self のメソッドを呼び出す # self.foo と等価 pp foo # => "foo" end end
トップレベルって?
そもそも『Ruby のトップレベルって何?』っていう話なんですが、Ruby では『class
や module
の中で定義していないメソッド』がトップレベルメソッドに該当します。
# class や module 内で定義していないメソッド def topleve_method "topleve_method" end class X # これは X のインスタンスメソッドなのでトップレベルではない def meth end end
また、トップレベルでも self
は存在し、トップレベルで self
を呼び出した場合、 main
という名前のオブジェクトを返します。
pp self # => main
main
はトップレベルで定義されている特別なオブジェクトになります。
トップレベルでメソッドを定義するとどうなるの?
トップレベルでメソッドを定義した場合、暗黙的に『Object
クラスの private
メソッド』として定義されます。
どういうことかというと
def hoge "hoge" end
というトップレベルメソッドは、
class Object private def hoge "hoge" end end
という定義と等価になります。
ちなみに Ruby における private
とは『レシーバをつけて呼び出すことが出来ないメソッド』になります。
class X # hoge を private メソッドとして定義する private def hoge end def foo # OK : レシーバをつけずに呼び出す hoge # NG : self もつけて呼び出すことは出いない self.hoge end end x = X.new # NG : レシーバをつけて呼び出すことは出来ない x.hoge
Object
クラスって何
Object
クラスは『全てのクラス』が継承しているクラスになります。
例えば String
や Array
といった組み込みクラスやユーザが定義したクラスも暗黙的に Object
クラスを継承しています。
# Module#< で任意のクラスを継承しているかチェックする # 組み込みクラスは Object を継承している p String < Object p Array < Object # ユーザが定義したクラスも暗黙的に Object を継承している class X end p X < Object
つまり 『トップレベル定義したメソッド= Object
クラスの private
メソッド』は『 Object
を継承しているクラス=全てのクラス』の『private
メソッド』として参照できます。
# Object の private メソッドとして定義される def hoge "hoge" end class X def foo # Object は親クラスなので # 親クラスの private メソッドが呼べる hoge end end x = X.new p x.foo # => "hoge" # NG : private メソッドなので直接呼べない # p "foo".hoge # OK : send だと private メソッドも明示的に呼べる p "foo".send(:hoge) # => "hoge"
結局トップレベルでメソッドって?
- トップレベルでメソッドを定義すると Object のメソッドとして定義される
- Object は全てのクラスが継承しているのでどのクラスからでも呼び出せる
- 当然トップレベルの
self
(=main
)もObject
クラスを継承しているのでトップレベルでも呼び出せる main
とトップレベルメソッドは特に関連はない
トップレベルの self
が main
っていう特別なオブジェクトなので、どうしてもそっちの方に意識が向くんですが、トップレベルの self
とトップレベルのメソッドは特に関連性はありません。
まずは、『トップレベルメソッド』というのが先にあり、『main
』っていうのが後にあります。
ちなみに『トップレベルメソッド』の他に『トップレベル定数』というものがありトップレベル定数はトップレベルメソッドの100000万倍ぐらいややこしい仕様なので知らないほうがいいです。
Rails の touch 時に処理をフックする
任意のレコードの updated_at
のみを更新する際に ActiveRecord の #touch
を使うことはあると思います。
class User < ActiveRecord::Base end user = User.create(name: "Homu") pp user.updated_at.iso8601(10) # => "2019-02-18T12:36:37.2315494900Z" # updated_at のみを更新する user.touch pp user.updated_at.iso8601(10) # => "2019-02-18T12:36:37.2315494900Z"
#touch
時に処理をフックする
#touch
がやっていることは『updated_at
の更新』なので after_save
等でフックしたくなるんですが残念ながら #touch
時には after_save
は呼ばれません。
class User < ActiveRecord::Base # #touch 時に after_save は呼ばれない after_save { pp "after_save" } end
#touch
時に処理をフックする場合は after_touch
を使用します。
class User < ActiveRecord::Base # touch で更新した後に after_touch が呼ばれる after_touch { pp "after_save" pp updated_at.iso8601(10) } end user = User.create(name: "Homu") pp user.updated_at.iso8601(10) user.touch pp user.updated_at.iso8601(10) # output: # "2019-02-18T12:40:49.6699328380Z" # "after_save" # "2019-02-18T12:40:49.6741588740Z" # "2019-02-18T12:40:49.6741588740Z"
#touch
がやっていることは更新なので after_save
が呼ばれて当然と思っていてハマりました。
#touch
時には、
after_touch
after_commit
after_rollback
のみが呼ばれます。