💡

[翻訳] Why Algebraic Effects? - なぜ代数的エフェクトなのか

に公開

なぜ代数的エフェクト(Algebraic Effects)なのか

代数的エフェクト[1](別名: エフェクトハンドラ)は、非常に有用で、今後のプログラミング言語において大きな人気の波が来ると私が個人的に考えている新しい機能です。これはAnteにおける中核的な機能の一つであり、KokaEffektEffFlix といった多くの研究用言語でも注目の的となっています。しかし、多くの記事やドキュメントはエフェクトハンドラ「とは何か」を説明しようとします(Ante自身のドキュメンテーションもそうです)が、「なぜ」それを使うべきかについて深く掘り下げているものは少ないです。本記事ではまさにその「なぜ」を説明し、代数的エフェクトのあらゆるユースケースを可能な限り網羅して紹介します。

構文と意味論についての注意

この記事では多くの箇所でAnteの疑似コードを用います。エフェクトハンドラやAnteに馴染みがない方は、上記のドキュメントリンクや他のエフェクト対応言語の資料を読むとよいでしょう!ただし、「なぜそれが有用か」を示す前に学習意欲を高めるのは難しいことも理解しています(だからこそ本記事があるのです!)。そこで、エフェクトを理解するための良い思考モデルを簡単に紹介します。

代数的エフェクトは本質的には「再開(resume)可能な例外」だと考えることができます。たとえば、以下のようにエフェクト関数を定義できます:[2]

effect SayMessage with
    // このエフェクト関数はUnit型を受け取り、Unit型を返します。
    // `Unit` は命令型言語における `void` とほぼ同じです。
    // 両者には違いもありますが、ここでは関係ありません。
    say_message: Unit -> Unit

この関数を呼び出すことでエフェクトを「投げる(throw)」ことができ、その関数内では検査例外(checked exception)と似たようなやり方で、そのエフェクトを使用可能であることを宣言しなければなりません:

foo () can SayMessage =
    say_message ()
    42

そして、エフェクトを「キャッチする(catch)」には handle 式(try/catch のようなもの)を使います:

handle foo ()
| say_message () ->
    print "Hello World!"  // 「Hello World!」を出力
    resume ()             // 計算を再開して 42 を返す

さらに疑問がある場合は、エフェクトに関するドキュメントを読むことを改めておすすめしますが、エフェクトが使われているときにそれを認識できるようになったので、これから「再開可能な例外」というアイデアがなぜそれほど有用なのかを説明していきます。


ユーザー定義可能な制御フロー

エフェクトハンドラを使う理由として最も一般的に挙げられるのは、通常であれば複数の個別の言語機能(ジェネレータ、例外、非同期処理、コルーチンなど)として実装されるようなものを、単一の言語機能としてライブラリで実装できる点です。さらに、エフェクトハンドラは関数をエフェクトに対して多相(polymorphic)にすることで、"what color is your function"[3] 問題も解決します。たとえば、ベクター(拡張可能配列)の map 関数は次のように一度書くだけで済みます:

map (input: Vec a) (f: a -> b can e): Vec b can e =
    // 実装は省略

この関数の型は、入力関数 f が「任意の」エフェクト e を使えること、そして map 自身もその同じエフェクト e を使うことを表しています。したがって、標準出力に出力する f、非同期関数を呼ぶ f、要素をストリームとして生成する f など、さまざまな f を具体化(instantiate)することができます。多くのエフェクトハンドラを備える言語では、多相なエフェクト変数 e を省略できるため、昔ながらの馴染み深い map の型になります:

map (input: Vec a) (f: a -> b): Vec b =
    // 実装は省略

さて、話を元に戻しましょう。エフェクトハンドラが優れているのは、ジェネレータ、例外、コルーチン、自動微分、さらに、それ以上の多くのものを実装できることです。でも、これらの構造を実装するのは難しく、低レベルな知識が必要になるのでしょう?いいえ、実際には、多くの場合とても単純です。

例外を考えてみましょう。前に代数的エフェクトを「再開可能な例外」として説明しましたが、これはエフェクトを使って例外を実装するための良いヒントにもなります。どうやるのでしょうか?エフェクトを投げたときに resume しなければ良いのです:

effect Throw a with
    throw: a -> never_returns

safe_div x y =
    if y == 0 then
        throw "error: Division by zero!"

    x / y

// 出力: "error: Division by zero!"
handle 
    safe_div 5 0
    print "successfully divided by zero"  // この行には到達しません
