hasumikin.com

hasumikin is a programmer.

関ケ原Ruby会議01で発表したTask#usingとは何だったのか

関ケ原Ruby会議01といういかにも癖の強そうなカンファレンスに参加しました。 オーガナイザのみなさん、楽しい会議をありがとうございました。

ちょっと長めの解説記事を書きます。難しくはないです。長いので、サクっと読めることを期待してこの記事を開いた人はいますぐ閉じてください。

発表タイトルは「PicoRubyに於けるRefinementsの再解釈」

話の流れ:

  • Refinementsについての初心者向けの解説
  • Refinementsの有効範囲がファイル内(レキシカルスコープ)であるという仕様についての個人的な不満の気持ち(このスコーピング機能があることは構わないが、もっと別のスコーピングがあってもよいのでは?という意味です)
  • Refinements作者である西軍総大将「世界のshugomaeda」が未だ諦めていない Proc#using というスコーピング仕様の紹介
  • ここからが本題。上記の流れからヒントを得た Task#using というアイデアについて

スライドはこちらです:

PicoRubyに於けるRefinementsの再解釈

要点はこう:

  • R2P2という、マイコン上で動くなんちゃってUNIX風シェルシステムがある。PicoRubyで書かれている
  • ベアメタルマイコンで動いているので、Linux OSの pipe(2) システムコールのようなものには頼れない
  • でもシェルコマンドにパイプラインの機能を加えたい
  • ところで、シェルコマンドもすべてピュアPicoRubyで書かれている
  • コマンド実装内は puts とか gets を単にそのまま書きたい。それらのメソッドがモンキーパッチされるかどうかについてコマンドは関知したくない
  • つまるところ、標準入出力をしれっと差し替えるメタな機能をRubyで書きたい
  • R2P2のシェルコマンドはひとつひとつがTaskインスタンスなので、タスクローカルのRefinements機能があれば、それによって標準入出力をモンキーパッチできるんじゃないだろうか?
  • タスク間の通信には Task::Queue クラスを利用するとよさそう

Refinementsの道具立てでいうと、現状のCRubyは主に RefinementクラスModule#refineModule#using で構成されていますが、3番目のRefine適用メソッドを Task#using へ変更したものが、タスクローカルRefinementsです。

これはRefinementsのスコーピングの考えかたとしてクールじゃない?というつもりだったのですが、たぶんあまり伝わらなかっただろうという手応えが、この長めの記事を書いている動機です。

わかりにくかったかもしれないポイント

いくつかあります。まず、Proc#using と同じスコーピングをPicoRubyでやりたいのだ、と理解した人がいたかもしれません。それは違います。見た目は似ているところもありますが、だいぶ異なるものです。実装詳細も大きく異なります(Proc#using の実物のパッチを見たことはないけど)。

あと、R2P2のシェルコマンドひとつひとつ、つまりパイプラインの両側のセグメントひとつひとつがTaskインスタンスであるのがR2P2のそもそもの設計なのです、という点は、言い忘れました。これを言っていれば、タスクローカルに影響を限定したRefinementsによって標準ストリームをきれいにモンキーパッチできるよね、というアイデアのよさが伝わりやすかったはず。不首尾。

cat hoge.txt | head -1
^^^^^^^^^^^^   ^^^^^^^これもタスク
これがタスク

上記の cat hoge.txthead -1 はR2P2内の別タスクです。前者が内部で呼ぶ Kernel#puts をモンキーパッチしてQueueに出力させ、後者が内部で呼ぶ Kernel#gets をモンキーパッチして同じQueueから読めば、全体としてパイプラインになるよね、というのがこの話の核心です。

このテーマを取り上げるに至った経緯

R2P2をつくりはじめたころ(6年くらい前?)から、ベアメタルマイコンで動くRuby処理系における標準ストリームとはなんなのだろう?と考えていました。
UARTのTXとRXに IO#writeIO#read を接続すればそれでいいのか?いやそんなんじゃない。つまらん。

筆者に後れること数年後、PicoPicoRuby(PicoRubyユーザグループ)の人たちが似たような問題意識を持ちはじめていることは観測していました。
ただ、彼らが考えていたのパイプラインのような切り口ではなく、独自デバイスに都合のよい標準入出力をどのように実装し直すか?あるいはそれのメンテナンス性は保てるのか?というものだったろうと思います。

タスクをRefineすればいいじゃんねー、という思いはどこかの時点で浮かんでいました。しかしこれに着手するきっかけがありませんでした。

