副作用を値として渡す

メモリは値がどこに住むかに答えた。エフェクトは、あるコードが何をしてよいかに答える —— DB に触れ、時計を読み、メールを送るのか。Mere の答えはケイパビリティ渡しだ:エフェクトを起こす権利は、引数として手渡す普通の値であり、だから関数のパラメータリストが、それにできることの網羅的な一覧になる。そして悪名高い代価 —— 使いもしない関数を通してケイパビリティを配管すること —— を、意図して受け入れる。

mereeffectscapabilitieslanguage-design

Part II は Mere に、値がどこに住み、いつ死ぬかへの明示的な答えを与えた。Part III は、 言語が同じくらい暗黙に委ねたがらない問いを引き受ける:あるコードは何をしてよいのか? この関数はネットワークに届くのか、時計を読むのか、ファイルを開くのか、メールを送るのか? たいていの言語では正直な答えは「呼び出し木を丸ごと読まないとわからない」だ。どの関数も、 どのグローバルでも import して、何でもできるからだ。それこそ Mere の第一原則が反対する、 不可視な力だ。この Part は、それを可視にする話だ。

エフェクトを明示する四つの道

ここにはよく踏査された設計空間があり、選択肢を見ておく価値がある。選択は、言語がすでに 重んじるものから直に導かれるからだ。

  • モナド、Haskell のように —— エフェクトを戻り値の型に通す。明示的で純粋だが、合成が ぎこちなく、実際の構文コストを伴う。
  • 代数的エフェクト、Koka のように —— 関数が行うエフェクトを宣言し、別のところで handle する。合成は美しくテストもしやすいが、計算を中断・再開するランタイム機構は無料 ではない。
  • ケイパビリティ渡し —— エフェクトを起こす権利が値であり、引数として渡される。最大限に 明示的、ランタイムコストゼロ、テストが容易。代価は記述量だ。
  • 注釈のみ、checked exceptions のように —— ただ「この関数は IO をする」と宣言する。 書くのは安いが弱い:注釈が真実から乖離し、コンパイラが本当に縛れない。

これらを、Mere がすでに約束したもの —— 暗黙より明示、ランタイムコストゼロ、そしてコードは 機械の助けで書かれるから記述量は許容できるという扱い —— に並べると、一つが妥協なく嵌まる。

ケイパビリティ渡し

Mere の答えはケイパビリティ渡しだ:エフェクトを行う権限は普通の値であり、それを普通の やり方で手に入れる —— 誰かが引数として手渡すのだ。

fn save_order(
    order:  owned Order,
    db:     &mut borrowed Database,
    logger: &borrowed Logger,
) -> Result[unit, owned DbError] {
    logger.info("saving order")
    db.insert("orders", order)
}

パラメータリストを読めば、その関数の届く範囲を丸ごと読んだことになる。それはデータベースと 話し、ロガーに書ける。DatabaseLogger を与えられたからだ。そして外の世界に対して 他の何もできない。その権限を得る他の道がないからだ —— import すべき環境グローバルもなく、 OS への隠れたチャネルもない。関数が何をしてよいかは、コードの傍らに文書化されるのではなく、 コードのシグネチャそのものである。それが核心のすべてだ:副作用は無料で移動しない、手渡さ れるのが見える値として移動する。

これはまた、特別なフレームワークなしにエフェクトを無料でテスト可能にする。テストは偽の Database と、バッファに書く Logger を渡し、テスト対象の関数はその違いを知ることも、 本物へ逃げることもできない —— それは与えられた値しか持ったことがないからだ。

配管問題と、Mere が配管を残す理由

ケイパビリティ渡しへの定番の反論は、即座で本物だ。深い呼び出し連鎖では、中間の関数が、 自分では決して触れないケイパビリティを、ただ下流へ渡すためだけに受け取らねばならない:

fn handle_request(req, db, cache, logger, mailer) -> Response {
    let user  = authenticate(req, db, logger)?
    let order = process_order(user, db, cache, logger, mailer)?
    build_response(order)
}
// process_order 自身は決してメールを送らない —— 下流の send_confirmation に
// 渡すためだけに mailer を取る

たいていの言語はこの配管を、技術で消し去るべき問題として扱う。ふつうは深い関数がグローバル に直接届けるようにして、中間の関数が知らずに済むようにする。Mere は逆をする:意図して配管 を受け入れる。四つの側面から見た、実は一つの理由で。

記述量それ自体は不利に数えられない —— これは機械の助けで書かれるコードで、新しいケイパビリ ティを連鎖に通すことは、まさに安く作れて安く検証できる機械的な編集だ。より大事なのは、配管が 情報だということだ:パラメータリストに mailer を持つ関数は、たとえ値をただ渡すだけでも、 メールが送られる経路の上にいることが可視だ。そして関数が長いケイパビリティのリストを要求する はめになったら、それは抑えるべきノイズではない —— 設計のシグナルだ。その関数が多すぎる エフェクト経路の上に座っていて、コードは再構成を欲しているかもしれないと告げる。配管が診断で あり、それを隠すことは診断を隠すことだ。

この下には、設計ノートが言い張る鋭い区別がある:「運搬を楽にしない」は「束ねない」と同じ ではない。 Mere は前者を拒む —— エフェクトを不可視に移動させることを目的とする機構は足さ ない。不可視な移動こそ、この Part 全体が防ぐために存在するものだからだ。すべてを魔法のように 通す「ケイパビリティ構造体」は、ケイパビリティ渡しが見せようとしているまさにそれを、再び 隠してしまう。だがそれは、関連するケイパビリティを、不可視にせずに便宜のため名前の下に グループ化できるか、という問いとは別だ —— そしてその問いは、ここでは開いたまま、Part の 後で答えられる。

これが意図して未完に残すもの

ケイパビリティ渡しへの決定は方向を定めるが、設計の全部ではない。継ぎ目について正直である 価値がある。次の数回が、まさにこれらについてだからだ:

  • 高階関数。 map(list, f)f を呼び、f がケイパビリティを要するなら、その要求は どこに現れるのか? それが黙って map のシグネチャに漏れてはならない。さもないと汎用関数が 一つも書けなくなる。これはモデルへの最初の本物の負荷で、次回はそれを解く話だ。
  • エフェクトの種類。 パラメータは、関数がデータベースに触れられることは告げるが、ただ 読むのか書くのかも、エフェクトが排他的か安全に共有できるかも告げない —— ロガーは書き 込まれるが、多くの呼び手が同時にログできる。パラメータの有無は粗すぎる。エフェクトには 粒度が要る。
  • 隠さずに束ねる。 現実的な関数がケイパビリティ五つと region パラメータを要すれば、 シグネチャは長くなる。何らかのグループ化の道がおそらく要る —— 開いた問いは、「運搬を 楽にする」へ戻らずに、それがどんな形を取るかだ。

方向は定まった:エフェクトは値であり、配管もろとも明示的に渡される。Part III の残りは、上の 三つの負荷についてだ。次回:高階関数とケイパビリティ —— 別の関数を取る関数が、エフェクト を漏らさずに明示的に保つ方法。

← Back to Mere: 言語を作る