Ruby のブロック構文の中身を文字列として取得する

using Module.new {
  refine RubyVM::InstructionSequence do
    def to_h
      %i(
        magic
        major_version
        minor_version
        format_type
        misc
        label
        path
        absolute_path
        first_lineno
        type
        locals
        args
        catch_table
        bytecode
      ).zip(to_a).to_h
    end
  end
  using self

  refine Proc do
    def body
      path, (start_lnum, start_col, end_lnum, end_col) = code_location

      raise "Unsupported file" if path.nil?

      File.readlines(path).then { |lines|
        start_line, end_line = lines[start_lnum-1], lines[end_lnum-1]
        if start_lnum == end_lnum
          start_line[(start_col+1)...(end_col-1)]
        elsif end_lnum - start_lnum == 1
          start_line[(start_col+1)...] + end_line[...(end_col-1)]
        else
          start_line[(start_col+1)...] +
          lines[(start_lnum)...(end_lnum-1)].join +
          end_line[...(end_col-1)]
        end
      }
    end

    def code_location
      RubyVM::InstructionSequence.of(self).to_h
        .then { |iseq| [iseq[:absolute_path], iseq.dig(:misc, :code_location)] }
    end
  end
}

pp proc { hoge bar }.body
# => " hoge bar "

pp proc { hoge
  bar }.body
# => " hoge\n" + "  bar "

pp proc { hoge
  foo1
  foo2
  bar }.body
# => " hoge\n" + "  foo1\n" + "  foo2\n" + "  bar "

RubyVM::InstructionSequence.ofProc を渡すことでメタ情報を取得する事が出来るので、そこからソースファイルを読み込み必要な部分だけ切り出しています。
なので、irb とかから呼び出した場合には未対応。
そっちもやりたいんですけどねえ。

【速報】Ruby 2.7 の Numbered parameters が @1 から _1 に変更され、pipeline 演算子が見送りになった

Numbered parameters の記号が @1 から _1 へ変更へ

表題通りなんですが Ruby 2.7 で導入予定であった Numbered parameters の記号が @1 から _1 へと変更になりました。

After discussion and consideration, we picked 1, 2... as numbered parameters. I investigated replaced examples, the current @1 etc. are too confusing with instance variables. So I picked underscore for numbered parameters (a la Scala). We didn't choose Scala's default parameter (_), because it is confusing from my view.

The whole parameter (e.g. |a|) can be captured using 0. The 0 part is kind of compromise, but I think it's acceptable.

The bare _1 etc. in the current code will be warned. You can use those identifiers as local variable names or method names, but they are not recommended from 2.7 and later. And they might be prohibited in the future.

Matz.

と、言う感じに matz の意向により Numbered paramters が @1 から _1 へと変更(予定)となりました。
個人的にはまあ〜〜〜〜 it よりはマシなんだけど〜〜〜既存のコードに対して破壊的変更になるのがな〜〜〜やっぱり嫌だな〜〜〜〜。
っていう気持ち。
ぶっちゃけ自作 gem で _1 とか使っているのでホントやめてほしい破壊的変更やめろ!!!!
まあでも matz がいうならしょうがないかなあ〜〜〜〜〜〜という気持ちしかないにゃんねえ…。
少なくとも Numbered parameters 自体が削除されるよりは…。
細かい挙動とか @1 と差異があるのでそのうちまとめたい。

pipeline 演算子がお蔵入り

Ruby 2.7 で pipeline 演算子が導入される予定だったんですが、いろいろとあり Ruby 2.7 に入ることは断念されました。

After experiments, |> have caused more confusion and controversy far more than I expected. I still value the chaining operator, but drawbacks are bigger than the benefit. So I just give up the idea now. Maybe we would revisit the idea in the future (with different operator appearance).

During the discussion, we introduced the comment in the method chain allowed. It will not be removed.

Matz.

pipeline 演算子に関してはこちらを参照。
まあ〜〜〜これも機能としてはほしいので pipeline 演算子ではなくて全然別の機能として復活してほしい気持ちですねえ。
果たして Ruby 2.7 は最終的にどうなるのか…。

Ruby 2.7 でスターリンソートを書いてみた

元ネタ

と、いうことで Ruby でも書いてみました。
ただ書くだけじゃつまらないので Ruby 2.7 で追加予定のパターンマッチを使ってみるなど。

コード

def stalin_sort(xs)
  case xs
  in []
    []
  in [x]
    [x]
  in [x, y, *zs] if x < y
    [x] + stalin_sort([y, *zs])
  else
    stalin_sort [x, *zs]
  end
end

p stalin_sort [1, 2, 1, 1, 4, 3, 9]
# => [1, 2, 4, 9]
p stalin_sort ['a', 'c', 'b', 'a','c','d']
# => ["a", "c", "d"]

パターンマッチ便利。

表参道.rb #48 で LT してきた

そういえば、書くのを忘れてたので忘れないうちに。

5分ぐらいでわかる Ruby 2.7

Ruby 2.7 で追加される(予定)機能やメソッドの話とかしてきました。
どっちかって言うと機能追加に対する議論とかをもうちょい聞きたかったところ(ナンパラとか。

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 のコードのエラーを検知する事が出来ます。

参考