| throw msg ->
    print msg

では、もう少し高度な例はどうでしょう?ジェネレータのほうが難しそうですよね?たしかに少しはそうですが、コードは付箋一枚に収まります:

effect Yield a with
    yield: a -> Unit

yield_all_elements_of_vec (vec: Vec a): Unit can Yield a =
    vec.for_each fn elem ->
        yield elem

// ジェネレータをフィルターするため、ジェネレータ関数と、どの要素を残すか判定する述語を渡す
filter (generator: Unit -> Unit can Yield a) (predicate: a -> Bool): Unit can Yield a =
    handle generator ()
    | yield x ->
        // `generator` が要素を yield したら、`predicate` が true を返すときだけ再び yield する
        if predicate x then
            yield x
        resume ()  // 次の要素を続けて yield させる

// yieldされた各要素に関数を適用するための補助関数を追加しましょう
my_for_each (generator: Unit -> Unit can Yield a) (f: a -> Unit): Unit =
    handle generator ()
    | yield x ->
        f x
        resume ()

// 実行してみましょう!
yield_all_elements_of_vec (Vec.of [1, 2, 3, 4])
    // `with` は effect handler 関数を適用するための構文糖
    with filter (fn x -> x % 2 == 0)
    with my_for_each print  // 2 と 4 が出力される

同様に、 yield: Unit -> Unit というエフェクトを使って、協調的スケジューラ(cooperative scheduler)も実装できます。これは、制御をハンドラに戻し、別の関数への実行に切り替える形です。Effektによるその例がこちらです。

要するに、代数的エフェクトを使えば言語の表現力が大きく広がり、さらにうれしいこととして、異なるエフェクト同士の合成(composition)もうまくできます。この点については後ほどさらに詳しく触れますが、エフェクトの合成のしやすさは他のエフェクト抽象と比べても非常に大きな利点です。


抽象として

さて、派手な話題がひと通り終わったところで、代数的エフェクトのあまり目立たない利点について触れておきたいと思います。エフェクトに関する議論はしばしば、ジェネレータ、例外、非同期処理などを実装するため「だけ」のもののように見られがちです。しかし、これらの機能に個人的に関心がない場合でも、一般的な業務アプリケーションにおいて代数的エフェクトを使う十分な理由があります。

そうしたアプリケーションで使う理由の一つは、エフェクトを依存性の注入(Dependency Injection)に使えることです。たとえば、データベースにアクセスするコードがあるとします:

business_logic (db: Database) (x: I32) =
    db.query "..."
    db.query "..."
    db.query "..."
    x * 2

これは一見問題なさそうですが、別のデータベースを使いたくなったり、このデータベースへのアクセスを制限したり、あるいは実際にこの関数をテストしたくなったときに困ります。そこで、データベースをエフェクトに抽象化します:

effect Database with
    query: String -> DbResponse

business_logic (x: I32) can Database =
    query "..."
    query "..."
    query "..."
    x * 2

こうすることで、呼び出し元(たとえば main)から使用するデータベースを差し替えることができます。実際のデータベースの代わりに、テスト用のモックデータベースも使えます:

mock_database (f: Unit -> a can Database): a =
    handle f ()
    | query _msg ->
        // メッセージは無視して常に Ok を返す
        resume DbResponse.Ok

test_business_logic () =
    // 関数全体に `mock_database` ハンドラを適用
    with mock_database

    assert (business_logic 0 == 0)
    assert (business_logic 1 == 2)
    assert (business_logic 21 == 42)
    // など

出力を標準出力に出さず、文字列にリダイレクトすることもできます:

output_messages (): U32 can Print =
    print "Hello!"
    print "Not sure what to write here, honestly"
    1234

// `print` 呼び出しを 1 つの文字列に集め、改行で区切る
print_to_string (f: Unit -> a can Print): a, String can Print =
    mut all_messages = ""

    handle
        result = f ()
        result, all_messages
    | print msg ->
        all_messages := all_messages ++ "\n" ++ msg
        resume ()

// `output_messages` を stdout に出さずにテストできる
test_output_messages () =
    int, messages = output_messages () with print_to_string
    assert (int == 1234)
    assert (messages == "Hello!\nNot sure what to write here, honestly")

あるいは、ログ出力を条件付きで無効化することもできます:

effect Log with
    log: LogLevel -> String -> Unit

