エフェクト多相
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix では、関数が純粋である(つまり副作用がない)ことを表現できます。
def inc(x: Int32): Int32 \ { } = x + 1
// ^^^ 空のエフェクト集合
inc 関数はエフェクト集合が空であるため 純粋 です。関数が純粋である場合、その関数は同じ引数を与えられれば同じ値を返さなければならないことがわかります。さらに、その関数は外の世界に対していかなる副作用も持つことができません。
空のエフェクト集合は書かなくても構いません。単純に次のように書けます。
def inc(x: Int32): Int32 = x + 1
Flix では、関数が単一のエフェクトを持つことを表現できます。
def incAndPrint(x: Int32): Int32 \ {IO} =
let result = x + 1; // ^^^^ 単一要素のエフェクト集合
println(result);
result
ここで incAndPrint 関数はプリミティブな IO エフェクトを持ちます。
関数が複数のエフェクトを持つことも表現できます。
def copyFile(src: File, dst: File): Unit \ {FsRead, FsWrite, IO} = ...
// ^^^^^^^^^^^^^^^^^^^^ 複数のエフェクト
ここで copyFile 関数は FsRead、FsWrite、IO という3つのプリミティブエフェクトを持ちます。
Flix では、ヒープエフェクトを持つ関数を表現できます。
def nth(i: Int32, a: Array[t, r]): Option[a] \ {r} = ....
// ^^^ ヒープエフェクト
ここで nth 関数はリージョン r におけるヒープエフェクトを持ちます。
異なるエフェクトを混在させた関数を書くこともできます。
use Time.Clock
def strange(a: Array[t, r]): Unit \ {r, Clock, Http, IO}
// ^^^^^^^^^^^^^^^^^^^ エフェクトの混在
この関数はヒープエフェクト r と、Clock、Http、IO という3つのエフェクトを持ちます。
高階関数
高階関数を書くときには、そのエフェクトの振る舞いについて注意深く考えなければなりません。
例えば、高階関数 Set.exists を次のように書くことができます。
def exists(f: a -> Bool \ { }, s: Set[a]): Bool = ...
^^^
ここで exists 関数は述語関数 f が純粋であることを強制します。なぜこのようにするのでしょうか。少なくとも2つの理由があります。(a) Set で使われる反復順序を隠蔽できること、(b) カウントを並列に実行できることです。
とはいえ、必要でない限り関数を純粋であるよう要求するのは、悪いプログラミングスタイルと考えられます。代わりに、エフェクト多相な 関数を書くべきです。エフェクト多相な関数とは、その関数引数のエフェクトに依存してエフェクトが定まる高階関数のことです。
例えば、エフェクト多相な map 関数を次のように書くことができます。
def map(f: a -> b \ ef, l: List[a]): List[b] \ ef = ...
^^ // エフェクト変数 ^^ エフェクト変数
map の型・エフェクトのシグネチャは次のことを示しています。map にエフェクト ef を持つ関数 f が与えられた場合、map の呼び出しはエフェクト ef を持ちます。つまり、f が純粋(つまりエフェクトを持たない)であれば、map の呼び出しも純粋になります。f が IO エフェクトを持つ場合、map の呼び出しは IO エフェクトを持ちます。
List.map(x -> x + 1, l) // { } エフェクトを持つ(つまり純粋)
List.map(x -> {println(x); x + 1}, l) // { IO } エフェクトを持つ
複数の関数引数を取る高階関数は、それらのエフェクトを組み合わせることがあります。
例えば、Flix 標準ライブラリにおける前方関数合成 >> の定義は、2つの関数 f と g を取り、それらを合成します。
def >>(f: a -> b \ ef1, g: b -> c \ ef2): a -> c \ (ef1 + ef2) = x -> g(f(x))
>> の型・エフェクトのシグネチャは次のことを示しています。map にエフェクト ef1 を持つ関数 f とエフェクト ef2 を持つ関数 g が与えられた場合、エフェクトの和集合 ef1 + ef2 を持つ新しい関数を返します。
Flix では、エフェクトの言語は集合の式に基づいています。
efの 補集合 は~efと書きます。ef1とef2の 和集合 はef1 + ef2と書きます。ef1とef2の 共通部分(積集合) はef1 & ef2と書きます。ef1とef2の 差集合 はef1 - ef2と書きます。
最もよく使われる操作は、圧倒的にエフェクトの和集合を計算することです。
同じエフェクト集合を書く方法が複数あり得ることを理解しておくことが重要です。例えば、ef1 + ef2 は、予想される通り ef2 + ef1 と等価です。
エフェクト除外
Flix の新しい機能として、エフェクト除外 のサポートがあります。簡単に言うと、エフェクト除外によって、特定のエフェクトを許可しない一方でその他すべてのエフェクトを許可するような高階関数を書くことができます。
例えば、イベントリスナーの登録関数を次のように書くことができます。
def onClick(listener: KeyEvent -> Unit \ (ef - Block), ...): ...
ここで onClick 関数は、Block エフェクトを 除く 任意の エフェクトを持ち得るイベントリスナーを取ります。したがって、リスナーは UI スレッドをブロックするようなアクションを除いて、任意のアクションを実行できます。
別の例として、例外ハンドラ関数を次のように書くことができます。
def recoverWith(f: Unit -> a \ Throw, h: ErrMsg -> a \ (ef - Throw)): a = ...
ここで recoverWith 関数は2つの関数引数を取ります。例外を投げる可能性のある関数 f と、そのエラーを処理できるハンドラ h です。注目すべきは、エフェクトシステムが h 自身は例外を投げられないことを強制する点です。
サブエフェクティング
注意: この機能はまだ有効化されていません。
Flix は サブエフェクティング をサポートしており、これによって式や関数がそのエフェクト集合を 広げる ことができます。
例えば、次のように書いた場合を考えます。
if (???) { x -> x + 1 } else { x -> {println(x); x + 1}}
最初の分岐は型 Int32 -> Int32 \ { }(つまり純粋)を持つはずであり、一方で2番目の分岐は型 Int32 -> Int32 \ { IO } を持ちます。サブエフェクティングがなければ、{ } != { IO } であるためこれら2つの型は互換性がありません。しかし、サブエフェクティングのおかげで、Flix は最初の分岐に新しいエフェクト変数 ef を用いた型 Int32 -> Int32 \ ef を与えます。これによって型推論は最初の分岐のエフェクトを IO に 広げる ことができます。したがってコンパイラは式全体を型検査できます。
別の例として、次を考えます。
def handle(f: Unit -> a \ (ef + Throw)): a = ...
ここで handle 関数は Throw エフェクトを持つ関数引数 f を期待します。しかし、サブエフェクティングのおかげで、純粋な関数を使って handle 関数を呼び出すこともできます。つまり、次の通りです。
def handle(x -> Throw.throw(x)) // OK。`Throw` エフェクトを持つ。
def handle(x -> x) // OK。サブエフェクティングによる。
def handle(x -> println(x)) // NG。handle は `IO` を許可しない。
Flix はインスタンス宣言でもサブエフェクティングを許可します。
例えば、次のトレイトを定義できます。
trait Foo[t] {
def f(x: t): Bool \ { IO }
}
ここで f は IO エフェクトを持ちます。これを次のように実装できます。
instance Foo[Int32] {
def f(x: Int32): Bool = x == 0 // 純粋関数
}
f の宣言されたエフェクトは IO ですが、ここでの f の実装は純粋です(つまり、空のエフェクト集合 { } を持ちます)。{ } は IO に広げることができるため、このプログラムは依然として型検査を通ります。
しかし、Flix はトップレベル関数についてはサブエフェクティングを許可しません。
例えば、次の関数を宣言した場合を考えます。
def foo(): Bool \ IO = true
Flix コンパイラは次のエラーメッセージを出力します。
❌ -- Type Error ------------------------------
>> Expected type: 'IO' but found type: 'Pure'.
1 | def foo(): Bool \ IO = true
^^^^^^^^^^^^^^^^^^^^^^^^^^^
expression has unexpected type.
まとめると、Flix は2つの場合にエフェクトの拡大を許可します。(a) ラムダ式と (b) インスタンス定義です。これを、Flix は 抽象化サイトのサブエフェクティング と インスタンス定義のサブエフェクティング をサポートしている、と言います。