Ruby の lambda に proc オブジェクトを渡した場合、lambda オブジェクトは返ってこない

某所で話題になっているやつです。
Ruby では Kernel.#lambdaproc オブジェクトを生成した場合 lambda として生成されます。

# Kernel.#proc だと lambda でないオブジェクトを生成する
proc_obj = lambda { |a, b| "#{a}:#{b}" }
pp proc_obj.lambda?
# => false

# 引数は厳密にチェックしないので OK
proc_obj.call 1

# Kernel.#lambda だと lambda なオブジェクトを生成する
lambda_obj = lambda { |a, b| "#{a}:#{b}" }
pp lambda_obj.lambda?
# => true

# 引数を厳密にチェックするのでエラーになる
# in `block in <main>': wrong number of arguments (given 1, expected 2) (ArgumentError)
lambda_obj.call 1

Kernel.#lambdaproc なオブジェクトを渡した場合

次のように Kernel.#lambdaproc なオブジェクトを渡した場合、lambda なオブジェクトではなくて proc なオブジェクトのままになります。

proc_obj = proc { |a, b| "#{a}:#{b}" }
lambda_obj = lambda &proc_obj
pp lambda_obj.lambda?          # => false
pp lambda_obj.call 42          # => "42:"

意図としては lambda なオブジェクトになってほしいところではあるんですが、うーん。
lambdaprocnextreturn の挙動も異なるので単純に lambda 化する、っていう対応をしても難しい気がする。
しかし、少なくとも現状の挙動はコードの意図する動作ではないので何かしらの対応が入ってほしい気持ち。
個人的には変換されないなら例外を吐いてほしいなー。

【速報】Ruby の trunk に pipeline operator がはいった

追記

pipeline operator の議論は RubyKaigi 2019 の Ruby Committers vs the World で話が出ていたみたいですね。

www.youtube.com

取り急ぎまとめ。
今日、Ruby の開発者会議に参加させて頂いて、さっき返ってきて ML みてたんですがいつの間にか pipeline operator がマージされていました。
思わず3度ぐらい ML と commit を見直してしまいました。
え、ほんまに???

開発者会議では議論されてなかったんですが、終わったあとに何があったんや…(会場の都合でオフライン勢は先に解散してオンライン勢で議論が続いていたみたい。
と、言うことでこの機能の提案自体は前から興味があったので現時点でどうなっているか簡単にまとめてみました。

|> でメソッド呼び出し

今までは以下のように . でメソッドを呼び出したんですが、

foo()
 .bar(1, 2)
 .display

|> でもメソッドを呼び出せるようになりました。

foo()
 |> bar 1, 2
 |> display

上記の違いをみてもらうとわかると思うんですが、今までは plus(5, 2).to_s(2) のようにメソッドチェーンする場合、 () を付ける必要がありました。
しかし |> を使用すると () をつけずに plus 5, 2 |> to_s(2) のようにメソッドチェーンする事が出来ます。
これにより plus 5, 2 というメソッド呼び出しをチェーンする場合、いちいち plus(5, 2) と書かずに plus 5, 2 |> to_s(2) というようにメソッドチェーンする事が出来ます。

使用例

def plus(a, b)
  a + b
end

# p plus(5, 2).to_s(2) と等価
p (plus 5, 2 |> to_s(2))
# => "111"


# x = (1..).take(10).map { |e| e * 2 }
x = (1.. |> take 10 |> map { |e| e * 2 })
# => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

こんな感じで |> を使うことが出来ます。
その他、詳しい使用例は Ruby 開発者会議の議事録を参照してください。

注意点

|> は優先順位が低いため代入演算子やメソッドの引数に渡す場合に注意があるようです。

p plus 5, 2 |> to_s(2)

の場合は

p(plus 5, 2) |> to_s(2)

と解釈され、

x = 1 + 2 |> to_s(2)

(x = 1 + 2) |> to_s(2)

のように解釈されるみたいです。
これを回避する場合は明示的に () をつけて処理する必要があります。

def plus(a, b)
  a + b
end
p (plus 5, 2 |> to_s(2))
# => "111"

x = (1 + 2 |> to_s(2))
p x
# => "11"

所感

これ、提案を見たときからめっちゃ便利そうーけど Ruby 的にはどうなんだろうなあ…と思っていたんですがあっさりマージされて面食らっています。
もうちょっと |> の記号とか議論されるのかと思っていましたが…。
で、実際書いてみてどうなのかと言われると慣れると『ああ、引数を渡したから () でくくって…』みたいなストレスがなくなるので意外としっくり来ますね。
Ruby はメソッド呼び出しで () を省略出来るのでかなり親和性が高いと思います。
なんというか書いてて気持ちがいいというか。
このあたりは実際に使ってみない事にはわからないと思うので『なんではいったの?』って思う人は1度使ってみてほしいです。
コードリーディングの面でいえば慣れるまでに時間がかかるとは思うんですが果たしてどうなるのか…。
ってか、Ruby 2.7 での追加機能がやばすぎてやばい。

Ripper でパースエラーを検知しようとしてハマった

Ruby でパースエラーになっている箇所を調べたくて Ripper を使ったらハマったので覚書。

Ripper を使う

Ripper とは Ruby に標準で付属している Ruby のコードをパースするためのライブラリです。
Ripper では Ripper というクラスがあり、これを継承してパース時の処理を実装していく感じになります。
例えば、以下のような感じで『任意の Ruby のテキスト』をパースしてエラーを検知することが出来ます

require "ripper"

class FizzBuzz < Ripper
    def compile_error(msg)
        pp __method__
        pp msg
    end
end

FizzBuzz.parse("@%")     # :compile_error
# output:
# :compile_error
# "`@%' is not allowed as an instance variable name"

こんな感じでコンパイルエラーを検知する事が出来ます。

パースエラーを検知する

さて、先ほどと同じように次のような Ruby のコードをパースしてみました。

require "ripper"

class FizzBuzz < Ripper
    def compile_error(msg)
        pp __method__
        pp msg
    end
end

FizzBuzz.parse(<<~EOS)
    def hoge
        1 +
    end

    def foo
EOS

しかし、上記の場合は #compile_error は呼ばれません。
上記のような Ruby のコードでエラー箇所を検知する場合は #compile_error ではなくて #on_parse_error というメソッドが呼ばれます。

require "ripper"

class FizzBuzz < Ripper
    def compile_error(msg)
        pp __method__
        pp msg
    end

    def on_parse_error(msg)
        pp __method__
        pp msg
    end
end

FizzBuzz.parse(<<~EOS)
    def hoge
        1 +
    end

    def foo
EOS
# output:
# :on_parse_error
# "syntax error, unexpected end"
# :on_parse_error
# "syntax error, unexpected end-of-input"

これでいい感じに Ruby のコードのエラーを検知する事が出来ます。

参考

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 で継承するってことはやらないのでスーパークラスのクラスメソッドをサブクラスから呼べるのは知らなかった。