type LogLevel = | Error | Warn | Info

LogLevel.greater_than_or_equal self (other: LogLevel): Bool =
    match self, other
    | Error, _ -> true
    | Warn, (Warn | Info) -> true
    | Info, Info -> true
    | _, _ -> false

foo () =
    log Info "Entering foo..."
    log Warn "foo is a fairly lazy example function"
    log Error "an error occurred!"

log_handler (f: Unit -> a can Log) (level: LogLevel): a can Print =
    handle f ()
    | log msg_level msg ->
        if level.greater_than_or_equal msg_level then
            print msg
        resume ()

foo () with log_handler Error  // "an error occurred!" が出力される

よりクリーンな API

代数的エフェクトは、よりクリーンなAPIを設計しやすくする助けにもなります。ほぼすべてのプログラミング言語に共通するパターンとして、「コンテキスト( Context )オブジェクト」の使用があります。これは多くの関数に渡す必要があり、プログラムやライブラリ全体に広がってしまいがちです。このパターンをエフェクトとして表現できます。必要なのは、コンテキストを取得(get)・設定(set)するための関数です:

effect Use a with
    get: Unit -> a
    set: a -> Unit

多くの言語ではこれをstateエフェクトと呼び、使用する状態の型に対してジェネリックです。

次のように、初期状態の値を提供するハンドラを定義できます[4]:

state (f: Unit -> a can Use s) (initial: s): a =
    mut context = initial
    handle f ()
    | get () -> resume context  // `get` の呼び出し元に context を渡す
    | set new_context ->
        context := new_context
        resume ()

このようにして、1つ以上のコンテキストオブジェクトを使うコードを整理することができます。たとえば、内部でベクタを使い、要素への参照をインデックスとして外部に渡すようなコードを考えてみましょう。この場合、通常はベクタをあらゆる関数に渡す必要があります:

type Strings = vec: Vec String
type StringKey = index: Usz

// `!` はミュータブル参照
push_string (strings: !Strings) (string: String): StringKey =
    key = StringKey (strings.len ())
    strings.push string
    key

get_string (strings: &Strings) (key: StringKey): &String =
    strings.get key |> unwrap

append_with_separator (strings: !Strings) (string1_key separator string2_key: String) =
    string1 = get_string strings string1_key
    string2 = get_string strings string2_key
    push_string strings (string1 ++ separator ++ string2)

example (strings: !Strings) =
    string1 = push_string strings "Hello!"
    string2 = push_string strings "Goodbye."

    // `strings` を必要とするすべての関数に明示的に渡す必要があります
    append_with_separator strings string1 " " string2

run_example () =
    mut context = Strings (Vec.new ())
    example !context

しかし、stateエフェクトを使うと、コンテキストの受け渡しを自動的に行えます:

type Strings = vec: Vec String
type StringKey = index: Usz

push_string (string: String): StringKey can Use Strings =
    mut strings = get () : Strings
    key = StringKey (strings.len ())
    strings.push string
    // `Use a` をミュータブル参照に変えたり、
    // `Use !Strings` を経由するように変更することも可能ですが、
    // この例では単に `set` を呼び出すことで `strings` を変更しています
    set strings
    key

get_string (key: StringKey): String can Use Strings =
    strings = get () : Strings
    strings.get key |> unwrap

append_with_separator (string1_key separator string2_key: String) can Use Strings =
    string1 = get_string string1_key
    string2 = get_string string2_key
    push_string (string1 ++ separator ++ string2)

example () can Use Strings =
    string1 = push_string "Hello!"
    string2 = push_string "Goodbye."
    // `strings` を明示的に渡す必要がなくなりました
    append_with_separator string1 " " string2

run_example () =
    context = Strings (Vec.new ())
    example () with state context

このように、 push_stringget_string のような、 strings にアクセスする getset を呼び出す必要がありますが、それらを使うコードでは strings を明示的に渡す必要がなくなりました。一般的に言えば、これらの基本操作を完全にラップするライブラリや抽象化においては、このトレードオフは非常にうまく機能します。ライブラリの利用者は、コンテキストオブジェクトの受け渡しの詳細を意識する必要がなくなるからです。

このパターンはさまざまな場所で登場します。Use a エフェクト を使うと特定のコンテキスト型に縛られますが、必要な関数群をインターフェースとして抽象化することもできます。そのインターフェースの実装に内部コンテキストが必要ならば、それはエフェクトハンドラによって自動的に受け渡されます。この点は、次の話題につながっていきます。

