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 にパータンマッチ構文が実験的に導入されました。

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 メソッドを呼び出すことが出来ます。

inHash を渡す

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 の構文を見たときは戸惑ったんですが、実際に使ってみると結構理にかなった構文になっている事がわかってきました。
変数に値をキャプチャすることが出来るのがかなりパターンマッチっぽくてよい。
あと ArrayHash などが使えるのはもちろんなんですが、 #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 のトップレベルメソッドって結局なんなの

以下のような質問があったのでちょっと解説。

トップレベルメソッドを理解するためには、以下の3つの Ruby の機能について理解する必要があります。

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 では『classmodule の中で定義していないメソッド』がトップレベルメソッドに該当します。

# 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 クラスは『全てのクラス』が継承しているクラスになります。
例えば StringArray といった組み込みクラスやユーザが定義したクラスも暗黙的に 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 とトップレベルメソッドは特に関連はない

トップレベルの selfmain っていう特別なオブジェクトなので、どうしてもそっちの方に意識が向くんですが、トップレベルの 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

のみが呼ばれます。