そこに「関ケ原」という話が流れてきました。東西大将の推薦フォームに、筆者は笹田さんと前田さん、つまり「実際に選ばれたおふたりを推薦した!」のです。前田さんはRefinementsをつくった人で、笹田さんはCRuby処理系の最適化にあたってRefinementsに手を焼いている人です。

両軍の大将が発表されたときに降ってきました。「機を得たり」

あわせて、Ruby言語がUNIX的な文化をリスペクトしていることをみんなと共有したい、という気持ちがあります。
スライドに書いたとおり、参考図書は『UNIXという考え方 — その設計思想と哲学』と『なるほどUnixプロセス — Rubyで学ぶUnixの基礎』です。わたしがごちゃごちゃ解説するよりも、これらを読んでもらうほうが早いでしょう。どちらも小さな本です。

角谷さんからの援護発矢もあるし:

このトークへの反応いくつか

角谷さんは褒めてくれました(いつも褒めてくれる):

前田さんからは、「べつにRefinementsじゃなくてもやりたいことできるんじゃないですか」と言われました。Refinementsに光を当てたんだから素直に喜べばいいのに、と思ったけど笑ってごまかしました。前田さんから褒められたことないんですよねぇ。

笹田さんは、CRubyのThreadクラスとPicoRuby(mruby)のTaskクラスのAPIがだいたい同じであることを理由に、「TaskじゃなくてThreadという名前でmrubyに入れればよかったのでは?」と突っこんできました。その場ではうまく返答できなかったのでここに書きます。将来Taskクラスに、Threadクラスにはない機能が入るかもしれません。たとえばFiberとのハイブリッド的な機能とか、IO待ちのための機能とか。
そういうのが入ったらCRubyのThreadとは明らかに別物になるので、異なるクラス名でmrubyに入れた判断は正解だと信じています。じつは、mrubyにTaskを入れるにあたって、まつもとさんと筆者とのあいだで名前についての議論はありませんでした。筆者にとってはThreadとTaskが別物であるのは自然だったのですが、まつもとさんがどう考えているのかを聞いたことがありません。そのうち聞いてみたいと思います。

金子さんからは、「後半にちょっと出てきた実装詳細をトークの中心にしなかったのはなぜか?」と聞かれました。20分のスロットには収まりそうになかったからです、と答えたら納得されたようでした。金子さんは東京ruby会議13のことで頭がいっぱいな様子なので、いつもより簡単に納得してくれたかもしれません。
もうちょっと付け加えると、「これはハードテックトークカンファレンスRubyKaigi」じゃないから、というのもあります。あと、トーク冒頭で笹田さんのRefinements愛を抽出したり、Proc#using への界隈の期待をご紹介するというのを通じて、Rubyはにんげんがつくっているのだ、ということを(繰り返しにはなるが)伝えたかった。

ほか、PicoPicoRuby燗系者数名からは、タスクローカルRefinementsのアイデアへの賛同が寄せられました。実際にR2P2を触っている人ならわかるよね。

タスクローカルRefinementsをmrubyのupstreamに入れられるか?

以下の理由により、この実装のRefinementsをmrubyに入れるのは難しそうです。

いまの実装だと Module#refine メソッドが必要になるという点。これはシグネチャ上はCRubyのRefinementsとまったく同じです。同じなのに最終的な利き方が異なるのは言語設計の一貫性にとってよろしくないでしょう。
トップレベルを汚さない、たとえば Task::Box のようなところに実装を押し込めれば、マージできるかもしれません。他方、書き味についての妥協は必要になるかもしれません。…と書いていま思ったけど、Refinementsみたいな機能に書き味が必要なのか?は議論の余地がありそう。いずれにしてもいましばらくの検討が必要です(などと言っているとmruby masterブランチのrebaseが辛くなってゆくのですが)。

あと、以下のふたつは似ているようで非なり:

Task.new do
  Task.current.using RefinedModule
end
task = Task.new do
       end
task.using RefinedModule

後者はコンテスクトの外からRefineを注入するのが一筋縄ではいかない。いまは禁止している。でもやりたい。という課題もあります。


Task#using は、技術的にもっと深化させて用例を敷衍すればRubyKaigiに出せるかもしれないくらいのテーマでした。万全に準備できたかというとそうでもないのですけど、これをあえて「関ケ原」にぶつけることができたのは本懐を遂げた気持ちです。

改めて、奉行衆ならびに参集いただいた大名家臣ご一同、そしてトークの刃を交えた武将諸氏の御尽力、誠に忝なく候。