グローバル変数の代替として

プログラマがステートレスだと考えがちなインターフェースの中には、実際には状態の受け渡しが必要であり、多くの場合利便性のためにグローバル変数によって実現されているものがあります。例として、乱数の生成やメモリの割り当てなどが挙げられます。

たとえば、乱数生成のAPIを考えてみましょう:

Prng.new (): Prng = ...

// ランダムなバイトを返す
Prng.random !self: U8 = ...

このAPIを使うためには、乱数を使うすべての箇所で Prng オブジェクトを明示的に渡す必要があります。これはそれほど大きな不便ではないかもしれませんが、プログラムの規模が大きくなるにつれて負担が増します。また、乱数というのは通常、プログラムのロジックにとっては小さな実装詳細にすぎません。そのような小さな実装上の詳細のために、コードの簡潔さを犠牲にしないといけないのでしょうか。これを避けるために、多くの言語やライブラリでは Prng をグローバル変数にすることがありますが、それにはお決まりの欠点があります。特に注目すべきは、そのオブジェクトをスレッドセーフにする必要がある点です。これを次のようなエフェクトにすれば:

effect Random with
    // ランダムなバイトを返す
    random: Unit -> U8

プログラム内でこの状態を(ほぼ)ただでスレッドに受け渡しできるようになります(ただし、呼び出しスタックの上位でハンドラによる初期化は必要です)。さらに、後から Prng オブジェクトの代わりに /dev/urandom やその他の乱数ソースを使いたくなった場合でも、エフェクトハンドラを差し替えるだけで済み、その他のコードには一切変更が不要です。

同様に、次のような Allocate エフェクトを考えてみましょう:

effect Allocate with
    allocate: (size: Usz) -> Alignment -> Ptr a
    free: Ptr a -> Unit

// 使用例
Box.new (elem: a): Box a can Allocate =
    ...

このようなエフェクトにより、呼び出しスタックのどこかで異なるハンドラを与えることで、割り当て方法を簡単に切り替えられます。たとえば、通常の呼び出しではグローバルアロケータを使い、特定の小さなループ内ではアリーナアロケータを使う、ということもハンドラをループの本体に追加するだけで実現できます。

このような例(パーサーやビルドシステムなど)をさらに挙げることもできますが、要点は伝わったと思います。

直接的なスタイルで書く

補足として、エフェクトが専用の値ではなく「投げられる(thrown)/実行される(performed)」ものとして設計されていることで、より直接的なスタイルで記述できる場合が多いという点もあります。

例外は分かりやすい例ですが、これは Future<T> などのラップされたエフェクトを返す非同期関数などにも同様に当てはまります。

例外がない場合、代わりに Maybe t のような型(Some t または None)を使うことになります。複数の処理が Some を返す場合、各ステップの間で map が必要です:

// このような関数があるとき:
try_get_line_from_stdin (): Maybe String can IO = ...
try_parse (s: String): Maybe U32 = ...

// 標準入力から整数を読み取り、それを2倍にして返す
call_failable_functions (): Maybe U32 can IO =
    try_get_line_from_stdin () |>.and_then fn line ->
        try_parse line |>.map fn x ->
            x * 2

このような記述は煩雑なので、Rustなどの言語では ? 演算子のような構文糖が提供され、成功パスの記述に集中できるようになっています。しかしエフェクトを使えば、こうした構文糖は不要です。直接的な記述でそのまま動作します:

// 今度はこのような関数があるとき:
get_line_from_stdin (): String can Fail, IO = ...
parse (s: String): U32 can Fail = ...

// 標準入力から整数を読み取り、それを2倍にして返す
call_failable_functions (): U32 can Fail =
    line = get_line_from_stdin ()
    x = parse line
    x * 2

もし失敗パスを扱いたい場合はハンドラを適用するだけです:

call_failable_functions (): U32 can Fail =
    // `get_line_from_stdin` の Failエフェクトを `default` によって扱う("42" を返す)
    line = get_line_from_stdin () with default "42"
    x = parse line
    x * 2

エラーユニオン(error unions)を使う場合と比べて、値を SomeOk で包む必要がなく、エラー型の合成が難しいという問題もありません:

// 例:
LibraryA.foo (): U32 can Throw LibraryA.Error = ...
LibraryB.bar (): U32 can Throw LibraryB.Error = ...

type MyError = message: String

