mrubyにパッチを送ったきっかけ

作品を出せるアイデアが浮かぶかどうかは置いておいて、今年の TRICK に向けて『あなたの知らない超絶技巧プログラミングの世界』を読んでいます。それがきっかけで、巡り巡って mruby にパッチを送る機会があったのを書き残しておきます。新しいことを学ぶ際にいろいろと迂回する良い (?) 事例かもしれません。

eval s=%q(puts %(eval s=%q(#{s}))) のようなコードはふだん使わないプログラミング脳の使い方のトレーニングになるのですが、複雑性が上がっていくと処理系なしで検証するのはなかなか難しかったりします。そこでスマホでも通信なしで Ruby を実行できると便利ということで、最近は rubyist.app という iOS アプリで実験することがあり、その処理系が mruby となっています。

rubyist.app

主題の mruby へのパッチですが、CRuby と mruby の処理系の違いで、超絶技巧プログラミングにおけるアスキーアート定石の %w(...)*'' が動かず、差分としては Array#* に文字列の引数を渡したときの挙動の違いが原因でした。

mruby の振る舞いということで、パッチを書く前に規格を調べてみます。『X 3017:2013 (ISO/IEC 30170:2012) 』規格の『15.2.12.5.2 Array#*』からの抜粋となりますが、引数 num が文字列のときは以下のように未規定でした。

動作: a) numがIntegerクラスのインスタンスでない場合,このメソッドの動作は未規定とする。

kikakurui.com

ここで CRuby の挙動が参考となるのですが、CRuby では Array#* の引数に文字列を渡したときは、Array#join と同様の振る舞いになります。

% ruby -ve "p ['a', 'b', 'c']*''"
ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [x86_64-darwin19]
"abc"

このあたりの流れで mruby に送ったパッチが以下で、このエントリの時点でまつもとさんにマージしてもらっています。ありがとうございます。

github.com

パッチを書くにあたって、CRuby と mruby で API は異なるものの CRuby の rb_ary_times 関数は参考になりました。

本業の Web+DB の世界では直接的に関わりのなかったことから mruby をあまり見てこなかったのですが、デフォルトで Regexp クラスが組み込まれていなかったり、RubyGems とは異なる mrbgems という仕組みに則ったりと異なる世界観がおもしろく、超絶技巧への知識を身に付ける副産物になりました。

ちなみに処理系の検証で毎回スマホで打ち込むのはめんどうなので PC の方で mruby を試したりする際、正規表現を使うには mruby/mruby リポジトリの build_config/default.rb の conf.gem に mrbgems を指定するようです。ここでは mattn さんの mruby-onig-regexp を指定していますが、正規表現の mrbgems にはいくつかあるようです (調べていないですが rubyist.app の Regexp はまた違う実装かも) 。

diff --git a/build_config/default.rb b/build_config/default.rb
index f5e2cbb71..d64e92730 100644
--- a/build_config/default.rb
+++ b/build_config/default.rb
@@ -13,6 +13,8 @@ MRuby::Build.new do |conf|
   # conf.gem :github => 'mattn/mruby-onig-regexp'
   # conf.gem :git => 'git@github.com:mattn/mruby-onig-regexp.git', :branch => 'master', :options => '-v'

+  conf.gem :github => 'mattn/mruby-onig-regexp'
+
   # include the GEM box
   conf.gembox 'default'

今後 mruby を使う機会があるかもしれない人へのご参考まで。

もうひとつ余談ついでに書き残すと、[1, 2, 3] * '' という書き方に対して RuboCop を適用してみたところ Style/ArrayJoin cop が既に存在していて [1, 2, 3].join('') にするよう警告がでました。