関ケ原Ruby会議01
(あとでちゃんと書く)
PicoRubyに於けるRefinementsの再解釈
スライド
関連ブログ
内容
R2P2でもUNIXのパイプ(ls | grep のような |)を実現したいというのが発端のモチベーション
PicoRubyとは
マイコンの上で動く超軽量なRuby。
R2P2とは
マイコン上で動く小さなOS/シェル環境
PicoRubyベースのシェル。パソコンのターミナルに相当するものを、Raspberry Pi Picoなどの小さなマイコン上で動かせるようにしたもの
マイコンに接続すると、コマンドを打ち込んでファイル操作したりプログラムを実行したりできる
そのためにTask::Queue(複数の処理をつなぐ仕組み)と、タスクローカルにしたRefinementsを使って、コマンド同士の入出力をつなぎ替えるパイプライン機構を作った
Refinementsとは
Rubyで「既存のクラスに後から機能を追加・上書きする」ことを限られた範囲だけでやる仕組み。
例えば「Stringクラスに新しいメソッドを足したいけど、プログラム全体に影響を与えると危ない」というとき、usingと書いた場所から下だけで有効にできる、という局所的な改造の機能。
なぜRefinementsで実現できるか
パイプ(|)を実現するには、「あるコマンドの出力先」を「次のコマンドの入力」に差し替える必要がある。
普通に差し替えると全体に影響するが、Refinementsを使えば「このコマンド(タスク)の中だけ、標準出力の動きを書き換える 」という局所的な差し替えができる。
タスクローカル
PicoRubyには Task という、複数の処理を同時並行で動かす仕組みがある
Taskとは
Taskは並行して動く処理のかたまり
マイコンで何かを作るとき複数のことを同時にやりたい場面がある
LEDを点滅させながら、ボタンの入力も監視したい。けど、LEDを光らせる処理が終わるまでボタンを見られない
センサーの値を読みながら、別の処理も進めたい。けど、センサーの値の読み込みが終わらなければ別の処理に移れない
Taskだと↑ができる。
マイコンのCPUは基本的に1つの処理しか同時にできない。
PicoRuby は、ものすごく短い時間でTaskを切り替え続けることで、複数が同時に動いているように見せかけている。切り替えはOSではなくPicoRuby自身が管理している。だからマイコンでも並行処理ができる
「Refinementsを、Taskごとに独立して効くようにした」=「タスクローカルRefinements」を実装
これにより、コマンドAとコマンドBがそれぞれ別のタスクとして動きつつ、AとBの間でだけ入出力をつなぎ替える、というパイプの動作が作れる。
R2P2での実現方法
Task::Queue
cat foo.txt | head のようなパイプはだいたいこんなイメージ↓
catコマンド(送り手) ──push──> 共有Queue(箱)──pop──> headコマンド(受け手)
cat と head をそれぞれ別のTaskとして並行に動かす。その2つのTaskの間に データを受け渡す「箱」(Task::Queue) を置き、cat がその箱にデータを入れ、head が箱から取り出す
Task::Queue
各Taskには inbox(受信箱) と outbox(送信箱) が割り当てられる
送り手側が push(箱に入れる)したデータを、受け手側が pop(箱から取り出す)で受け取る。
これがパイプの「出力→入力」の流れの正体
Refinementsで出力先を差し替える
cat コマンドの中身は、普通に puts で標準出力に書いているだけ
Taskの中で Kernel#puts をRefine(局所的に再定義)しておくと、putsの動作を「画面ではなく自分のoutbox(箱)にpushする」に差し替えられる。
同じように受け手側の gets(入力読み取り)は「inbox(箱)からpopする」に差し替わる。
PipelineIO というクラスで、各Taskの入出力を管理する
Taskごとに outbox(送信箱)と inbox(受信箱)を用意する
送り手(cat)の出力は outbox へ、受け手(head)の入力は inbox から、という流れを PipelineIO が仲介