// 異なるエラー型をそのまま合成できる
my_function (): Unit can Throw LibraryA.Error, Throw LibraryB.Error, Throw MyError =
    x = LibraryA.foo ()
    y = LibraryB.bar ()
    if x + y < 10 then
        throw (MyError "The results of `foo` and `bar` are too small")

もし Throw 節が多くて煩雑に感じたら、扱いたいエフェクトをまとめた型エイリアスを作ることもできます:

AllErrors = can Throw LibraryA.Error, Throw LibraryB.Error, Throw MyError

my_function (): Unit can AllErrors =
    x = LibraryA.foo ()
    y = LibraryB.bar ()
    if x + y < 10 then
        throw (MyError "The results of `foo` and `bar` are too small")

これは、エラーを返すために匿名のユニオン型を使うようなものと考えられます。タグ付きユニオン(tagged union)のように明示的なラッパーを定義する必要はなく、異なるエラー型は自然に合成されて1つのユニオンにまとまります。たとえば、あるライブラリが can Throw String を持っていて、自分のコードも can Throw String を使っている場合、それらは一つの can Throw String に統合されます。逆に別々に保ちたい場合は、上の例のように MyError のようなラッパー型を用いる必要があります。


純粋性の保証

エフェクトハンドラを持つ多くの言語(おそらくOCamlを除くほぼすべて)は、副作用が発生しうる場所すべてにおいてエフェクトを使います。前の例にあった can Printcan IO に気づかれたかもしれません。あなたの思っている通りです。Anteでは副作用を伴う処理を行うには、関数がそれを実行する可能性があることを明示的に記述しなければなりません[5]IO や標準出力の出力がリダイレクトされたり、モックのために使われたりする場合を除けば、こうしたエフェクトは通常 main で自動的に処理されます。では、関数にそのような印を付けさせることにどんな利点があるのでしょうか?

まず一つは、多くの関数が副作用のない(つまり、純粋な)関数を入力として要求するという事実です。たとえばスレッドの生成において、生成されるスレッドが呼び出し元スレッドに属するハンドラにアクセスすることは許されません:

// 与えられた関数群をすべてスレッドとして起動し、それらの完了を待つ
spawn_all (functions: Vec (Unit -> a pure)): Vec a can IO = ...

また、Software Transactional Memory(STM)と呼ばれる並行性のための技法があり、これも純粋関数を必要とします。これは複数の関数を同時に実行し、もしトランザクションの実行中に他のスレッドによって値が変更された場合には、そのトランザクションを単に再実行するというものです。興味のある方は、Effekt におけるそのPoC(Proof of Concept)的な実装をこちらからご覧ください。

再実行可能性

純粋性のもう一つの興味深い側面は、rr デバッガのような再実行可能性(replayability)をもたらすことです。これは、データベースやゲームネットワークにおいて使われる、決定論的なネットワークレプリケーションやログ構造化バックアップのための技術です。

これを実装するには、main が出す最上位のエフェクト(多くの言語では IO と呼ばれる)を処理するための record および replay の2つのハンドラが必要です。record は、エフェクトの発生を記録し、それを再度発生させて(re-raise)組み込みの IO ハンドラに渡し、その結果も記録します。次回の実行時には、 replayIO を処理し、実際の操作をせずに、エフェクトログに保存された結果を使用します。特に賢い言語であれば、デバッグビルド時にデフォルトで記録( record )を行うようにして、常に決定論的なデバッグを可能にすることもできるでしょう。

ケイパビリティベースのセキュリティ

未処理のエフェクトをすべて関数の型シグネチャに含めることを要求する仕組みは、ライブラリのセキュリティ監査において非常に有効です。たとえば get_pi: Unit -> F64 という関数がある場合、それがバックグラウンドでこっそりIO操作を行っていないと分かります。もし後に get_pi: Unit -> F64 can IO に変更された場合、それは何か怪しいことが起こっている兆候であり、その関数を呼んでいる箇所がすでに IO エフェクト を必要としていない限り、エラーとして検出されます[6][7]。これは Capability Based Security(あわせてこちらも: Designing with Static Capabilities and Effects)とも関係しており、そこでは fs: FileSystem のようなケイパビリティオブジェクトを明示的に渡し、それを持つ関数だけがファイルシステムにアクセスできるようになっています。代数的エフェクトでもこれと同様で、関数がケイパビリティの代わりにエフェクトを宣言します。
しかし、このアプローチにも欠点があります。それは先述の通り、エフェクトはプログラム全体に自動的に伝播されるため、たとえば get_piIO を必要とするように更新された場合に、呼び出し元の関数もすでに IO を使っていればエラーにならず、気づかないままエフェクトが伝播してしまうことです。このような状況はエフェクトが使われるあらゆる場面で起こり得ます。たとえば、Fail エフェクトを使うライブラリ関数がもともとは Fail を起こさなかったが、後に Fail を返す可能性が加わった場合、それを can Fail な関数内で使っていれば、既存のハンドラに伝播してしまいます。これは問題ないこともありますが、場合によっては意図しない動作となることもあります。たとえば、ユーザーがエラー時にデフォルト値を返すことで処理したかったかもしれません。


