par_map:よくある場合を人間的にする
spawn と channel は並列性のアセンブリ言語 —— 正しいが、大半のコードが書かされるべきものではない。並列マップは人が実際に手を伸ばす形で、Mere はそれを四度書かずに四つの backend で得る:par_map は spawn・channel・普通の map へ desugar される。それが lower する primitive からあらゆる保証 —— 安全性、byte 単位の parity —— を無料で継ぎ、そして以前の決定からの一つの機微が、desugar を call site ごとに起こさせる。
spawn と channel はいまあらゆる backend で安全に走るが、それらは生の primitive —— 並列性の
アセンブリ言語だ。それらで並列計算を書くことは、仕事を spawn されたスレッドへ手で fan-out し、結果を
channel で fan-in することを、毎回、手で意味する。それは primitive には結構で、日常の道具としては
敵対的だ。この短い回は、大半のコードが実際に欲しがる人間的な形 —— 並列なマップ —— について、そして
それを四度書かずに四つの backend で得ることについてだ。
人が手を伸ばす形
日常の並列操作はマップだ:リストのすべての要素に関数を適用する、だが要素を同時にやる。par_map は
ちょうどそれを与える —— 関数とリストを取り、結果のリストを、並列に計算して返す:
par_map heavy_work items
一行が、手書きの fan-out と fan-in を置き換える。「要素ごとに worker を spawn し、channel で順に集める」 を綴っていたデータ並列プログラムが、単一の呼び出しになる。それがコンビネータの要点のすべてだ: primitive が制御を与え、コンビネータがよくある場合を儀式なしに与える。
四つの実装でなく、desugar で建てる
par_map を加える誘惑的なやり方は、各 backend のランタイムで実装すること —— 書いて一致に保つべき、
四つの並行コードがさらに —— だろう。Mere は逆をした。par_map は desugar で実現される:saturated
な呼び出しが、プログラムがパースされるにつれ、すでに存在するものの組み合わせに書き換えられる。おおよそ、
各要素は自分の結果を新しい channel に送る worker を得、channel は順にリストに集められ、それから各
channel が同じ順で受信される —— fan-out のマップに続く fan-in のマップ、spawn・channel・普通の
リスト map で表現された。
spawn・channel・map がすでに四つの backend で走るから、この単一の書き換えが par_map を四つの
backend で無料で走らせる。新しいコード生成なし、四重の実装なし、backend が食い違う新しい機会なし。
それが desugar の一般的な力だ:新しい構文を、すでに動くもので表現すれば、それはその動くことを丸ごと
継ぐ。順序の保証は channel をリストに集めることから来る;安全性は primitive から来る;byte 単位の
parity も primitive から来る。par_map はそのどれも稼ぐ必要がなかった —— それが lower する先に手渡された。
なぜ desugar が call site ごとに起きるのか
ここに機微があり、それは二回前になされた決定の直接の帰結だ。なぜ par_map を一度、標準ライブラリの
普通の多相関数として、Mere 自身で書かないのか? Send 束縛が単相化制約に真っすぐぶつかるからだ:
その型変数がまだ果たされない Send 義務を運ぶ定義は generalize されず、だから普通のライブラリ関数と
して書かれた par_map は 1 プログラム 1 要素型に固定される —— ある型のリストをマップできるが、同じ
プログラムの他所で別の型のリストをできない。それは、皆が手を伸ばす唯一の操作には、あまりに制限的だ。
desugar はそれをちょうど回避する。各 par_map 呼び出しがそれ自身の call site で、そのサイトの
具体型で lower されるから、あらゆる使用が自分の特殊化された展開を得る —— そして操作はプログラム全体で
真に多相だ、ここで一つの型、あそこで別の型、単相化制約が噛まずに、制限すべき単一の generalize された
定義が無いから。以前の選択 —— Send 制約変数を generalize することを拒んで型システムを solver なしに
保つ —— には代価があり、ここがその代価が払われ回避されるところだ:多相コンビネータが、ライブラリ関数
でなく lowering で届けられる。決定は合成する;一つの答えの形が、三回前の答えに定められる。
無料でついてくるもの
primitive が保証するすべてを、desugar された par_map が余計な仕事なしに保証する。Send 要件は依然
強制される —— 結果は channel を通って移動し、その要素型は送れなければならず、送れない値を返す par_map
は拒否される、生の channel がそれを拒否するのとちょうど同じに。move 追跡も変更を要さない:par_map に
渡される関数は普通の、複数回走りうるクロージャなので、owned cap をそれに move しようとすることは
multi-run 規則ですでに禁じられ、それは要素ごとに一度走る関数にちょうど正しい。そして四つの backend は
なお同一の結果を生む、本物の並列プログラムで検証されて、みな同じ lower された primitive を走らせるから。
それが確立する手法は、この一つのコンビネータより長く残る:高レベルの並列操作は、それを spawn と
channel へ desugar することで届けられ、下から安全性と parity を継ぎ、将来のコンビネータは、それぞれ
四度建てられるのでなく、同じ道を辿れる。
par_map は安全な primitive の上の人間的な表面だ —— そしてその存在が、次回が本当に扱う問いを立てる。
並行処理のロードマップにはもっとあった:atomic、mutex、「完全な」並行処理の物語が含むべき、より低水準の
共有状態の機構。それらは建てられる必要があるか? 答えはチェックリストからでなく、需要から来た ——
どのプログラムが実際にそれらを要するかを問うことから。次回、Part VII の締め:何かを作らないという決定。