ふぅ、長かったですがここまでで一区切りです。本記事はエフェクトの肯定的側面と、それが将来的により広く使われるようになると私が考える理由に焦点を当ててきましたが、もちろん否定的な側面も存在します。
前述の「エフェクトの意図しない伝播」問題に加えて、エフェクトの主な欠点は伝統的には「効率性への懸念」でした。しかし近年では、エフェクトのコンパイル出力は大きく改善されています。多くの代数的エフェクトを持つ言語では、「末尾再開型(tail-resumptive)」なエフェクト(ハンドラの最後の操作が resume の呼び出しであるようなもの)を通常のクロージャ呼び出しへ最適化します。これは実用上ほとんどのエフェクトがこのカテゴリに該当するため非常に有効です(例外は、あー、例外で、これは再開されないため唯一の例外です!)。言語によっては残りのエフェクトハンドラを最適化する独自の戦略も取っています:Koka は「エビデンス渡し(evidence passing)」と「エフェクトのバブルアップ」を使って、ランタイムなしで C にコンパイルします。AnteOCamlresume の呼び出しを最大1回までに制限することで、非決定性など一部のエフェクトは不可能になりますが、リソース管理が単純化され、継続の内部実装を効率的にできます(例:セグメントスタックによる実装)。Effektはハンドラをプログラムから完全に特殊化する手法をとります[8]

脚注
  1. 「代数的エフェクト」の「代数的」という語は、ほとんど名残のようなものです。実際にはエフェクトハンドラと呼ぶほうが正確かもしれませんが、本記事では一般に広く知られている用語である「代数的エフェクト」を主に用います。また、「エフェクト自体」について話しているのに「ハンドラ」と呼ぶのは紛らわしいと感じるためでもあります。 ↩︎

  2. 訳注: 現状Zennで使われているPrism.jsはAnteのシンタックスハイライトに対応していないようですので、Rustのシンタックスハイライトを使っています。 ↩︎

  3. 訳注: 日本語訳もあります。[翻訳] あなたの関数は何色ですか? ↩︎

  4. この state の定義は所有権ルールを完全に無視しています。実際の実装では Copy a の制約が必要になりますが、本記事では所有権やトレイトの説明に脱線したくなかったため、触れていません。というのも、これらはエフェクト全般には関係がないからです。多くの代数的エフェクトを持つ言語では、値の広範な共有が許容されています。一方、Rustの所有権セマンティクスを継承したAnteはこの点では異端的な存在です。 ↩︎

  5. コンパイラは extern 定義の内容を検査できないため、それらの型定義は信頼するしかありません。また、(予定されている)機能として、デバッグモードでコンパイルしたときのみ IO エフェクトを実行できる仕組みがあり、これによりリリースモードでエフェクトの安全性を保ちながらデバッグ用の出力を可能にすることができます。 ↩︎

  6. これが、通常は最小限のエフェクトを宣言するのが望ましいとされる理由の一つです。たとえば、can IO 全体を指定するのではなく、「この関数は can Print を行う」と明示するようにする、といった具合です。もし最小の(エフェクトの)集合が分からない場合でも、型推論がそれを自動的に導き出してくれます。 ↩︎

  7. get_pi に新しいeffectを追加することは、セマンティックバージョニングの観点でも破壊的変更となるため、バグフィックス版にこっそり含めることはできません。 ↩︎

  8. この方法には「ほとんどの関数がセカンドクラスである」という制約が伴いますが、関数をボックス化して「使った分だけ支払う(pay-as-you-go)」方式に切り替えることで、ファーストクラス関数を得ることも可能です。詳しくはこちらのドキュメントおよびこの論文を参照してください。 ↩︎

Discussion