Flix へようこそ
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、オーフス大学、オープンソースコントリビューター、ウォータールー大学、テュービンゲン大学、コペンハーゲン大学の研究者と協力して開発されている、原則に基づいた関数型・論理型・命令型プログラミング言語です。
Flix は OCaml と Haskell に触発されており、Rust と Scala からのアイデアも取り入れています。Flix は Scala に似た見た目ですが、その型システムは完全な型推論をサポートする Hindley-Milner に基づいています。Flix は以下のような複数の革新的な機能を備えた最先端のプログラミング言語です:
- 完全な型推論を備えた多相型およびエフェクトシステム
- リージョンベースのローカルミュータブルメモリ
- ユーザー定義のエフェクトとハンドラ
- 関連型と関連エフェクトを持つ高カインドトレイト
- 組み込みの第一級 Datalog プログラミング
Flix は効率的な JVM バイトコードにコンパイルされ、Java 仮想マシン上で動作し、完全な末尾呼び出し除去(tail call elimination)をサポートしています。Flix は Java との相互運用性を持ち、JVM のクラスやメソッドを使用できます。そのため、Java エコシステム全体が Flix から利用可能です。
Flix は世界クラスの Visual Studio Code サポートを目指しています。Flix Visual Studio Code 拡張機能は実際の Flix コンパイラを使用するため、Flix 言語とエディタで報告される内容の間に常に 1:1 の対応があります。利点は多数あります:(a) 診断は常に正確、(b) コードナビゲーションが「そのまま動く」、(c) リファクタリングは常に正しい。
ルック・アンド・フィール
Flix の見た目と使用感を示すいくつかのプログラムを紹介します:
このプログラムは代数的データ型とパターンマッチングの使用を示しています:
/// 図形のための代数的データ型
enum Shape {
case Circle(Int32), // 円の半径
case Square(Int32), // 辺の長さ
case Rectangle(Int32, Int32) // 高さと幅
}
/// パターンマッチングと基本的な算術演算を使用して
/// 与えられた図形の面積を計算する
def area(s: Shape): Int32 = match s {
case Shape.Circle(r) => 3 * (r * r)
case Shape.Square(w) => w * w
case Shape.Rectangle(h, w) => h * w
}
// 2 x 4 の面積を計算する
def main(): Unit \ IO =
area(Shape.Rectangle(2, 4)) |> println
以下は多相レコードを使用した例です:
/// 多相レコード `r` の面積を返す
/// 型変数 `a` を使用することで、レコード `r` が
/// `x` と `y` 以外のラベルを持つことができる
def polyArea[a : RecordRow](r: {x = Int32, y = Int32 | a}): Int32 = r#x * r#y
/// 様々な長方形レコードの面積を計算する
/// 一部のレコードには追加のラベルがある
def polyAreas(): List[Int32] =
polyArea({x = 1, y = 2}) ::
polyArea({x = 2, y = 3, z = 4}) :: Nil
def main(): Unit \ IO =
polyAreas() |> println
以下はリージョンベースのローカルミューテーションを使用した例です:
///
/// リージョンを使用して、内部的なミュータビリティ(不純性)を
/// 使用する純粋関数を定義できる
/// リージョンはミュータビリティを宣言されたスコープにカプセル化する
///
def deduplicate(l: List[a]): List[a] with Order[a] =
/// 新しいリージョン `rc` を宣言する
region rc {
/// リージョン `r` に新しい `MutSet` を作成する
/// これは `l` 内のユニークな要素を追跡するために使用される
let s = MutSet.empty(rc);
/// `filter` の呼び出しで使用されるラムダは
/// リージョンがなければ不純になる
List.filter(x -> {
if (MutSet.memberOf(x, s))
false // `x` は既に出現済み
else {
MutSet.add(x, s);
true
}
}, l)
}
以下は組み込みのエフェクトとハンドラを使用した例です:
use Net.Http
use Net.Retry
use Net.HttpResponse
use Time.Clock
use Time.Duration.{milliseconds, seconds}
def main(): Unit \ { Clock, Http, Logger, IO } =
let defaultHeaders = Map#{
"Accept" => List#{"application/json"},
"Authorization" => List#{"Bearer tok123"}
};
run {
let urls = List#{"/api/users", "/api/posts"};
foreach (url <- urls) {
match Http.get(url) {
case Ok(resp) => println("${url} -> ${HttpResponse.status(resp)}")
case Err(err) => println("${url} -> ${err}")
}
};
match Http.get("https://notfound.flix.dev/") {
case Ok(resp) => println("notfound -> ${HttpResponse.status(resp)}")
case Err(err) => println("notfound -> ${err}")
}
} with Http.withBaseUrl("https://flix.dev")
with Http.withDefaultHeaders(defaultHeaders)
with Http.withRetry(Retry.linear(maxRetries = 2, delay = milliseconds(100)))
with Http.withCircuitBreaker(failureThreshold = 3, cooldown = seconds(5))
with Http.withSlidingWindow(maxRequests = 2, window = seconds(1))
with Http.withLogging
以下はユーザー定義のエフェクトとハンドラの例です:
eff MyPrint {
def println(s: String): Unit
}
eff MyTime {
def getCurrentHour(): Int32
}
def sayGreeting(name: String): Unit \ {MyPrint, MyTime} = {
let hour = MyTime.getCurrentHour();
if (hour < 12)
MyPrint.println("Good morning, ${name}")
else
MyPrint.println("Good afternoon, ${name}")
}
def main(): Unit \ IO =
run {
(sayGreeting("Mr. Bond, James Bond"): Unit)
} with handler MyPrint {
def println(s, k) = { println(s); k() }
} with handler MyTime {
def getCurrentHour(_, k) = k(11)
}
以下は第一級 Datalog 制約を使用した例です:
def reachable(edges: List[(Int32, Int32)], src: Int32, dst: Int32): Bool =
let db = inject edges into Edge/2;
let pr = #{
Path(x, y) :- Edge(x, y).
Path(x, z) :- Path(x, y), Edge(y, z).
Reachable() :- Path(src, dst).
};
let result = query db, pr select () from Reachable();
not Vector.isEmpty(result)
最後に、チャネルとプロセスを使用した構造化並行性の例です:
/// リストのすべての要素を送信する関数
def sendAll(l: List[Int32], tx: Sender[Int32]): Unit \ Chan =
match l {
case Nil => ()
case x :: xs => Channel.send(x, tx); sendAll(xs, tx)
}
/// n 個の要素を受信してリストに集める関数
def recvN(n: Int32, rx: Receiver[Int32]): List[Int32] \ {Chan, NonDet} =
match n {
case 0 => Nil
case _ => Channel.recv(rx) :: recvN(n - 1, rx)
}
/// receive を呼び出し、結果を tx に送信する関数
def wait(rx: Receiver[Int32], n: Int32, tx: Sender[List[Int32]]): Unit \ {Chan, NonDet} =
Channel.send(recvN(n, rx), tx)
/// send と wait のプロセスをスポーンし、結果を表示する
def main(): Unit \ {Chan, NonDet, IO} = region rc {
let l = 1 :: 2 :: 3 :: Nil;
let (tx1, rx1) = Channel.buffered(100);
let (tx2, rx2) = Channel.buffered(100);
spawn sendAll(l, tx1) @ rc;
spawn wait(rx1, List.length(l), tx2) @ rc;
println(Channel.recv(rx2))
}
その他の例は、本書の各ページや GitHub の examples フォルダで確認できます。
はじめに
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix を始めるのは簡単です。必要なのは Java バージョン 21 以上だけです。
Java がインストールされているかどうか、またそのバージョンを確認するには、以下のコマンドを入力します:
$ java -version
以下のような出力が表示されるはずです:
openjdk version "21" 2023-09-19 LTS
OpenJDK Runtime Environment Temurin-21+35 (build 21+35-LTS)
OpenJDK 64-Bit Server VM Temurin-21+35 (build 21+35-LTS, mixed mode, sharing)
Java がインストールされていない場合や、バージョンが古い場合は、Adoptium から新しいバージョンをダウンロードできます。
Java 21 以上をインストールしたら、以下の2つの方法で進めることができます:
- Flix VSCode 拡張機能を使用する(強く推奨)、または
- コマンドラインから Flix コンパイラを実行する。
VSCode で Flix を使う
Flix にはフル機能の VSCode プラグインが付属しています。以下の手順で始めましょう:
- 新しい空のフォルダを作成します(例:
my-flix-project)。- VSCode を開き、
File -> Open Folderを選択します。- フォルダ内に
Main.flixという名前の新しいファイルを作成します。- VSCode がマーケットプレイスで拡張機能を検索するかどうか尋ねてきます。「Yes」と答えてください。
- Flix 拡張機能 がダウンロードされ、インストールされます。完了すると、Flix コンパイラ をダウンロードするかどうか尋ねてきます。再び「Yes」と答えてください。
- 「Starting Flix」に続いて「Flix Ready!」と表示されたら、準備完了です。
Flix Visual Studio Code 拡張機能の動作中のスクリーンショット:
Neovim で Flix を使う
Flix は Neovim からも使用できます。以下の手順で始めましょう:
- 公式プラグインは Neovim 0.11 でリリースされた機能に依存しています
- インストールされている Neovim のバージョンを確認してください
nvim --version
Neovim プラグイン
ネイティブ Neovim LSP 用の LSP 設定と、Flix CLI と対話するためのいくつかの関数を提供する Lua プラグインがあります。リポジトリには詳細なインストールと設定手順があります。 お好みのプラグインマネージャーでインストールするか、Neovim のランタイムパスにローカルでクローンできます。
プラグインはキーマッピングを提供しませんが、Flix LSP サーバーをセットアップし、デフォルトの LSP マッピングで動作するようにします。nvim 0.11 での LSP キーマッピングの設定例を以下に示します。LspAttach autocmd を設定すると、キーマッピングはすべての設定された LSP サーバーに適用されます。
-- LSP サーバーがバッファにアタッチしたときにトリガーされる autocmd を作成
vim.api.nvim_create_autocmd('LspAttach', {
-- autocmd の競合を防ぐために名前を付ける
group = vim.api.nvim_create_augroup('my.lsp', {}),
-- サーバーがアタッチしたときに実行する関数
callback = function(args)
-- オプションの説明文字列を持つキーバインディングオプションを設定するヘルパー関数
-- !!! 重要 !!!
-- `buffer` はこれらのマッピングを autocmd をトリガーするバッファのみに設定する
local function get_opts(desc)
return { desc = desc, buffer = args.buf, noremap = true, silent = true }
end
-- キーバインディングを設定する前に LSP クライアントが機能をサポートしているか確認
local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
if client:supports_method('textDocument/format') then
vim.keymap.set('n', '<leader>=', vim.lsp.buf.format, get_opts('format buffer'))
end
if client:supports_method('textDocument/rename') then
vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, get_opts('rename'))
end
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, get_opts("lsp code action"))
vim.keymap.set("n", "<leader>cl", vim.lsp.codelens.run, get_opts("lsp codelens run"))
vim.keymap.set("n", "gr", vim.lsp.buf.references, get_opts("lsp references"))
vim.keymap.set("n", "gd", vim.lsp.buf.definition, get_opts("lsp definition"))
vim.keymap.set("n", "<leader>h", vim.lsp.buf.document_highlight, get_opts("lsp document highlight"))
vim.keymap.set("n", "K", vim.lsp.buf.hover, get_opts("lsp hover"))
vim.keymap.set("n", "gi", vim.lsp.buf.implementation, get_opts("lsp buf implementation"))
vim.keymap.set('i', '<C-a>', '<C-x><C-o>', get_opts("manual expand completion"))
vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, get_opts(""))
vim.keymap.set("n", "<leader>d", vim.diagnostic.open_float, get_opts("diagnostic open float"))
vim.keymap.set("n", "<leader>ws", vim.lsp.buf.workspace_symbol, get_opts("lsp workspace symbol"))
vim.keymap.set("n", "<leader>ds", vim.lsp.buf.document_symbol, get_opts("lsp doc symbol"))
end
})
上記のスニペットは以下のキーバインディングを提供します。
| キーバインド | アクション |
|---|---|
gd | 定義へ移動 |
gi | 実装へ移動 |
gr | 参照を検索 |
ctrl+a | 自動補完をトリガー |
shift+k | ホバー |
<leader>rn | シンボルの名前変更 |
<leader>ca | コードアクション |
<leader>cl | コードレンズを実行 |
<leader>ws | ワークスペースシンボルを表示 |
<leader>ds | ドキュメントシンボルを表示 |
<leader>d | 診断を表示 |
<leader>h | ドキュメントハイライトを表示 |
以前は lspconfig が Neovim に LSP 機能と設定を提供していました。しかし、バージョン 0.11 以降、Neovim には LSP が組み込まれており、lspconfig は一般的な LSP サーバーの設定のみを提供します。これにより、インストールの必要性は減りましたが、まだ推奨されています。
インストール
お好みのプラグインマネージャーで flix/nvim リポジトリを使ってインストールしてください。
Neovim 0.12 には組み込みのプラグインマネージャーが搭載されているため、サードパーティのツールは不要です。
-- Neovim 0.12+ (vim.pack)
vim.pack.add({ "https://github.com/flix/nvim" })
require("flix").setup()
vim.lsp.enable("flix")
lazy.nvim を使う場合:
{
"flix/nvim",
ft = "flix",
config = function()
require("flix").setup()
vim.lsp.enable("flix")
end,
}
設定
require("flix").setup() を呼び出して設定を登録し、その後サーバーを有効化します。
-- Flix LSP の設定を作成
require("flix").setup()
-- サーバーを有効化
vim.lsp.enable("flix")
autocommand を使うか、自分の設定に ftplugin/flix.lua を作成して、Flix 用のローカルキーバインディングを設定してください。以下の例では、Flix プロジェクトの run/test をキーにマッピングしています。
-- `flix_cmd` 関数をインポート
local flix_cmd = require("flix.commands").flix_cmd
-- `buffer` を設定することでマッピングが Flix バッファのみに限定される
local bufnr = vim.api.nvim_get_current_buf()
vim.keymap.set("n", "<Space>br", function() flix_cmd("run") end,
{ noremap = true, silent = true, buffer = bufnr, desc = "run flix project" })
vim.keymap.set("n", "<Space>bt", function() flix_cmd("test") end,
{ noremap = true, silent = true, buffer = bufnr, desc = "test flix project" })
flix_cmd は root_markers からプロジェクトのルートを見つけるため、LSP がアタッチされていない場合でも動作し、flix.toml が見つからない場合はエラーを報告します。
このコードを
$HOME/.config/nvim/init.luaまたは Neovim でプラグインを設定している場所に配置してください。
Emacs で Flix を使う
Flix は Emacs からも使用できます。プレーンな Emacs でも、Spacemacs でも利用可能です。どちらも Flix コンパイラの LSP サーバーを使って、フルの言語サポートを提供します。
プレーンな Emacs
flix-mode パッケージ(メジャーモード)をインストールし、LSP クライアント(eglot または lsp-mode)で設定してください。README に、両方に対応した最小限のすぐに使える設定が記載されています。
Spacemacs
flix レイヤーを追加してください。これにより、flix-mode が lsp-mode と Spacemacs のキーバインドシステムに接続されます。README にインストールと設定の手順が記載されています。
コマンドラインで Flix を使う
Flix はコマンドラインからも使用できます。以下の手順に従ってください:
- 新しい空のフォルダを作成します(例:
my-flix-project)。- 最新の
flix.jarを https://github.com/flix/flix/releases/latest からダウンロードし、フォルダに配置します。- 作成したディレクトリに移動し(例:
cd my-flix-project)、java -jar flix.jar initを実行して空の Flix プロジェクトを作成します。java -jar flix.jar runを実行してプロジェクトをコンパイルし、実行します。
Nix で Flix をインストールする
Flix は nix パッケージマネージャを使用してインストールすることもできます。 現在実行中のシェルにインストールするには、以下を実行します:
$ nix-shell -p flix
または、グローバルにインストールするには:
$ nix-env -i flix
その後、プロジェクトディレクトリで flix run を実行します。
トラブルシューティング
Flix が動作しない最も一般的な原因は、(a) java コマンドが PATH に含まれていない、(b) JAVA_HOME 環境変数が設定されていないか、間違って設定されている、(c) 間違ったバージョンの Java がインストールされている、のいずれかです。これらの問題をデバッグするには、以下を確認してください:
- コマンド
java -versionが正しい Java バージョンを表示すること。 JAVA_HOME環境変数が正しく設定されていること。- Windows では、
echo %JAVA_HOME%と入力して変数を表示できます。 - Mac および Linux では、
echo $JAVA_HOMEと入力して変数を表示できます。
- Windows では、
まだ問題が解決しない場合は、Zulip でヘルプを求めることができます。
Hello World
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
それでは、src/Main.flix ファイルにある、かの有名な Hello World プログラムを見てみましょう:
def main(): Unit \ IO =
println("Hello World!")
これだけです!
他のプログラミング言語とは異なる点がいくつかあることにすぐ気づくでしょう:
main関数には仮引数がありません。特に、引数の配列を受け取りません。代わりに、コマンドライン引数はEnvエフェクトを通じて利用できます(main 関数を参照)。main関数の戻り値の型はUnitです。main関数はターミナルに出力するため、IOエフェクトを持ちます。
次のステップ
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
いよいよ、最初の本格的なプログラムを書く準備が整いました!
ここでは、UNIX の由緒あるワードカウント(wc)プログラムの簡単な変種を書いてみます。
この機会に、Flix で代数エフェクト(Algebraic effects)をどのように使うかを紹介します。
use Fs.FileRead
def wc(file: String): Unit \ { FileRead, IO } =
match FileRead.readLines(file) {
case Ok(lines) =>
let totalLines = List.length(lines);
let totalWords = List.sumWith(numberOfWords, lines);
println("Lines: ${totalLines}, Words: ${totalWords}")
case Err(_) =>
println("Unable to read file: ${file}")
}
def numberOfWords(s: String): Int32 =
s |> String.words |> List.length
def main(): Unit \ { FileRead, IO } =
wc("Main.flix")
このプログラムは次のように動作します。
まず、ファイル名を受け取り、FileRead エフェクトを使ってファイルからすべての行を読み込む wc 関数を定義します。
ファイルの読み込みに成功した場合は、次の値を計算します:
List.lengthを使った行数。- 各行に
numberOfWordsを適用した結果を合計して求めた単語数。
計算結果は println を使ってターミナルに出力されます。
ファイルを読み込めなかった場合は、代わりにエラーメッセージが出力されます。
wc 関数の型とエフェクトのシグネチャには、エフェクト集合 {FileRead, IO} が指定されており、これらのエフェクトが必要であることを示しています。FileRead と IO はどちらもデフォルトハンドラを持っているため、main の中で明示的にハンドラを呼び出す必要はありません。
データ型
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix には、ブール値、浮動小数点数、整数などの組み込みデータ型と、タプルやレコードなどの複合型が用意されています。
さらに、標準ライブラリでは Option[a]、Result[e, t]、List[a]、Set[a]、Map[k, v] などの型が定義されています。
これらの型に加えて、Flix ではプログラマが独自の型を定義できます。列挙型、再帰型、多相型などを定義することが可能です。
Flix は型エイリアス(新しい型)もサポートしています。
プリミティブ型
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は以下のプリミティブ型をサポートしています:
| 型 | 構文 | 説明 |
|---|---|---|
| Unit | () | Unit 値 |
| Bool | true, false | 真偽値 |
| Char | 'a', 'b', 'c' | 文字1値 |
| Float32 | 0.0f32, 21.42f32, -21.42f32 | 32ビット浮動小数点数 |
| Float64 | 0.0f64, 21.42f64, -21.42f64 | 64ビット浮動小数点数 |
| Int8 | 0i8, 1i8, -1i8, 127i8, -128i8 | 符号付き8ビット整数 |
| Int16 | 0i16, 123i16, -123i16 | 符号付き16ビット整数 |
| Int32 | 0i32, 123i32, -123i32 | 符号付き32ビット整数 |
| Int64 | 0i64, 123i64, -123i64 | 符号付き64ビット整数 |
| String | "hello", "world" | 文字列値 |
| BigInt | 0ii, 123ii, -123ii | 任意精度整数 |
| BigDecimal | 0.0ff, 123.45ff, -123.45ff | 任意精度小数 |
Float64 と Int32 の値はサフィックスなしで記述できます。
つまり、123.0f64 は単に 123.0 と、123i32 は 123 と書くことができます。
リテラル
Flix には List(リスト)、Set(セット)、Map(マップ)、正規表現のための組み込みの糖衣構文があります。
List リテラル
List リテラルは中置演算子 :: を使って記述します。例えば:
1 :: 2 :: 3 :: Nil
これは以下の糖衣構文です:
Cons(1, Cons(2, Cons(3, Nil)))
また、同じ List は以下のようにも書けます:
List#{1, 2, 3}
Set リテラル
Set リテラルは Set#{v1, v2, ...} という記法で記述します。例えば:
Set#{1, 2, 3}
これは以下の糖衣構文です:
Set.insert(3, Set.insert(2, Set.insert(1, Set.empty())))
要素は左から右へ挿入されるため、1 が最初に挿入されることに注意してください。
Map リテラル
Map リテラルは Map#{k1 => v1, k2 => v2, ...} という記法で記述します。
例えば:
Map#{1 => "Hello", 2 => "World"}
これは以下の糖衣構文です:
Map.insert(2, "World", Map.insert(1, "Hello", Map.empty()))
上記の Set と同様に、エントリは左から右へ挿入されることに注意してください。特に、複数のエントリが同じキーを持つ場合、最も右のエントリが以前の値を上書きします。
正規表現リテラル
正規表現リテラルは regex"..." という記法で記述します。例えば:
Regex.isMatch(regex"abc", "abc")
さらに、正規表現リテラルは regex"\\..." という記法で正規表現のエスケープシーケンスをサポートしています。例えば:
Regex.isMatch(regex"\\w", "W")
-
より正確には、
Char値は単一の UTF-16 コードユニットに対応します。UTF-16 は可変長エンコーディングです: 一部の Unicode コードポイントは単一の UTF-16 コードユニットで表現されますが、2つのコードユニットで表現されるものもあります。 (また、通常1つの文字として認識されるものを表現するために、複数の Unicode コードポイントの組み合わせが必要になる場合があることにも注意してください。) ↩
タプル
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Tuple(タプル)は値の積です。タプルは (exp1, ..., expn) という形式で記述します。
例えば、以下は Int32 と Bool からなる 2 要素タプル(ペア)です:
(123, true)
このタプルの型は (Int32, Bool) です。
タプルはパターンマッチングを使って分解できます。例えば:
let t = ("Lucky", "Luke", 42, true); // 4要素タプル
let (fstName, lstName, age, male) = t;
lstName
は文字列 "Luke" に評価されます。
Flix の Prelude では fst と snd 関数が定義されています:
let t = (1, 2);
let x = fst(t); // x = 1
let y = snd(t) // y = 2
これらは 2 要素タプル(つまりペア)を扱うときに便利です。例えば:
let l = (1, 1) :: (2, 2) :: Nil; // 型は List[(Int32, Int32)]
List.map(fst, l) // 型は List[Int32]
これは次のリストに評価されます:
1 :: 2 :: Nil
列挙型
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
列挙型 (Enumerated Types)
列挙型は、有限個(列挙可能)の値を持つ型を定義するために使われます。列挙型は、方角、トランプの札、曜日などをモデル化するのに便利です。
例えば、以下は曜日の列挙です:
enum Weekday {
case Monday,
case Tuesday,
case Wednesday,
case Thursday,
case Friday,
case Saturday,
case Sunday
}
ここで Monday、Tuesday などは enum のコンストラクタと呼ばれます。
曜日は Monday または Weekday.Monday のように参照できます。後者は、似た名前のコンストラクタを持つ複数の enum がスコープ内にある場合に必要になります。
パターンマッチングを使って enum 値を分解できます。例えば:
enum Animal {
case Cat,
case Dog,
case Giraffe
}
def isTall(a: Animal): Bool = match a {
case Animal.Cat => false
case Animal.Dog => false
case Animal.Giraffe => true
}
isTall 関数は Animal 型の値を受け取り、それに対してパターンマッチを行います。値が Giraffe の場合、関数は true を返します。そうでなければ false を返します。
Flix はパターンマッチが網羅的であること、つまりすべてのケースがカバーされていることを保証します。パターンマッチが非網羅的である場合はコンパイル時エラーになります。パターンマッチは、最後のケースとしてデフォルトケースを追加することで、常に網羅的にできます。デフォルトケースはアンダースコアを使って case _ => ??? と書きます。
再帰型 (Recursive Types)
再帰型は、自己参照的な型を定義するために使われます。
例えば、整数の二分木は以下のように定義できます:
enum Tree {
case Leaf(Int32),
case Node(Tree, Tree)
}
木は Int32 の値を持つ Leaf か、左右のサブツリーを持つ内部ノード Node のいずれかです。Tree の定義は自分自身を参照していることに注意してください。
パターンマッチングを使って、そのような木に含まれるすべての整数の合計を計算する関数を書くことができます:
def sum(t: Tree): Int32 = match t {
case Tree.Leaf(x) => x
case Tree.Node(l, r) => sum(l) + sum(r)
}
sum 関数は木の値に対してパターンマッチを行います。木が葉である場合、その値をそのまま返します。そうでなければ、関数は両方のサブツリーに対して自身を再帰的に呼び出し、その結果を加算します。
多相型 (Polymorphic Types)
多相型は、他の型によってパラメータ化された型です。例えば、以下のように書けます:
enum Bottle[a] {
case Empty,
case Full(a)
}
def isEmpty(b: Bottle[a]): Bool = match b {
case Bottle.Empty => true
case Bottle.Full(_) => false
}
ここで Bottle 型は型パラメータ a によってパラメータ化されています。Flix では、型パラメータは通常のパラメータと同様に、常に小文字で書きます。Bottle 型には2つのケースがあります。ボトルが空である(値を含まない)か、満たされている(型 a の値を1つ含む)かのいずれかです。isEmpty 関数は、型 a でパラメータ化されたボトルを受け取り、そのボトルが空かどうかを判定します。
注意深い読者は、Bottle がより広く知られている Option 型と等価であることに気づいたかもしれません。
一般に、多相型は複数の型引数を持つことができます。例えば、標準ライブラリの Result の実装には2つの型パラメータがあります:
enum Result[e, t] {
case Ok(t),
case Err(e)
}
省略形の Enum 構文
典型的な enum は以下のような形をしています:
enum Weekday {
case Monday,
case Tuesday,
case Wednesday,
case Thursday,
case Friday,
case Saturday,
case Sunday
}
同じ enum は以下のようにも宣言できます:
enum Weekday {
case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}
この省略形の構文は常に利用可能ですが、単純な enum にのみ使うべきです。
シングルトン Enum 構文
単一ケースの enum:
enum USD {
case USD(Int32)
}
は以下のように短縮できます:
enum USD(Int32)
型エイリアス
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
型エイリアス(Type alias)は、ある型に対する短縮名を導入します。例えば:
///
/// 型 `k` のキーから型 `Result[String, v]` の値への
/// Map に対する型エイリアス
///
type alias M[k, v] = Map[k, Result[String, v]]
def foo(): M[Bool, Int32] = Map#{true => Ok(123)}
型エイリアスは新しい別個の型を定義するわけではありません。型エイリアスは単に、(通常は複雑な)型に対する構文上の短縮形に過ぎません。
Flix コンパイラは型検査の前に型エイリアスを展開します。そのため、型エラーは常に、実際の基となる型に基づいて報告されます。
注意: 型エイリアスは自分自身を使って再帰的に定義することはできません。Flix コンパイラはそのような再帰的な循環を検出して報告します。
関数
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
関数と高階関数(Higher-order function)は、関数型プログラミング言語における重要な構成要素です。
Flix では、トップレベルの関数は def キーワードを使って定義します。例えば次のようになります:
def add(x: Int32, y: Int32): Int32 = x + y + 1
定義は、関数名に続いて引数リスト、戻り値の型、そして関数本体から構成されます。Flix は型推論をサポートしていますが、トップレベルの関数定義では引数の型と戻り値の型を宣言する必要があります。
Flix では、すべての関数の引数とローカル変数は使用されなければなりません。もし関数の引数が使用されない場合は、未使用であることを明示するためにアンダースコアを接頭辞として付ける必要があります。
高階関数
高階関数(Higher-order function) とは、それ自体が関数であるパラメータを受け取る関数のことです。例えば次のようになります:
def twice(f: Int32 -> Int32, x: Int32): Int32 = f(f(x))
ここで twice 関数は、関数 f と整数 x という 2 つの引数を受け取り、f を x に 2 回適用します。
twice 関数にはラムダ式(Lambda expression)を渡すことができます:
twice(x -> x + 1, 42)
これは 42 が 2 回インクリメントされるため、44 に評価されます。
また、2 つの引数を受け取る関数を必要とする高階関数を定義することもできます:
def twice(f: (Int32, Int32) -> Int32, x: Int32): Int32 =
f(f(x, x), f(x, x))
これは次のように呼び出すことができます:
twice((x, y) -> x + y, 42)
高階関数は、次のようにトップレベルの関数を渡して呼び出すこともできます:
def inc(x: Int32): Int32 = x + 1
def twice(f: Int32 -> Int32, x: Int32): Int32 = f(f(x))
twice(inc, 42)
関数型の構文
関数の引数の数に応じて、関数型の構文は異なります:
Unit -> Int32 // 引数を取らない関数の場合
Int32 -> Int32 // 1 引数の関数の場合
(Int32, Int32, ...) -> Int32 // それ以外の場合
関数合成
Flix は、関数合成(Function composition)とパイプライン処理のためのいくつかの演算子をサポートしています:
let f = x -> x + 1;
let g = x -> x * 2;
let h = f >> g; // x -> g(f(x)) と同等
ここで >> は前方関数合成(forward function composition)です。
また、パイプライン演算子(Pipeline operator)を使って関数適用を記述することもできます:
List.range(1, 100) |>
List.filter(x -> x `Int32.mod` 2 == 0) |>
List.map(x -> x * x) |>
println;
ここで x |> f は、関数適用 f(x) と同等です。
デフォルトでカリー化
関数はデフォルトでカリー化(curried)されています。カリー化された関数は、宣言している引数の数より少ない引数で呼び出すことができ、残りの引数を受け取る新しい関数を返します。例えば次のようになります:
def sum(x: Int32, y: Int32): Int32 = x + y
def main(): Unit \ IO =
let inc = sum(1);
inc(42) |> println
ここで sum 関数は x と y という 2 つの引数を受け取りますが、main 内では 1 つの引数だけで呼び出されています。この呼び出しは、sum と似た新しい関数を返しますが、この関数では x が常に 1 に束縛されている点が異なります。したがって、inc を 42 で呼び出すと 43 が返されます。
カリー化は、多くのプログラミングパターンで役立ちます。例えば、List.map 関数を考えてみましょう。この関数は、型 a -> b の関数と型 List[a] のリストという 2 つの引数を受け取り、その関数をリストのすべての要素に適用して得られる List[b] を返します。さて、カリー化とパイプライン演算子 |> を組み合わせると、次のように書くことができます:
def main(): Unit \ IO =
List.range(1, 100) |>
List.map(x -> x + 1) |>
println
ここで List.map の呼び出しには関数 x -> x + 1 が渡されており、これはリスト引数を期待する新しい関数を 返します。このリスト引数は、パイプライン演算子 |> によって供給されます。この場合、|> はリストと、リストを受け取る関数を期待しています。
パイプライン
Flix はパイプライン演算子 |> をサポートしています。これは単に関数適用の前置版です(つまり、引数が関数より前に現れます)。
パイプライン演算子は、関数型のコードをより読みやすくするためにしばしば使うことができます。例えば次のようになります:
let l = 1 :: 2 :: 3 :: Nil;
l |>
List.map(x -> x * 2) |>
List.filter(x -> x < 4) |>
List.count(x -> x > 1)
もう一つの例を示します:
"Hello World" |> String.toUpperCase |> println
演算子
Flix には、いくつかの組み込みの単項演算子と中置演算子(Infix operator)があります。さらに Flix では、関数名をバッククォートで囲むことで中置関数適用(infix function application)をサポートしています。例えば次のようになります:
123 `sum` 456
これは、通常の関数呼び出しと同等です:
sum(123, 456)
さらに、演算子名(+、-、*、<、>、=、!、&、|、^、$ の組み合わせ)で名付けられた関数も、中置記法で使うことができます。例えば次のようになります:
def <*>(x: Int32, y: Int32): Int32 = ???
これは次のように使うことができます:
1 <*> 2
イミュータブルなデータ
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
関数型プログラミングの_要_となるのが、イミュータブル(不変)なデータ型です。
これまでにも、イミュータブルなデータ型の例をいくつか見てきました。
加えて、Flix の標準ライブラリは次のようなイミュータブルなデータ型を提供しています。
List[t]: 型tの要素からなるイミュータブルな単方向リスト。Chain[t]: 型tの要素からなり、追加(append)が高速なイミュータブルなチェーン。Vector[t]: 型tの要素からなり、要素の参照(lookup)が高速なイミュータブルなシーケンス。Set[t]: 型tの要素からなるイミュータブルな Set(セット)。Map[k, v]: 型kのキーを型vの値に対応づけるイミュータブルな Map(マップ)。
その他のイミュータブルなデータ型には、次のものがあります。
Option[t]:NoneまたはSome(t)のいずれかになる型。Result[e, t]:Ok(t)またはErr(e)のいずれかになる型。Nel[t]: 型tの要素からなる、空でないイミュータブルな単方向リスト。Nec[t]: 型tの要素からなり、追加(append)が高速な、空でないイミュータブルなシーケンス。MultiMap[k, v]: 型kのキーを型vの値の_Set_に対応づけるイミュータブルな Map。
リスト
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
List(リスト)は、Nil と書かれる空のリストか、
もしくは x :: xs と書かれる cons cell(コンスセル)のいずれかです。
ここで x は先頭の要素、xs はリストの残り(末尾)を表します。
List 型は多相的なので、整数のリスト(List[Int32] と書きます)や、
文字列のリスト(List[String] と書きます)を作ることができます。
空のリストは次のように書きます。
Nil
"Hello" と "World" という文字列を要素に持つ文字列のリストは、
次のように構築できます。
"Hello" :: "World" :: Nil
あるいは、次の記法を使うこともできます。
List#{"Hello", "World"}
リストに対しては、さまざまな便利な操作を行えます。
たとえば、リストの長さは次のように計算できます。
List.length(1 :: 2 :: 3 :: Nil)
リストの要素の順序を反転させることもできます。
List.reverse(1 :: 2 :: 3 :: Nil)
2 つのリストは、List.append 関数を使って次のように連結できます。
let xs = (1 :: 2 :: 3 :: Nil);
let ys = (4 :: 5 :: 6 :: Nil);
List.append(xs, ys)
あるいは、組み込みの連結演算子 ::: を使って次のように書くこともできます。
let xs = (1 :: 2 :: 3 :: Nil);
let ys = (4 :: 5 :: 6 :: Nil);
xs ::: ys
Flix には、リストを操作するための関数が豊富に用意されています。
以下は、よく使われるものの一部です。
List.count(x -> x == 1, 1 :: 2 :: 3 :: Nil);
List.filter(x -> x == 1, 1 :: 2 :: 3 :: Nil);
List.map(x -> x + 1, 1 :: 2 :: 3 :: Nil);
List.foldLeft((x, y) -> x + y, 0, 1 :: 2 :: 3 :: Nil)
さらに、少し変わった関数もあります。
List.intersperse("X", "a" :: "b" :: "c" :: Nil)
これは、リストのすべての要素の間に "X" を挿入します。
let l1 = "X" :: "Y" :: Nil;
let l2 = ("a" :: "b" :: Nil) :: ("c" :: "d" :: Nil) :: Nil;
List.intercalate(l1, l2)
これは、リスト l2 のすべての要素の間に、リスト l1 を挿入します。
リストを操作する再帰関数を、自分で書くこともできます。
たとえば、map 関数の実装は次のようになります。
///
/// `l` のすべての要素に `f` を適用した結果を返します。
/// すなわち、結果は `f(x1) :: f(x2) :: ...` という形になります。
///
pub def map(f: a -> b \ ef, l: List[a]): List[b] \ ef = match l {
case Nil => Nil
case x :: xs => f(x) :: map(f, xs)
}
Chains and Vectors
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix はイミュータブルな List に加えて、イミュータブルな Chain と Vector もサポートしています。
次の表は、list、chain、vector のあいだの性能上のトレードオフを示しています:
| 操作 \ 型 | List | Chain | Vector |
|---|---|---|---|
| 先頭要素の取得 | O(1) | O(n) | O(1) |
| 末尾要素の取得 | O(n) | O(n) | O(1) |
| インデックス指定の取得 | O(n) | O(n) | O(1) |
| Cons | O(1) | O(n) | O(n) |
| Append | O(n + m) | O(1) | O(n + m) |
List、Chain、Vector のどれを使うべきでしょうか?:
Listデータ構造は、シンプルでよく知られているため、デフォルトの選択肢になります。Vectorデータ構造は、コレクションのサイズが固定されている場合や、高速なランダムアクセスが必要な場合に最適な選択肢です。Chainデータ構造はあまり使われませんが、高速な append が必要な場合に真価を発揮します。
Chains
Chain[t] は、要素のイミュータブルな連結シーケンスです。
Chain[t] データ型は次のように定義されています:
enum Chain[t] {
case Empty
case One(t)
case Chain(Chain[t], Chain[t])
}
このデータ構造が O(1) の append をサポートするのは、Chain コンストラクタ(より適切には Chain.append)を使って、既存の2つの chain から新しい chain を構築できるためです。
chain は Chain.empty、Chain.singleton、Chain.cons、Chain.append を使って構築できます。
たとえば、次のように書けます:
let c = Chain.cons(1, Chain.empty());
println(c)
これはコンパイルして実行すると Chain#{1} を出力します。
Vectors
Vector[t] は、型 t の連続した要素からなる、イミュータブルで固定長のシーケンスです。
Flix は Vector リテラルをサポートしています。たとえば、次のように書けます:
Vector#{1, 2, 3}
これは、要素 1、2、3 を持つ長さ 3 の vector を作成します。
vector は Vector.get 操作による高速なランダムアクセスをサポートしています:
let v = Vector#{1, 2, 3};
println(Vector.get(2, v))
これはコンパイルして実行すると 3 を出力します。
警告: vector の範囲を超えたインデックスでアクセスすると、プログラムはパニックします。
vector は多くの操作をサポートしています。たとえば、vector に対して関数をマップできます:
let v = Vector#{1, 2, 3};
Vector.map(x -> x + 1, v)
これは Vector#{2, 3, 4} に評価されます。
Set と Map
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は平衡木に基づく(イミュータブルな)Set(セット)と Map(マップ)を強力にサポートしています。そのため、Set の要素と Map のキーは Order トレイトを実装している必要があります。
ヒント: Flix の
SetおよびMapデータ構造は、特定の操作を自動的に並列化します。そのような操作には、API ドキュメント内で@ParallelWhenPureという印が付けられています。
Set
空の Set は次のように記述します。
Set#{}
これは Set.empty() と同等です。Set リテラルは次のように記述します。
Set#{1, 2, 3}
Set.insert を使って Set に要素を挿入することができます(この関数は新しい Set を返します)。
let s1 = Set#{1, 2, 3};
let s2 = Set.insert(4, s1);
Set.memberOf を使って、Set がある要素を含んでいるかどうかを判定することができます。
let s = Set#{1, 2, 3};
Set.memberOf(2, s)
Set.union を使って 2 つの Set を結合することができます。
let s1 = Set#{1, 2, 3};
let s2 = Set#{3, 4, 5};
let sr = Set.union(s1, s2);
Set は SemiGroup であるため、++ 演算子を使って s1 ++ s2 と書くこともできます。
Map
空の Map は次のように記述します。
Map#{}
これは Map.empty() と同等です。Map リテラルは次のように記述します。
Map#{"a" => 1, "b" => 2, "c" => 3}
Map.insert を使って Map に要素を挿入することができます(この関数は新しい Map を返します)。
let m1 = Map#{"a" => 1, "b" => 2, "c" => 3};
let m2 = Map.insert("d", 4, m1);
Map.get を使って、あるキーに関連付けられた値を取得することができます。
let m = Map#{"a" => 1, "b" => 2, "c" => 3};
Map.get("b", m)
Map.get 関数は Option[v] を返します。
Map.unionWith および Map.unionWithKey 関数のいずれかを使って 2 つの Map を結合することができます。
レコード
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、行多相(Row Polymorphism)で拡張可能なレコードをサポートしています。
Flix のレコードはイミュータブル(不変)です(ただし、内部にミュータブルな参照セルを持つことはできます)。
レコードリテラル
レコードリテラルは波括弧を使って記述します。
{ x = 1, y = 2 }
これは次のレコード型を持ちます。
{ x = Int32, y = Int32 }。
レコード内のラベルの順序は問いません。したがって上記のレコードは、次のものと等価です。
{ y = 2, x = 1 }
これは型 { y = Int32, x = Int32 } を持ちます。この型は { x = Int32, y = Int32 } と等価です。言い換えると、レコード型におけるラベルの順序は問わないということです。
ラベルアクセス
ハッシュ記号を使って、レコードのラベルにアクセスできます。
let p = { x = 1, y = 2 };
p#x + p#y
型システムによって、存在しないラベルにはアクセスできないことが保証されます。
レコードはイミュータブルです。いったん構築されると、レコードのラベルの値を変更することはできません。
ラベルの更新
レコードはイミュータブルですが、ラベルの値を更新した新しいレコードを構築することはできます。
let p1 = { x = 1, y = 2 };
let p2 = { x = 3 | p1 };
p1#x + p2#x
式 { x = 3 | p1 } は、レコード p1 の x ラベルを新しい値で更新します。ラベルの更新には、そのラベルがレコード上に存在していることが必要である点に注意してください。レコードは新しいラベルで更新することはできませんが、後ほど見るように、新しいラベルで拡張することはできます。
レコードの拡張
既存のレコードに新しいラベルを追加するには、次のようにします。
let p1 = { x = 1, y = 2 };
let p2 = { +z = 3 | p1 };
p1#x + p1#y + p2#z
ここで式 { +z = 3 | p1 } は、レコード p1 を新しいラベル z で拡張し、その結果は x、y、z の3つのラベルを持ちます。これらはすべて Int32 型です。
レコードの制限
レコードの拡張と同様に、レコードからラベルを取り除くこともできます。
let p1 = { x = 1, y = 2 };
let p2 = { -y | p1 };
ここでレコード p2 は、y ラベルが取り除かれている点を除いて、p1 と同じラベルを持ちます。
行多相
関数は、2つのラベルを持つレコードを要求すると指定できます。
def f(r: {x = Int32, y = Int32}): Int32 = r#x + r#y
この関数はレコード { x = 1, y = 2 } や { y = 2, x = 1 } で呼び出せますが、レコード { x = 1, y = 2, z = 3 } では呼び出せません。f のシグネチャは x と y というちょうど2つのラベルを持つレコードを要求するからです。このとき、レコード r は*閉じている(closed)*といいます。
行多相を使うことで、この制約を緩和できます。
def g(r: {x = Int32, y = Int32 | s}): Int32 = r#x + r#y
この関数は、Int32 型の x と y ラベルを持ってさえいれば、任意のレコードで呼び出せます。このとき、r のレコード型は*開いている(open)*といいます。
名前付きパラメータ
関数が同じ型の複数のパラメータを持つ場合、正しい引数の順序が分からなくなりがちです。例えば、String.contains("Hello","Hello World") は何を返すでしょうか? String.contains("Hello World", "Hello") は何を返すでしょうか?
この問題に対するよくある解決策が、*名前付きパラメータ(named parameters)*を使うことです。Flix はレコードを基盤とした名前付きパラメータの一形態をサポートしています。例えば、ある言語から別の言語へ翻訳する関数 translate を次のように書けます。
def translate(src: {src = Language}, dst: {dst = Language}, text: String): String = ???
この関数は次のように呼び出せます。
translate({src = English}, {dst = French}, "Where is the library?")
このような冗長な書き方は煩雑になるため、次の糖衣構文を使うこともできます。
translate(src = English, dst = French, "Where is the library?")
これは上記と等価です。
ミュータブルなデータ
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は 関数型ファースト(functional-first) なプログラミング言語であり、 immutable(イミュータブル)なデータ構造の使用を推奨しますが、必須とはしません。 immutable なデータ構造をデフォルトとすべきではありますが、Flix は mutable(ミュータブル)なデータへの破壊的更新(destructive update)を伴う 命令型プログラミングも手厚くサポートしています。
Flix は、その effect system(エフェクトシステム)を使って、純粋(pure)な コードと純粋でない(impure)コードを分離します。具体的には、Flix は region(リージョン)という概念を使って、mutable なメモリの使用を追跡します。 すなわち、すべての mutable なメモリは、静的にスコープの定まった何らかの region に属します。
Flix には、基本的な mutable メモリの型が 3 種類あります。
これらのデータ型を使って、より高水準な mutable データ構造を構築できます。
たとえば、Flix 標準ライブラリは MutList、MutDeque、MutSet、MutMap
といったコレクションを提供しています。原則として、こうした高水準なデータ構造は、
低水準な参照や配列よりも優先して使うべきです。
本章は、まず regions についての説明から始めます。
Regions
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は スコープ付き のミュータブルなメモリをサポートしています。Flix では、すべてのミュータブルなメモリは、そのレキシカルスコープに紐づく リージョン(region) に属します。実行がリージョンのレキシカルスコープを抜けると、そのリージョン内のすべてのメモリは到達不能になります。
リージョンが役立つのは、内部で ミューテーション(mutation) を使う 純粋関数 を実装できるようにするからです。この強力なアイデアを、いくつかの実例で説明していきますが、まずはリージョンの使い方から見ていきましょう。
新しいリージョンスコープは region 構文で導入します:
region rc { // リージョン開始。
... // リージョンハンドル `rc` がスコープ内にあります。
} // リージョンが終了し、`rc` に紐づくすべてのデータがスコープ外になります。
リージョンを使えば、内部でミューテーションを使いながらも純粋な sort 関数を実装できます:
def sort(l: List[a]): List[a] with Order[a] =
region rc {
let arr = List.toArray(rc, l);
Array.sort(arr);
Array.toList(arr)
}
ここでは rc という名前のリージョンを導入しています。List.toArray 関数を使って、リスト l をリージョン rc に紐づくミュータブルな配列 arr に変換します。次に、効率的なインプレースソートアルゴリズムを使う Array.sort で arr をソートします。最後に、ソート済みの配列をリストに戻して返します。sort 関数は内部でミューテーションを使っているにもかかわらず、純粋です。
別の例として、List[a] 向けの toString 関数を実装してみましょう。これは純粋ですが、内部ではミュータブルな StringBuilder を使っています:
def toString(l: List[a]): String with ToString[a] =
region rc {
let sb = StringBuilder.empty(rc);
List.forEach(x -> StringBuilder.appendString("${x} :: ", sb), l);
StringBuilder.appendString("Nil", sb);
StringBuilder.toString(sb)
} // rc のスコープが終了し、式全体は純粋です。
プログラミングのパターンは同じです。新しいリージョンを開き、リージョン内に StringBuilder を確保し、ビルダーを文字列で埋め、最後に1つの文字列に変換します。
リージョンを使うと、特定の 関数型の操作 をより効率的に実装できます。たとえば、次は List.flatMap の高速な実装です:
def flatMap(f: a -> List[b] \ ef, l: List[a]): List[b] \ ef =
region rc {
let ml = MutList.empty(rc);
l |> List.forEach(x -> MutList.append(f(x), ml));
MutList.toList(ml)
}
リージョンは値である
リージョン(またはリージョンハンドル)は、関数の引数として渡せる 値 です。これは、たとえばミュータブルなデータ構造を確保して返す再利用可能な関数を書きたいときに便利です。
たとえば、次は List.toMutDeque 関数です:
def toMutDeque(rc: Region[r], l: List[a]): MutDeque[a, r] \ r =
let d = MutDeque.empty(rc);
foreach (x <- l) {
MutDeque.pushBack(x, d)
};
d
この関数はリージョンハンドル rc を受け取り、与えられたリージョン内に新しいミュータブルな両端キュー(deque、MutDeque)を確保し、リスト l のすべての要素を deque に挿入して、それを返します。
リージョンはスコープを持つ
リージョンと、それに紐づくすべてのメモリは、そのレキシカルスコープより長く生存することはできません。
次のプログラムを考えてみましょう:
def main(): Unit \ IO =
let escaped = region rc {
Array#{1, 2, 3} @ rc
};
println(escaped)
ここでは Array#{1, 2, 3} をリージョン rc 内に確保し、それを囲んでいるスコープの外へ返そうとしています。Flix コンパイラはこのようなエスケープ違反を検出し、エラーを報告します:
❌ -- Type Error ----------------------------
>> The region variable 'rc' escapes its scope.
2 |> let escaped = region rc {
3 |> Array#{1, 2, 3} @ rc
4 |> };
region variable escapes.
References
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix はミュータブル(mutable)でスコープ付きの参照(reference)をサポートしています。参照とは、その値が時間とともに変化しうるボックスです。参照に対する 3 つの主要な操作は次のとおりです。
- 新しい参照を作成する
Ref.fresh(rc, e)。 - 参照をデリファレンス(dereference)する
Ref.get(e)。 - 参照に代入する
Ref.put(e, e)。
Flix では、参照の型は Ref[t, r] であり、t は要素の型、r はそのリージョンです。Flix のすべてのミュータブルなメモリと同様に、すべての参照は何らかのリージョンに属していなければなりません。参照に対する読み取りと書き込みは、エフェクトを伴う(effectful)操作です。たとえば、参照 Ref[t, r] の値を読み取ると、エフェクト r を伴います。
Ref.fresh(rc, e) 操作はヒープのリージョン内に参照セルを割り当て、その位置を返します。Ref.get 操作はある位置をデリファレンスし、参照セルの内容を返します。そして代入操作 Ref.put は参照セルの値を変更します。直感的には、参照セルは、値を変更できる単一のフィールドを持つ「オブジェクト」と考えることができます。
参照の割り当て
参照セルは Ref.fresh(rc, e) 関数で割り当てます。たとえば次のようになります。
region rc {
let c = Ref.fresh(rc, 42);
println(Ref.get(c))
}
ここでは rc という名前のリージョンを導入しています。このリージョンの内部で、値 42 を持つ c という参照セルを作成し、それをデリファレンスして出力しています。
参照のデリファレンス
参照セルには Ref.get 関数でアクセスします(デリファレンスします)。たとえば次のようになります。
region rc {
let c = Ref.fresh(rc, 42);
let x = Ref.get(c);
let y = Ref.get(c);
println(x + y)
}
ここでは、このプログラムは 42 + 42 = 84 を出力します。
代入
参照セルの値を更新することができます。たとえば次のようになります。
region rc {
let c = Ref.fresh(rc, 0);
Ref.put(Ref.get(c) + 1, c);
Ref.put(Ref.get(c) + 1, c);
Ref.put(Ref.get(c) + 1, c);
println(Ref.get(c))
}
ここでは、このプログラムは値 0 を持つ参照セル c を作成します。そしてそのセルをデリファレンスし、その値を 3 回インクリメントします。したがって、このプログラムは 3 を出力します。
例:シンプルなカウンター
参照を使って、シンプルなカウンターを実装することができます。
enum Counter[r: Region] { // ここでの Region は型カインド(type-kind)です
case Counter(Ref[Int32, r])
}
def newCounter(rc: Region[r]): Counter[r] \ r = Counter.Counter(Ref.fresh(rc, 0))
def getCount(c: Counter[r]): Int32 \ r =
let Counter.Counter(l) = c;
Ref.get(l)
def increment(c: Counter[r]): Unit \ r =
let Counter.Counter(l) = c;
Ref.put(Ref.get(l) + 1, l)
def main(): Unit \ IO =
region rc {
let c = newCounter(rc);
increment(c);
increment(c);
increment(c);
getCount(c) |> println
}
ここでは、Counter データ型がリージョンの型パラメータを持っています。これは、カウンターが内部でリージョンを必要とする参照を使用しているため必須です。したがって、Counter もスコープ付きとなります。newCounter 関数が新しい Counter を作成するためにリージョンハンドルを必要とすることに注意してください。さらに、getCount 関数と increment 関数がともに r エフェクトを持つことに注意してください。
エイリアシングと参照への参照
エイリアシングこそが参照の目的なので、参照は自然にこれをサポートします。たとえば次のようになります。
region rc {
let l1 = Ref.fresh(rc, 42);
let l2 = l1;
Ref.put(84, l2);
println(Ref.get(l1))
}
これは 84 を出力します。なぜなら、l1 が指している参照セルが、エイリアス l2 を通じて変更されるためです。
参照は、次の例が示すように、参照を指すこともできます。
region rc {
let l1 = Ref.fresh(rc, 42);
let l2 = Ref.fresh(rc, l1);
let rs = Ref.get(Ref.get(l2));
println(rs)
}
ここでは、l2 の型は Ref[Ref[Int32, rc], rc] です。
ミュータブルなタプルとレコード
Flix のタプルとレコードはイミュータブル(不変)です。しかし、タプルとレコードはミュータブルな参照を含むことができます。
たとえば、次のものは 2 つのミュータブルな参照を含むペアです。
region rc {
let p = (Ref.fresh(rc, 1), Ref.fresh(rc, 2));
Ref.put(123, fst(p))
};
このペアの型は (Ref[Int32, rc], Ref[Int32, rc]) です。この代入はペアそのものを変更するのではなく、第 1 要素にある参照セルの値を変更します。
同様に、次のものは 2 つのミュータブルな参照を含むレコードです。
region rc {
let r = { fstName = Ref.fresh(rc, "Lucky"), lstName = Ref.fresh(rc, "Luke") };
Ref.put("Unlucky", r#fstName)
};
このレコードの型は { fstName = Ref[String, rc], lstName = Ref[String, rc] } です。ここでも、この代入はレコードそのものを変更するのではなく、fstName ラベルに対応する参照セルの値を変更します。
配列
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、ミュータブル(可変)な スコープ付き 配列(Array)をサポートしています。配列とは、同じ型を共有する固定長でミュータブルな要素の列です。配列はメモリ上に連続して配置されます。配列はミュータブルであるため、その要素は時間とともに変化し得ます。ただし、いったん作成すると、配列の長さは変更できません。
Flix では、配列の型は Array[t, r] です。ここで t は要素の型、r はそのリージョン(region)です。Flix のすべての可変メモリと同様に、すべての配列はいずれかのリージョンに属さなければなりません。配列の読み取りと書き込みは エフェクトを持つ 操作です。たとえば、型 Array[t, r] の配列から要素を読み取る操作はエフェクト r を持ちます。同様に、リージョン内で配列を作成する操作もエフェクトを持つ操作です。
配列は 常に アンボックス化(unboxed)されています。たとえば、型 Array[Int32, r] の配列は、プリミティブな32ビット整数の列として表現されます。すなわち JVM の用語で言えば、その配列は int[] として表現されます。Flix はプリミティブな整数を java.lang.Integer オブジェクトとしてボックス化することは決してありませんが、それでもジェネリックなコレクションや関数の中でプリミティブを使うことは許可しています。これは他の種類のプリミティブやプリミティブの配列についても同様です。
配列は低レベルのデータ構造であり、通常はより高レベルなデータ構造を実装するために使われます。したがって、そのようなデータ構造を実装する場合を除いては、配列の使用は控えめにすることをおすすめします。代わりに、MutList、MutDeque、MutSet、MutMap といったデータ構造を使うことをおすすめします。
ヒント: 要素の 伸長可能(growable) な可変列が必要な場合は、
MutListを使ってください。
配列リテラル
配列リテラルの構文は Array#{e1, e2, e3, ...} @ r の形をとります。ここで e1、e2 などは 要素式 であり、r は リージョン式 です。たとえば次のようになります。
region rc {
let fruits = Array#{"Apple", "Pear", "Mango"} @ rc;
println(Array.toString(fruits))
}
ここでは rc という名前のリージョンを導入しています。このリージョン内で、"Apple"、"Pear"、"Mango" という3つの文字列を含む配列 fruits を作成しています。fruits の型は Array[String, rc] です。リージョンの詳細については、リージョンの章を参照してください。
このプログラムを実行すると Array#{"Apple", "Pear", "Mango"} が出力されます。
配列の確保
Array.repeat 関数を使うと、同じ要素で満たされたサイズ n の配列を確保できます。たとえば次のようになります。
region rc {
let arr = Array.repeat(rc, 1_000, 42);
println(Array.toString(arr))
}
ここでは、各要素が値 42 を持つ、長さ 1_000 の配列 arr を作成しています。Array.repeat にはリージョン rc を引数として渡す必要がある点に注意してください。返される配列がどのリージョンに属するべきかを、この関数が知る必要があるからです。
ゼロから99までのすべての整数で満たされた配列を作成することもできます。
region rc {
let arr = Array.range(rc, 0, 100);
println(Array.toString(arr))
}
さらに、ほとんどのデータ構造は配列に変換できます。たとえば次のようになります。
region rc {
let fruitList = List#{"Apple", "Pear", "Mango"};
let fruitArray = List.toArray(rc, fruitList);
}
List.toArray にはリージョン rc を引数として渡す必要がある点に注意してください。返される配列がどのリージョンに属するべきかを、この関数が知る必要があるからです。
未初期化の要素を持つ配列の確保
Array.empty 関数を使うと、内容が未初期化の、指定した長さの配列を作成できます。たとえば次のようになります。
region rc {
let arr: Array[String, rc] = Array.empty(rc, 100);
// ... ここで `arr` を初期化します ...
}
ここでは、型 Array[String, rc] の長さ 100 の配列を作成しています。配列の期待される型を Flix に伝えるために、明示的な型注釈 : Array[String, rc] を使っています。
警告: 未初期化の要素を持つ配列を使うのは危険です。
未初期化の配列の要素は何になるのでしょうか? Flix は Java(および JVM)に従い、すべてのプリミティブ型および参照型に対して デフォルト値 を定義しています。たとえば Bool と Int32 のデフォルト値は、それぞれ false と 0 です。参照型のデフォルト値は null です。ですから注意してください! Flix には null 値はありませんが、適切に初期化されていない配列を読み取ることで間接的に null が持ち込まれてしまい、NullPointerException を引き起こす可能性があります。
配列の読み取りと書き込み
Array.get と Array.put を使うと、配列の特定の位置にある要素をそれぞれ取得・更新できます。たとえば次のようになります。
region rc {
let strings = Array.empty(rc, 2);
Array.put("Hello", 0, strings);
Array.put("World", 1, strings);
let s1 = Array.get(0, strings);
let s2 = Array.get(1, strings);
println("${s1} ${s2}")
}
ここでは長さ2の空の配列を作成しています。次に、位置ゼロに文字列 "Hello" を、位置1に文字列 "World" を格納します。続いて、その2つの文字列を取得して出力します。したがって、このプログラムをコンパイルして実行すると Hello World が出力されます。
!> パイプライン演算子を使うと、プログラムの一部をより 流れるようなスタイル(fluent-style) で書くこともできます。
let strings =
Array.empty(rc, 2) !>
Array.put("Hello", 0) !>
Array.put("World", 1);
配列のスライス
Array.slice を使って配列をスライスできます。配列のスライスとは、元の配列の部分範囲を新しく(浅く)コピーしたものです。たとえば次のようになります。
region rc {
let fruits = Array#{"Apple", "Pear", "Mango"} @ rc;
let result = Array.slice(rc, start = 1, end = 2, fruits);
println(Array.toString(result))
}
これは実行すると Array#{"Pear"} を出力します。
配列の長さの取得
Array.length 関数を使って配列の長さを計算できます。たとえば次のようになります。
region rc {
let fruits = Array#{"Apple", "Pear", "Mango"} @ rc;
println(Array.length(fruits))
}
これは実行すると 3 を出力します。
注意: 配列をインデックスベースで反復処理することはおすすめしません。代わりに、
Array.count、Array.forEach、Array.transformといった関数を使うことをおすすめします。
その他の配列操作
Array モジュールは、配列を扱うための関数を豊富に取りそろえています。いくつか挙げると、たとえば Array.append、Array.copyOfRange、Array.findLeft、Array.findRight、Array.sortWith、Array.sortBy などがあります。このモジュールは合計で100を超える関数をすぐに使える形で提供しています。
構造体
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、ミュータブル(mutable)で スコープ付き(scoped) な構造体(struct)を
サポートしています。構造体は、ユーザーが定義したフィールドの並びです。
フィールドはデフォルトではイミュータブル(immutable)ですが、mut 修飾子を
付けることでミュータブルにできます。Flix のすべてのミュータブルなメモリと同様に、
すべての構造体は何らかのリージョン(region)に属していなければなりません。
構造体は、イミュータブルな拡張可能レコード(extensible record)に対する、 ミュータブルな代替手段です。
構造体のフィールドは非ボックス化(unboxed)されています。すなわち、プリミティブ型が 間接参照(indirection)を引き起こしません。そのため構造体はメモリ効率の良い データ構造であり、より高水準なミュータブルなデータ構造(たとえばミュータブルなリスト、 スタック、キューなど)を実装するために利用できます。
Flix は、構造体を扱うために 3 つの操作をサポートしています。
new Struct @ rc { ... }による、リージョン内での構造体インスタンスの生成。struct->fieldによる、構造体のフィールドへのアクセス。struct->field = ...による、ミュータブル なフィールドの更新。
それぞれの操作は、その構造体が属するリージョンにおいてエフェクトを持ちます。
構造体の宣言
構造体は、同じ名前のモジュール——そのコンパニオン——の 内部で宣言します。たとえば:
mod Person {
pub struct Person[r] {
name: String,
mut age: Int32,
mut height: Int32
}
}
ここでは、name、age、height という 3 つのフィールドを持つ構造体を
宣言しています。name フィールドはイミュータブルであり、構造体インスタンスが
生成された後に変更することはできません。age と height のフィールドは
ミュータブルなので、生成後に変更できます。Person 構造体は、r という 1 つの型パラメータを持ち、
これは構造体が属するリージョンを指定します。
すべての構造体はリージョンの型パラメータを持たなければならず、それは型パラメータの リストの最後になければなりません。
構造体の生成
Person 構造体のインスタンスは、次のように生成できます。
mod Person {
pub def mkLuckyLuke(rc: Region[r]): Person[r] \ r =
new Person @ rc { name = "Lucky Luke", age = 30, height = 185 }
}
mkLuckyLuke 関数は、構造体に関連付けるリージョンケイパビリティ(capability)
rc という 1 つの引数を取ります。
次の構文:
new Person @ rc { name = "Lucky Luke", age = 30, height = 185 }
は、構造体 Person の新しいインスタンスをリージョン rc 内に生成することを
指定します。続いて、構造体の各フィールドの値を指定します。構造体のすべての
フィールドは、即座にかつ明示的に初期化しなければなりません。
フィールドの読み書き
構造体のフィールドは、フィールドアクセス演算子 -> を使って読み書きできます。
たとえば:
mod Person {
pub def birthday(p: Person[r]): Unit \ r =
p->age = p->age + 1;
if(p->age < 18) {
p->height = p->height + 10
} else {
()
}
}
birthday 関数は Person 構造体 p を受け取り、その age フィールドと
height フィールドを変更(mutate)します。
たとえば、次の行:
p->age = p->age + 1;
では、p->age で現在の age を取得し、それをインクリメントして、その結果を
age フィールドに書き戻しています。
構造体のフィールドアクセス演算子 -> と、関数の矢印
-> は区別しなければなりません。前者は前後に
スペースがなく、後者は両側にスペースを置きます。まとめると:
s->f:構造体sのフィールドfへのフィールドアクセスです。x -> x:仮引数xから変数式xへの関数です。
フィールドの可視性
Flix では、構造体のフィールドはそのコンパニオンモジュール(companion module)の 内部からのみ可視です。これは、コンパイラによって強制されるカプセル化 (encapsulation)の一形態と考えることができます。
たとえば、次のように書くと:
mod Point {
pub struct Point[r] {
x: Int32,
y: Int32
}
}
def area(p: Point[r]): Int32 \ r =
p->x * p->y
Flix コンパイラは 2 つのエラーを出力します。
❌ -- Resolution Error --------------------------------------------------
>> Undefined struct field 'x'.
7 | p->x * p->y
^
undefined field
❌ -- Resolution Error --------------------------------------------------
>> Undefined struct field 'y'.
7 | p->x * p->y
^
undefined field
代わりに、area 関数はコンパニオンモジュールの 内部 で定義すべきです。
mod Point {
pub struct Point[r] {
x: Int32,
y: Int32
}
pub def area(p: Point[r]): Int32 \ r =
p->x * p->y
}
構造体のフィールドへのアクセスをコンパニオンモジュールの外部から提供したい 場合は、明示的なゲッター(getter)とセッター(setter)を導入できます。 たとえば:
mod Point {
pub def getX(p: Point[r]): Int32 \ r = p->x
pub def getY(p: Point[r]): Int32 \ r = p->y
}
このように、構造体のフィールドへのアクセスは厳密に制御されます。
イミュータブルなフィールドとミュータブルなフィールド
Flix では、構造体のすべてのフィールドはイミュータブルかミュータブルのいずれかです。
ミュータブルなフィールドには mut 修飾子を付けなければなりません。そうでない場合、
フィールドはデフォルトでイミュータブルとなり、すなわち構造体インスタンスが生成された
後はそのフィールドの値を変更できません。
たとえば、ユーザーを表す構造体を次のように定義できます。
mod User {
pub struct User[r] {
id: Int32,
mut name: String,
mut email: String
}
}
ここで識別子 id はイミュータブルであり変更できませんが、name と email の
フィールドは構造体インスタンスの生存期間を通じて変更できます。
イミュータブルなフィールドを変更しようとすると:
mod User {
pub def changeId(u: User[r]): Unit \ r =
u->id = 0
}
Flix コンパイラはエラーを出力します。
❌ -- Resolution Error --------------------------------------------------
>> Modification of immutable field 'id' on User'.
9 | u->id = 0
^^
immutable field
Mark the field as 'mut' in the declaration of the struct.
フィールドの immutability(不変性)は 推移的ではない ことに注意してください。
たとえば、次の構造体を定義できます。
mod Book {
pub struct Book[r] {
title: String,
authors: MutList[String, r]
}
}
ここで authors フィールドはイミュータブルです。
しかし、MutList は変更できるので、次のように書けます。
mod Book {
pub def addAuthor(a: String, b: Book[r]): Unit \ r =
MutList.push(a, b->authors)
}
ここでは、構造体のフィールドを変更しているのではありません。その下にある ミュータブルなリストを変更しているのです。
再帰的な構造体と多相的な構造体
再帰的(recursive)かつ多相的(polymorphic)な、二分探索木のための構造体を 定義できます。
mod Tree {
pub struct Tree[k, v, r] {
key: k,
mut value: v,
mut left: Option[Tree[k, v, r]],
mut right: Option[Tree[k, v, r]]
}
}
Tree[k, v, r] がソート済みであると仮定すると、search 関数を次のように
定義できます。
mod Tree {
// 木 `t` から指定されたキー `k` を探索する関数。
pub def search(k: k, t: Tree[k, v, r]): Option[v] \ r with Order[k] =
match (Order.compare(k, t->key)) {
case Comparison.EqualTo => Some(t->value)
case Comparison.LessThan =>
// 左の部分木を探索します。
match t->left {
case None => None
case Some(leftTree) => search(k, leftTree)
}
case Comparison.GreaterThan =>
// 右の部分木を探索します。
match t->right {
case None => None
case Some(rightTree) => search(k, rightTree)
}
}
}
ミュータブルなコレクション
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix の標準ライブラリは、option、list、chain、set、map をはじめとする多くのイミュータブルなコレクションをサポートしています。これらの利用を強くおすすめします。
加えて、Flix の標準ライブラリはいくつかのミュータブルなコレクションも提供しています:
MutList[t, r]: 型tの要素からなる、伸縮可能で連続した配列。MutSet[t, r]: 型tの要素からなるミュータブルな set。MutMap[k, v, r]: 型kのキーを型vの値に対応づけるミュータブルな map。MutDeque[t, r]: 型tの要素からなるミュータブルな両端キュー(double-ended queue)。
Flix では、ミュータブルなコレクションを含むすべてのミュータブルなメモリがリージョンに属することを思い出してください。
次は MutList[t] の使い方の例です:
def main(): Unit \ IO =
region rc {
let fruits = MutList.empty(rc);
MutList.push("Apple", fruits);
MutList.push("Pear", fruits);
MutList.push("Mango", fruits);
MutList.forEach(println, fruits)
}
これは Apple、Pear、Mango を出力します。ここで MutList[String, rc] は、要素が push(または pop)されるたびに自動的に拡張(または縮小)します。
上記のプログラムは、!> パイプライン演算子を使って、より 流れるようなスタイル(fluent-style) で書くこともできます:
def main(): Unit \ IO =
region rc {
let fruits =
MutList.empty(rc) !>
MutList.push("Apple") !>
MutList.push("Pear") !>
MutList.push("Mango");
MutList.forEach(println, fruits)
}
上記のプログラムは、次のように複数の関数に分割できます:
def main(): Unit \ IO =
region rc {
let fruits = sweetFruits(rc);
printFruits(fruits)
}
def sweetFruits(rc: Region[r]): MutList[String, r] \ r =
MutList.empty(rc) !>
MutList.push("Apple") !>
MutList.push("Pear") !>
MutList.push("Mango")
def printFruits(fruits: MutList[String, r]): Unit \ {r, IO} =
MutList.forEach(println, fruits)
ここで main 関数は新しいリージョン rc を導入します。このリージョンを sweetFruits に渡し、sweetFruits は新しいフルーツのミュータブルなリストを作成して返します。sweetFruits は rc を使ってミュータブルなメモリを確保するため、エフェクト r を持つことに注意してください。printFruits はフルーツのミュータブルなリストを受け取り、それらを出力します。この関数は r 内のミュータブルなメモリから読み取るためエフェクト r を持ち、また端末に出力するためエフェクト IO を持つことに注意してください。
制御構造
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は関数型プログラミング言語であるため、制御構造は多くありません。 制御のほとんどは、単なる関数適用によって行われます。Flix の制御構造は次のとおりです。
- If-Then-Else: 従来の if-then-else 式です。
- Pattern Matching: 代数的データ型(algebraic data type)を分解するための関数型の構文です。
- Foreach: コレクションを反復処理するための命令型の構文です。
- Monadic For-Yield: モナドの操作のための関数型の構文で、Scala の
for内包表記や Haskell のdo記法に似ています。 - Applicative For-Yield: アプリカティブな操作のための関数型の構文で、Haskell のアプリカティブな
do記法に似ています。
foreach、モナドの forM、アプリカティブの forA の違いは何でしょうか。
次の表は、各構文の使用例を示しています。
| アクション | 構文 |
|---|---|
| コレクション内のすべての要素を出力する。 | Foreach |
| コレクション内の各要素にエフェクトを伴う操作を適用する。 | Foreach |
Option や Result を扱う。 | Monadic For-Yield |
Monad を介して flatMap する。 | Monadic For-Yield |
Validation を扱う。 | Applicative For-Yield |
注意: Flix には従来の
whileループやforループはありません。代わりに、再帰や上記の構文のいずれかを使用することを推奨します。
If-then-else
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、おなじみの if-then-else 式をサポートしています。
if (1 == 1) "Hello" else "World"
これは Hello に評価されます。
加えて、if ガードは言語の他の部分でもサポートされています。
ガード付きパターンマッチ
パターンマッチの中で if ガードを使えます。
def isSquare(s: Shape): Bool = match s {
case Rectangle(h, w) if h == w => true
case _ => false
}
ガード付き Datalog ルール
Datalog ルールの中で if ガードを使えます。
Path(x, z) :- Path(x, y), Edge(y, z), if (x != z).
ガードを囲む括弧は必須である点に注意してください。
パターンマッチング
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Enum に対するマッチング
Flix は、代数的データ型(algebraic data type)に対するパターンマッチングを サポートしています。
たとえば、図形をモデル化する代数的データ型があるとします。
enum Shape {
case Circle(Int32)
case Square(Int32)
case Rectangle(Int32, Int32)
}
このとき、パターンマッチングを使って Shape の面積を計算する関数を、
次のように書けます。
def area(s: Shape): Int32 = match s {
case Shape.Circle(r) => 3 * (r * r)
case Shape.Square(w) => w * w
case Shape.Rectangle(h, w) => h * w
}
レコードに対するマッチング
上記はレコード型に対しても機能しますが、構文が少し異なります。先ほどの
Shape 型を、今度はレコードを使って書き換えてみましょう。
enum Shape {
case Circle({ radius = Int32 })
case Square({ width = Int32 })
case Rectangle({ height = Int32, width = Int32 })
}
def area(s: Shape): Int32 = match s {
case Shape.Circle({ radius }) => 3 * (radius * radius)
case Shape.Square({ width }) => width * width
case Shape.Rectangle({ height, width }) => height * width
}
上の例では、各パターンが指定されたラベルをちょうど持つことを、暗黙に 要求しています。多すぎても少なすぎてもいけません。 しかし一般に、レコードパターンの構文はその型の構文と似ています。 そのため、少なくとも特定のラベルを 1 つ持つレコードにマッチさせることができます。
def f(r: { height = Int32 | a }): Int32 = match r {
case { height | _ } => height
// 拡張部分は使われないのでワイルドカードパターンになっています
}
ただし、パターンは型も規定する(imply)ことに注意してください。そのため、 次の例は動作しません。
def badTypes(r: { height = Int32 | a }): Int32 = match r {
case { height } => height
}
さらに、すべての case は同じ型でなければならないため、これも動作しません。
match ??? {
case { height | _ } => height
case { height } => height
}
これはやや不自然な(こじつけのような)例かもしれませんが、よくある落とし穴を示しており、 簡単に修正できます。
これは、最初の case が height ラベルを持つ多相的なレコード(polymorphic
record)であるのに対し、2 番目の case が height ラベル のみ が定義された
閉じたレコード(closed record)にマッチするためです。
さらに、{ label } パターンは実際には { label = pattern } の
糖衣構文(syntactic sugar)です。そのため、複数のレコードを扱う場合は、
異なるパターンを使う必要があるかもしれません。
def shadowing(r1: { height = Int32 | a }, r2: { height = Int32 | b }): Int32 =
match (r1, r2) {
case ({ height | _ }, { height | _ }) => height + height
// `height = height` が 2 回定義されるため、これは動作しません
}
しかし、変数の名前を変えれば、プログラムは型検査を通ります。
def renaming(r1: { height = Int32 | a }, r2: { height = Int32 | b }): Int32 =
match (r1, r2) {
case ({ height = h1 | _ }, { height = h2 | _ }) => h1 + h2
}
まとめると、レコードパターンの例をいくつか挙げます。
{ }- 空のレコード{ radius = r }-radiusラベルだけを持つレコード。その値はスコープ内でrに束縛されます{ radius }-radiusラベルだけを持つレコード(これは実際には{ radius = radius }の糖衣構文です){ radius | _ }- 少なくともradiusラベルを持つレコード{ radius | r }- 少なくともradiusラベルを持つレコード。レコードの残りの部分はrに束縛されます
let パターンマッチ
パターン match 構文に加えて、let 束縛(let-binding)を使って値を分解する
こともできます。たとえば:
let (x, y, z) = (1, 2, 3)
これは変数 x、y、z を、それぞれ値 1、2、3 に束縛します。
網羅的(exhaustive)なパターンであれば、let 束縛の中で使えます。たとえば:
let (x, Foo(y, z)) = (1, Foo(2, 3))
これは、Foo がその型における唯一のコンストラクタである場合に限り、
正当です。
次の let 束縛は網羅的でないため 不正 です。
let (1, 2, z) = ...
let Some(x) = ...
Flix コンパイラは、このような網羅的でないパターンを拒否します。
let パターンマッチはレコードと相性が良く、レコードを分解して、関心のある ラベルだけを使うことができます。
let { height | _ } = r;
height + height
マッチラムダ
パターンマッチは、ラムダ式とともに使うこともできます。たとえば:
List.map(match (x, y) -> x + y, (1, 1) :: (2, 2) :: Nil)
は次と等価です。
List.map(w -> match w { case (x, y) => x + y }, (1, 1) :: (2, 2) :: Nil)
let 束縛の場合と同様に、このようなパターンマッチも網羅的でなければなりません。
2 つのラムダ式の違いに注意してください。
let f = (x, y, z) -> x + y + z + 42i32
let g = match (x, y, z) -> x + y + z + 42i32
ここで f は 3 つ の Int32 引数を取る関数であるのに対し、g は 1 つ の
3 要素タプル (Int32, Int32, Int32) を引数に取る関数です。
Foreach
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、コレクションを命令的に反復するための、伝統的な foreach 構文をサポートしています。
foreach 構文は通常、1つ以上のコレクションを反復し、それぞれの要素に対してエフェクトを伴う操作を実行したいときに使います。
たとえば、次のプログラム:
def main(): Unit \ IO =
let fruits = List#{"Apple", "Pear", "Mango"};
foreach (fruit <- fruits)
println(fruit)
は、文字列 Apple、Pear、Mango を出力します。
複数のコレクションを反復することもできます:
def main(): Unit \ IO =
let fruits = List#{"Apple", "Pear", "Mango"};
let creams = List#{"Vanilla", "Stracciatella"};
foreach (fruit <- fruits)
foreach (cream <- creams)
println("Would you like some ${fruit} with ${cream} icecream?")
同じループは、次のようにも書けます:
def main(): Unit \ IO =
let fruits = List#{"Apple", "Pear", "Mango"};
let creams = List#{"Vanilla", "Stracciatella"};
foreach (fruit <- fruits; cream <- creams)
println("Would you like some ${fruit} with ${cream} icecream?")
フィルタ付きのループを書くこともできます。たとえば:
def main(): Unit \ IO =
let fruits = List#{"Apple", "Pear", "Mango"};
let creams = List#{"Vanilla", "Stracciatella"};
foreach (fruit <- fruits; if isExcotic(fruit); cream <- creams)
println("Would you like some ${fruit} with ${cream} icecream?")
def isExcotic(fruit: String): Bool = match fruit {
case "Mango" => true
case _ => false
}
見やすさのための波括弧の追加(省略可能)
foreach 式は、波括弧を追加することで見やすさを向上できる場合があります:
def main(): Unit \ IO =
let fruits = List#{"Apple", "Pear", "Mango"};
let creams = List#{"Vanilla", "Stracciatella"};
foreach (fruit <- fruits) {
foreach (cream <- creams) {
println("Would you like some ${fruit} with ${cream} icecream?")
}
}
波括弧は foreach ループの意味には一切影響しません。純粋にスタイル上のものです。
ForEach トレイト
foreach 構文は、ForEach トレイトを実装している任意のコレクション型を反復するために使えます。具体的には、ForEach トレイトは1つのシグネチャを定義しています:
///
/// forEach 操作をサポートするデータ構造のためのトレイト。
///
trait ForEach[t] {
///
/// データ構造内の要素の型。
///
type Elm: Type
///
/// `forEach` のエフェクト。
///
type Aef: Eff = {}
///
/// データ構造内の各要素に `f` を適用します。
///
pub def forEach(f: ForEach.Elm[t] -> Unit \ ef, t: t): Unit \ (ef + ForEach.Aef[t])
}
注意: Flix は、
foreachの本体となる式がUnit型であることを期待します。
ForEach コンビネータ
ForEach モジュールは、コレクションの反復方法を変換する4つのコンビネータ withIndex、withFilter、withMap、withZip を提供しています。各コンビネータはコレクションをラップし、foreach 構文で直接使える新しい ForEach 互換の値を返します。
インデックス付きの反復
withIndex コンビネータは、各要素を0始まりのインデックスと組にします:
use ForEach.withIndex;
def main(): Unit \ IO =
let langs = List#{"Flix", "Haskell", "Scala"};
foreach ((i, lang) <- withIndex(langs)) {
println("${i}: ${lang}")
}
これは次を出力します:
0: Flix
1: Haskell
2: Scala
要素のフィルタリング
withFilter コンビネータは、述語を満たさない要素をスキップします:
use ForEach.withFilter;
def main(): Unit \ IO =
let numbers = List#{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
foreach (x <- withFilter(x -> x `Int32.modulo` 2 == 0, numbers)) {
println("${x}")
}
これは偶数のみ、すなわち 2、4、6、8、10 を出力します。
要素のマッピング
withMap コンビネータは、各要素が生成(yield)される前に変換を適用します:
use ForEach.withMap;
def main(): Unit \ IO =
let numbers = List#{1, 2, 3, 4, 5};
foreach (x <- withMap(x -> x * 10, numbers)) {
println("${x}")
}
これは 10、20、30、40、50 を出力します。
2つのコレクションの zip
withZip コンビネータは、2つのコレクションを要素ごとに zip し、組を生成します。反復は短い方のコレクションが尽きた時点で停止します:
use ForEach.withZip;
def main(): Unit \ IO =
let names = List#{"Alice", "Bob", "Carol"};
let ages = List#{30, 25, 40};
foreach ((name, age) <- withZip(names, ages)) {
println("${name} is ${age} years old")
}
注意:
withZipは、両方のコレクションが(ForEachだけでなく)Iterableトレイトを実装していることを必要とします。
Monadic For-Yield
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、Scala の for 内包表記や Haskell の do 記法に似た、モナドの forM-yield 構文をサポートしています。forM 構文は、point と flatMap(これらは Monad トレイトによって提供されます)を使用するための糖衣構文です。forM 構文は、empty(これは MonadZero トレイトによって提供されます)を使用する guard 式もサポートしています。
たとえば、次のモナドの forM 式:
let l1 = 1 :: 2 :: Nil;
let l2 = 1 :: 2 :: Nil;
forM (x <- l1; y <- l2)
yield (x, y)
は、次のリストに評価されます:
(1, 1) :: (1, 2) :: (2, 1) :: (2, 2) :: Nil
ガード式を使用する
forM 式の中で ガード式 を使用することができます。たとえば、次のプログラム:
let l1 = 1 :: 2 :: Nil;
let l2 = 1 :: 2 :: Nil;
forM (x <- l1; y <- l2; if x < y)
yield (x, y)
は、次のリストに評価されます:
(1, 2) :: Nil
Option と Result を扱う
forM を使って Option データ型を扱うこともできます。たとえば:
def divide(x: Int32, y: Int32): Option[Int32] =
if (y == 0) None else Some(x / y)
def f(): Option[Int32] =
forM (
x <- divide(5, 2);
y <- divide(x, 8);
z <- divide(9, y)
) yield x + y + z
ここで、関数 f は None を返します。なぜなら x = 5 / 2 = 2 であり、2 / 8 = 0 となるため、最後の除算が失敗するからです。
同様に、forM を使って Result[e, t] データ型を扱うことができます。たとえば:
use Sys.Console
def main(): Result[String, Unit] \ IO =
println("Please enter your first name, last name, and age:");
forM (
fstName <- Console.readln();
lstName <- Console.readln();
ageLine <- Console.readln();
ageNum <- Int32.parse(10, ageLine)
) yield {
println("Hello ${lstName}, ${fstName}.");
println("You are ${ageNum} years old!")
}
ここで main は、ユーザーに名(first name)、姓(last name)、年齢の入力を促します。Console.readln の各呼び出しは、エラーまたは入力文字列のいずれかである Result[String, String] 値を返します。したがって、ローカル変数 fstName、lstName、ageLine は String です。Int32.parse を使用して ageLine を Int32 にパースします。これは Result[String, Int32] 値を返します。すべての操作が成功した場合、あいさつを表示して Ok(())(すなわち Unit の Ok)を返します。そうでなければ、Err(msg) 値を返します。
その他の Monad を扱う
forM は、Chain や Nel(空でないリスト)を含む、他の種類の Monad でも使用できます。たとえば、次のように書くことができます:
let l1 = Nel(1, 2 :: Nil);
let l2 = Nel(1, 2 :: Nil);
forM (x <- l1; y <- l2)
yield (x, y)
これは、次の空でないリストに評価されます:
Nel((1, 1), (1, 2) :: (2, 1) :: (2, 2) :: Nil)
注意: 空でないリストでは
if-ガードを使用することができません。なぜなら、そのようなif-ガードはMonadZeroトレイトのインスタンスを必要としますが、空でないリストはこれを実装していないからです(そのようなリストは空になることができないため)。
脱糖
forM 式は、Monad.flatMap、Applicative.point、MonadZero.empty を使用するための糖衣構文です。
たとえば、次の式:
let l1 = 1 :: 2 :: Nil;
let l2 = 1 :: 2 :: Nil;
forM (x <- l1; y <- l2; if x < y)
yield (x, y)
は、次のように脱糖されます:
Monad.flatMap(x ->
Monad.flatMap(y ->
if (x < y)
Applicative.point((x, y))
else
MonadZero.empty(),
l2),
l1);
Applicative For-Yield
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、モナド的(monadic)な forM 式に加えて、Applicative トレイト(Trait)を基盤とするアプリカティブ(applicative)な forA 式をサポートしています。forA 構文を使うと、Validation[e, t] データ型を用いたエラー処理コードを簡単に書けます。
Validation を扱う
forA 式を使うと、すべてのエラーを収集しながらユーザー入力を検証(validate)できます。
enum Connection(String, String)
enum InvalidInput {
case InvalidUserName,
case InvalidPassword
}
def validateUser(s: String): Validation[InvalidInput, String] =
if (8 <= String.length(s) and String.forAll(Char.isLetter, s))
Validation.Success(s)
else
Validation.Failure(Nec.singleton(InvalidInput.InvalidUserName))
def validatePass(s: String): Validation[InvalidInput, String] =
if (12 <= String.length(s) and String.length(s) <= 20)
Validation.Success(s)
else
Validation.Failure(Nec.singleton(InvalidInput.InvalidPassword))
def connect(u: String, p: String): Validation[InvalidInput, Connection] =
forA (
user <- validateUser(u);
pass <- validatePass(p)
) yield Connection.Connection(user, pass)
次の式は、
connect("Lucky Luke", "Ratata")
次のように評価されます。
Failure(Nec#{InvalidUserName, InvalidPassword})
これには入力検証のエラーが 両方とも 含まれています。一方、次の式は、
connect("luckyluke", "password12356789")
次のように評価されます。
Success(Connection(luckyluke, password12356789))
アプリカティブは独立した計算である
モナド的な forM 式では、あるモナド操作の結果を別のモナド操作の入力として使うことができます。たとえば次のようになります。
forM(x <- Some(123); y <- Some(x))
yield (x, y)
ここでは y の値が x に依存しています。つまり、x と y の計算は独立していません。
これと同じことをアプリカティブな forA 式で試すと、
forA(x <- Some(123); y <- Some(x))
yield (x, y)
Flix コンパイラは次のコンパイルエラーを出力します。
❌ -- Resolution Error --------------
>> Undefined name 'x'.
10 | y <- Some(x)
^
name not found
これは、x と y の計算が 独立している ため、y の値を定義する時点で x の値がスコープ内に ない からです。
脱糖(Desugaring)
forA 式は、Functor.map と Applicative.ap を使うための糖衣構文です。
たとえば、次の式は、
let o1 = Some(21);
let o2 = Some(42);
forA(x <- o1; y <- o2)
yield x + y;
次のように脱糖されます。
Applicative.ap(Functor.map(x -> y -> x + y, o1), o2)
エフェクトシステム
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、言語に完全に統合された最先端の型およびエフェクトシステム (type and effect system)を備えています。Flix のエフェクトシステムは 強力かつ広範であり、エフェクト多相(effect polymorphism)、 サブエフェクティング(sub-effecting)、エフェクト除外(effect exclusion)、 純粋性リフレクション(purity reflection)、関連エフェクト(associated effects)を サポートしています。
これらの新しく刺激的な機能については、以降のページで探っていきます。
エフェクトシステムにはどのような利点があるのでしょうか。利点は数多くあります。
-
(純粋性 / Purity) 型およびエフェクトシステムは、純粋(pure)な関数と 純粋でない(impure)関数を分離します。Flix では、純粋な関数は一切の副作用を 持つことができず、同じ引数を与えられたときには同じ値を返さなければなりません。 とはいえ、純粋な関数であっても、ミュータブルなデータ構造を使って命令型スタイルで 実装することは可能です。ただし、それらのデータ構造が関数の終了時にスコープから 外れる場合に限ります。
-
(推論 / Reasoning) 型およびエフェクトシステムは、すべての関数に対して 引数と戻り値の型、そして関数の副作用を明示することを求めることで、プログラマが 自分のプログラムの動作を理解する助けになります。
-
(モジュール性 / Modularity) 型およびエフェクトシステムは、プログラムの どこでどの副作用が許可されるかをプログラマに検討させることで、モジュール性を 強制します。さらに、エフェクトは — 型と同じように — コンパイラに よって検査されるドキュメントとしての役割を果たします。
-
(エフェクトとハンドラ / Effects and Handlers) 型およびエフェクトシステムは、 代数エフェクト(algebraic effects)とハンドラ(handler)の基盤となります。 これらによって、プログラマは例外(exception)、async/await、協調的マルチタスク (cooperative multitasking)といった独自の制御構造を実装できるようになります。
-
(セキュリティ / Security) 型およびエフェクトシステムは、関数の振る舞いに ついて鉄壁の保証を提供し、プログラマが未知のコードへの信頼を高められるように します。たとえば、ある関数が純粋であれば、その関数は一切の副作用を持つことが できません。すなわち、ファイルシステムやネットワークなどにアクセスできません。 具体的な利点として、プログラムがサプライチェーン攻撃(supply chain attack)に 対してより強くなります。
-
(純粋性リフレクション / Purity Reflection) Flix 標準ライブラリ(および それを拡張する他のライブラリの作者)は、purity reflection を使って、高階関数に渡される関数引数の純粋性を検査できます。この情報を活用すれば、 プログラムの本来の意味論を保ちながら、自動並列化を実装できます。たとえば Flix では、
Set.count関数は、(a) 集合が十分に大きく、かつ (b) 渡された述語関数が純粋である 場合に、並列評価を使います。 -
(最適化 / Optimizations) Flix コンパイラは、純粋性の情報を活用して、 積極的なデッドコード除去(dead code elimination)とインライン化(inlining)を 行います。
Flix の型およびエフェクトシステムはかなり洗練されており、効果的に使うには ある程度の予備知識が必要です。次のいくつかの節では、型およびエフェクトシステムの 機能を順を追って紹介し、その使用例をいくつか示します。
先に進む前に、Flix には 3 種類のエフェクトがあることを理解しておくことが重要です。
トレイト(trait)とエフェクトがどのように相互作用するかについては、関連エフェクト の節で説明します。
ダイレクトスタイル
Flix は、伝統的な型およびエフェクトシステムを備えた、いわゆる
ダイレクトスタイル(direct-style) のプログラミング言語です。これは、
Cats Effect、Kyo、
ZIO のような、いわゆる 関数型エフェクトシステム
(functional effect systems) とは対照的です。これらのシステムは、ライブラリ
レベルのエフェクトシステム、本質的にはカスタムの IO モナドを提供します。
このアプローチにはいくつかの利点がありますが、欠点も少なくとも 2 つあります。
(a) 伝統的な型およびエフェクトシステムが提供する保証(たとえば、ある関数が
いつ純粋であるかを知ること)が一切得られないこと、そして (b) プログラムを
モナディックスタイル(monadic-style)で書かなければならず、これは煩雑である
ことです。
プリミティブエフェクト
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Note: このページは少しだけ更新されており、書き直しが予定されています。
Flix には、あらかじめ定義された一連のプリミティブエフェクト(primitive effect)が用意されています。代数エフェクト(algebraic effect)やヒープエフェクト(heap effect)とは異なり、プリミティブエフェクトはハンドルすることができず、スコープから外れることもありません。プリミティブエフェクトは、マシン上で発生する副作用を表します。それは取り消したり、再解釈したりすることはできません。
最も重要なプリミティブエフェクトは IO エフェクトです。
IO エフェクト
IO エフェクトは、プログラムの外側の世界と相互作用するあらゆるアクションを表します。そのようなアクションには、コンソールへの出力、ファイルの作成・読み込み・書き込み、ネットワークへのアクセスなどが含まれます。IO は、外側の世界を 変更する アクション(例:ファイルの変更)だけでなく、外側の世界に単に アクセスする アクション(例:現在時刻の取得)も表します。純粋関数とは異なり、IO エフェクトを持つ関数は、たとえ引数が同じであっても、呼び出されるたびに振る舞いが変わる可能性があります。たとえば、同じファイルを2回読み込んでも、2回のアクセスの間にファイルが変更されているかもしれないため、同じ結果が返るとは限りません。
IO エフェクト、そして他のすべてのプリミティブエフェクトは 伝播的(viral) です。ある関数がプリミティブエフェクトを持つ場合、その呼び出し元もすべて同じプリミティブエフェクトを持つことになります。つまり、いったん不純さに染まってしまうと、染まったままであり続けるのです。
その他のプリミティブエフェクト
- NonDet:
NonDetエフェクトは、ほぼ純粋な計算を表します。たとえば、コインを投げる関数は事実上純粋です。副作用を持ちません。しかし、同じ引数を与えられても、異なる結果を返す可能性があります。
エフェクト多相
💡 お知らせ: このドキュメントは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 は 抽象化サイトのサブエフェクティング と インスタンス定義のサブエフェクティング をサポートしている、と言います。
エフェクトとハンドラ
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、Eff や Koka のスタイルで、代数エフェクト(algebraic effects)とハンドラ(handlers)をサポートしています。
Flix のエフェクトハンドラは、動的スコープ(dynamic scope)、ディープハンドラ(deep handlers)を用い、複数回の再開(multiple resumptions)をサポートしています。
このセクションではエフェクトとハンドラを紹介しますが、あわせて次の資料にも目を通すことをおすすめします。
- An Introduction to Algebraic Effects and Handlers — Matija Pretnar
まずは、ほとんどのプログラマになじみのある種類のエフェクト、すなわち 例外(exceptions) から始めましょう。
再開不可能なエフェクト: 例外
エフェクトとハンドラを使って例外を実装できます。たとえば次のようになります。
eff DivByZero {
def divByZero(): Void
}
def divide(x: Int32, y: Int32): Int32 \ DivByZero =
if (y == 0) {
DivByZero.divByZero()
} else {
x / y
}
def main(): Unit \ IO =
run {
println(divide(3, 2));
println(divide(3, 0))
} with handler DivByZero {
def divByZero(_resume) = println("Oops: Division by Zero!")
}
ここでは DivByZero エフェクトを宣言し、divide 関数の中でそれを使っています。そのため divide 関数は DivByZero エフェクトを持ちます。main では2つの除算を行います。1つ目は成功して 1 を出力します。2つ目は失敗してエラーメッセージを出力します。継続(continuation)である _resume は使われておらず、その引数の型が Void であるため使うこともできません。main 関数はハンドラの中で println を使っているため IO エフェクトを持ちますが、DivByZero エフェクトは処理済みであるため、それは持ち ません。
例外が再開不可能(non-resumable)であるのは、いったん例外が送出されると、例外が投げられた場所から実行を再開できないからです。できるのは、例外を処理して別のことを行うことだけです。DivByZero が例外であると分かるのは、そのエフェクト操作が Void の戻り値型を持つからです。
注意:
Void型は、Flix に組み込まれた空の型、すなわち値を持たない(uninhabited)型です。戻り値型がVoidの関数は正常に返ることができず、異常な形で(たとえば例外を投げることで)のみ返ります。同様に、Void型の引数を取る関数は呼び出すことができません。
Flix はエフェクト多相(effect polymorphism)をサポートしていることを思い出してください。そのため、次のコードは問題なく動作します。
def main(): Unit \ IO =
let l = List#{1, 2, 0, 3};
run {
List.map(x -> println(divide(42, x)), l);
()
} with handler DivByZero {
def divByZero(_) = println("Oops: Division by Zero!")
}
このプログラムは次を出力します。
42
21
Oops: Division by Zero!
これは、divide の最初の2回の呼び出しは成功する一方で、最後の呼び出しが DivByZero 例外を送出するからです。特筆すべきは、Flix の型およびエフェクトシステムが、List.map へのエフェクト多相な呼び出しを通じて例外エフェクトを追跡できるという点です。
再開可能なエフェクト
Flix は再開可能(resumable)なエフェクトもサポートしています。たとえば次のようになります。
import java.time.LocalDateTime
eff HourOfDay {
def getCurrentHour(): Int32
}
def greeting(): String \ {HourOfDay} =
let h = HourOfDay.getCurrentHour();
if (h <= 12)
"Good morning"
else if (h <= 18)
"Good afternoon"
else
"Good evening"
def main(): Unit \ IO =
run {
println(greeting())
} with handler HourOfDay {
def getCurrentHour(_, resume) =
let dt = LocalDateTime.now();
resume(dt.getHour())
}
ここでは、その日の現在の時刻を返す1つの操作を持つエフェクト HourOfDay を宣言しています。次に、HourOfDay エフェクトを使って現在時刻に応じた挨拶を返す greeting 関数を定義します。最後に main で greeting を呼び出し、その結果を出力します。特に、HourOfDay のハンドラは Java 相互運用を使って現在の時刻を取得しています。
重要なのは、エフェクト getHourOfDay が呼び出されると、Flix が現在の継続をキャプチャし、(main にある)もっとも近いハンドラを見つけ、そのハンドラがシステムクロックから取得したその日の現在時刻を使って greeting の内部から計算を 再開(resume) するという点です。
複数のエフェクトとハンドラ
複数のエフェクトを使う関数を書くことができます。
eff Ask {
def ask(): String
}
eff Say {
def say(s: String): Unit
}
def greeting(): Unit \ {Ask, Say} =
let name = Ask.ask();
Say.say("Hello Mr. ${name}")
def main(): Unit \ IO =
run {
greeting()
} with handler Ask {
def ask(_, resume) = resume("Bond, James Bond")
} with handler Say {
def say(s, resume) = { println(s); resume() }
}
ここでは Ask と Say という2つのエフェクトを宣言しています。Ask エフェクトは消費者(consumer)です。すなわち、環境から文字列を必要とします。Say エフェクトは生産者(producer)です。すなわち、環境へ文字列を渡します。greeting ではこの両方のエフェクトを使っています。main では greeting を呼び出し、それぞれのエフェクトを処理します。Ask エフェクトは、常に文字列 "Bond, James Bond" で継続を再開することによって処理します。Say エフェクトは、コンソールに出力してから継続を再開することによって処理します。
複数回の再開
Flix は、複数回の再開を伴う代数エフェクトをサポートしています。このようなエフェクトを使うと、async/await、バックトラッキング探索、協調的マルチタスクなどを実装できます。
次は簡単な例です。
eff Amb {
def flip(): Bool
}
eff Exc {
def raise(m: String): Void
}
def drunkFlip(): String \ {Amb, Exc} = {
if (Amb.flip()) {
let heads = Amb.flip();
if (heads) "heads" else "tails"
} else {
Exc.raise("too drunk to flip")
}
}
def handleAmb(f: a -> b \ ef): a -> List[b] \ ef - Amb =
x -> run {
f(x) :: Nil
} with handler Amb {
def flip(_, resume) = resume(true) ::: resume(false)
}
def handleExc(f: a -> b \ ef): a -> Option[b] \ ef - Exc =
x -> run {
Some(f(x))
} with handler Exc {
def raise(_, _) = None
}
def main(): Unit \ IO = {
// 出力: Some(heads) :: Some(tails) :: None :: Nil
handleAmb(handleExc(drunkFlip))() |> println;
// 出力: None
handleExc(handleAmb(drunkFlip))() |> println
}
ここでは Amb(ambiguous の略)と Exc(exception の略)という2つのエフェクトを宣言しています。次に drunkFlip 関数を定義します。これは、コインを投げようとする酔っ払いをモデル化するというアイデアです。第1に、その男がコインを投げられるか、それとも落としてしまうかを決めるためにコインを投げます。第2に、コイン投げが成功した場合、表(heads)か裏(tails)かを得るためにもう一度コインを投げます。重要なのは、drunkFlip が概念的に「heads」「tails」「too drunk」という3つの結果を持つという点です。
次に、handleAmb と handleExc という2つのエフェクトハンドラを定義します。後者から見ていくと、Exc ハンドラは例外を捕捉して None を返します。例外が送出されなければ、計算された値の Some(x) を返します。Amb ハンドラは、true と false で継続を 2回 呼び出し、その結果をリストに収集することで flip エフェクトを処理します。言い換えると、Amb ハンドラはコイン投げの 両方 の結果を探索します。
main ではこの2つのエフェクトハンドラを使います。特筆すべきは、ハンドラのネストの順序が重要である という点です! Exc エフェクトを先に処理すると、リスト Some(heads) :: Some(tails) :: None :: Nil が得られます。一方、Exc を最後に処理すると、計算全体が None で失敗します。
代数エフェクトとモナド
Flix は、代数エフェクトハンドラとモナド(monads)の両方をサポートしています。これは、両方のプログラミングスタイルをサポートしたいからです。
-
エフェクトハンドラを使ってプログラミングしたければ、そうできます。
-
ファンクタ(functors)、アプリカティブファンクタ(applicative functors)、モナドを使ってプログラミングしたければ、そうできます。
Flix は(まだ)IO モナドを定義していませんが、自分で作ることはできます。
Flix の標準ライブラリは、ハイブリッドなアプローチに寄っています。外界とのやり取りをモデル化するには代数エフェクトを使いますが、単純なエラー処理には Option や Result データ型を好みます。Option や Result を扱うのは、モナド構文(monadic syntax)を使うとより快適になります。
制限: 多相エフェクト
Flix の型およびエフェクトシステムは、多相エフェクト(polymorphic effects)をまだサポートしていません。1
たとえば、多相的な Throw[a] エフェクトを宣言することは できません。
eff Throw[a] {
def throw(x: a): Void
}
Flix コンパイラは次のエラーメッセージを出力します。
❌ -- Syntax Error --
>> Unexpected effect type parameters.
1 | eff Throw[a] {
^
unexpected effect type parameters
残念ながら、異なる型の値を投げる必要がある場合は、別々のエフェクトを宣言しなければなりません。
たとえば次のようになります。
eff ThrowBool {
def throw(x: Bool): Void
}
eff ThrowInt32 {
def throw(x: Int32): Void
}
new object 式と spawn 式における未処理のエフェクト
Flix は、new object 式でも spawn 式でも、未処理のエフェクトを許可しません。
たとえば、次のように書くと、
eff Ask {
def ask(): String
}
def main(): Unit \ IO =
region rc {
spawn Ask.ask() @ rc
}
Flix コンパイラは次のエラーメッセージを出力します。
-- Safety Error --------------------------------------------------
>> Illegal spawn effect: 'Ask'.
>> A spawn expression must be pure or have a primitive effect.
7 | spawn do Ask.ask() @ rc
^^^^^^^^^^^^
illegal effect.
-
この制限を取り除く方法を、現在調査中です。 ↩
デフォルトハンドラ
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は デフォルトハンドラ(default handler) をサポートしています。これは、
エフェクトが、そのエフェクトを IO エフェクトへと変換するハンドラを宣言できる
ことを意味します。これにより、main(および @Test が付与された任意のメソッド)は、
run-with ブロックでハンドラを明示的に提供することなく、そのエフェクトを
使えるようになります。
たとえば、次のように書けます。
use Sys.Env
use Time.Clock
def main(): Unit \ {Clock, Env, Logger} =
let ts = Clock.currentTime(TimeUnit.Milliseconds);
let os = Env.getOsName();
Logger.info("UNIX Timestamp: ${ts}");
Logger.info("Operating System: ${os}")
これを Flix コンパイラは次のように変換します。
use Sys.Env
use Time.Clock
def main(): Unit \ IO =
run {
let ts = Clock.currentTime(TimeUnit.Milliseconds);
let os = Env.getOsName();
Logger.info("UNIX Timestamp: ${ts}");
Logger.info("Operating System: ${os}")
} with Clock.runWithIO
with Env.runWithIO
with Logger.runWithIO
すなわち、Flix コンパイラは Clock.runWithIO、Env.runWithIO、
Logger.runWithIO の呼び出しを自動的に挿入します。これらは、それぞれの
エフェクトに対するデフォルトハンドラです。
たとえば、Clock.runWithIO は次のように宣言されています。
use Time.Clock
@DefaultHandler
pub def runWithIO(f: Unit -> a \ ef): a \ (ef - Clock) + IO = ...
デフォルトハンドラは @DefaultHandler アノテーションを使って宣言します。
各エフェクトが持てるデフォルトハンドラは高々 1 つであり、それはそのエフェクトの
コンパニオンモジュール(companion module)の中になければなりません。
デフォルトハンドラは、次の形式のシグネチャを持たなければなりません。
def runWithIO(f: Unit -> a \ ef): a \ (ef - E) + IO
ここで E はエフェクトの名前です。
デフォルトハンドラを持つエフェクトは、テストの中で使えます。たとえば:
@Test
def myTest01(): Unit \ {Assert, Logger} =
Logger.info("Running test!");
Assert.assertEq(expected = 42, 42)
エフェクト指向プログラミング
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
エフェクトを使ったプログラミングには、新しい考え方、すなわち エフェクト指向の考え方(effect-oriented mindset) が必要です。
JavaScript や Python から、C# や Java のような静的型付けのプログラミング言語へ移ってきたプログラマーを想像してみてください。もし彼らが自分自身の型を導入せず、オブジェクト、map、文字列ばかりでプログラミングを続けるなら、静的型システムの恩恵は失われてしまいます。同じように、もしプログラマーが エフェクト指向の考え方 を取り入れずに Flix にやってきたなら、Flix の型システムとエフェクトシステムの恩恵は失われてしまいます。
Flix では、すべての関数に IO エフェクトを与えて、エフェクトを伴うコードをあらゆる場所で呼び出すこともできますが、これはエフェクト指向プログラミングではなく、悪いプログラミングスタイルです。適切なエフェクト指向のプログラム設計は、代数エフェクトとハンドラを使うかもしれない関数的なコア(functional core)と、それを取り囲み IO を実行する命令的なシェル(imperative shell)から構成されます。経験則として、IO エフェクトは main 関数の 近く にあるべきです。
それでは、これらの点を例を使って説明していきましょう。
数当てゲーム — 間違ったやり方
Flix と Java の混在したスタイルで書かれた、次のプログラムを考えてみましょう:
import java.lang.System
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.{Random => JRandom}
def getSecretNumber(): Int32 \ {NonDet, IO} =
let rnd = new JRandom();
rnd.nextInt()
def readGuess(): Result[String, String] \ IO =
let reader = new BufferedReader(new InputStreamReader(System.in));
let line = reader.readLine();
if (Object.isNull(line))
Result.Err("no input")
else
Result.Ok(line)
def readAndParseGuess(): Result[String, Int32] \ IO =
forM(g <- readGuess();
n <- Int32.parse(10, g)
) yield n
def gameLoop(secret: Int32): Unit \ IO = {
println("Enter a guess:");
match readAndParseGuess() {
case Result.Ok(g) =>
if (secret == g) {
println("Correct!")
} else {
println("Incorrect!");
gameLoop(secret)
}
case Result.Err(_) =>
println("Not a number? Goodbye.");
println("The secret was: ${secret}")
}
}
def main(): Unit \ {NonDet, IO} =
let secret = getSecretNumber();
gameLoop(secret)
ここでは、すべての関数、すなわち getSecretNumber、readGuess、readAndParseGuess、gameLoop、main が IO エフェクトを持っています。その結果、どの関数も何でもできてしまいます。エフェクトを伴うコードが、プログラム全体のあちこちに散らばっている点に注目してください。
このスタイルで書かれたプログラムを理解し、リファクタリングし、テストするのは悪夢です。
エフェクト指向のスタイルでプログラミングするということは、外側の世界と相互作用するあらゆるアクションに対してエフェクトを定義すべきだということです。そして、それらのエフェクトを main 関数の近くで ハンドル すべきです。
数当てゲーム — 正しいやり方
本来こうすべきだったものを次に示します:
import java.lang.System
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.{Random => JRandom}
eff Guess {
def readGuess(): Result[String, String]
}
eff Secret {
def getSecret(): Int32
}
eff Terminal {
def println(s: String): Unit
}
def readAndParseGuess(): Result[String, Int32] \ {Guess} =
forM(g <- Guess.readGuess();
n <- Int32.parse(10, g)
) yield n
def gameLoop(secret: Int32): Unit \ {Guess, Terminal} = {
Terminal.println("Enter a guess:");
match readAndParseGuess() {
case Result.Ok(g) =>
if (secret == g) {
Terminal.println("Correct!")
} else {
Terminal.println("Incorrect!");
gameLoop(secret)
}
case Result.Err(_) =>
Terminal.println("Not a number? Goodbye.");
Terminal.println("The secret was: ${secret}")
}
}
def main(): Unit \ {NonDet, IO} =
run {
let secret = Secret.getSecret();
gameLoop(secret)
} with handler Secret {
def getSecret(_, resume) =
let rnd = new JRandom();
resume(rnd.nextInt())
} with handler Guess {
def readGuess(_, resume) =
let reader = new BufferedReader(new InputStreamReader(System.in));
let line = reader.readLine();
if (Object.isNull(line))
resume(Result.Err("no input"))
else
resume(Result.Ok(line))
} with handler Terminal {
def println(s, resume) = { println(s); resume() }
}
ここでは、3つの代数エフェクトを導入しました:
- ユーザーに推測を尋ねるアクションを表す
Guessエフェクト。 - 秘密の数を選ぶアクションを表す
Secretエフェクト。 - コンソールへの出力というアクションを表す
Terminalエフェクト。
各関数は、関連するエフェクトのみを使うように書かれています。たとえば、gameLoop 関数は Guess と Terminal エフェクトを使い、それ以外のエフェクトは持ちません。さらに、すべてのエフェクトが1か所、すなわち main 関数の中でハンドルされるようになりました。その結果、ビジネスロジックは純粋に関数的になります。不純さが必要な箇所では、エフェクトとハンドラの使用によって、それが正確にカプセル化されます。
ライブラリエフェクト
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、一般的な I/O 操作のための組み込みライブラリエフェクト(Library effect)をいくつか提供しています。これらのエフェクトはすべてデフォルトハンドラを持っているため、main で明示的に runWithIO を呼び出す必要はありません。
| エフェクト | 説明 |
|---|---|
Assert | 実行時アサーション(assertTrue、assertEq など)。ハンドラの設定が可能です。 |
Logger | 5 段階の重大度レベルによる構造化ログ。フィルタリングと収集に対応しています。 |
Math.Random | 擬似乱数の生成。シードによる決定的な動作も選択できます。 |
Fs.FileSystem Fs.FileRead Fs.FileWrite Fs.FileStat | ファイル I/O、メタデータ、ディレクトリ、およびミドルウェア(chroot、アトミックな書き込み、インメモリファイルシステムなど)。 |
Net.Http Net.Https | フルーエント API による HTTP リクエストの送信と、ミドルウェア(リトライ、レート制限、サーキットブレーカー)。 |
Sys.Console | ターミナル I/O:入力の読み取り、stdout/stderr への出力、プロンプト、メニュー。 |
Sys.Env | 環境変数、システムプロパティ、プラットフォーム情報へのアクセス。 |
Sys.Exit | 指定した終了コードによるプログラムの終了。 |
Sys.Process | OS プロセスの起動と管理。 |
Time.Clock | さまざまな単位での現在時刻(実時間)の取得。 |
Time.Sleep | 現在のスレッドの一時停止。合成可能なミドルウェア(ジッター、上限、ログ出力)に対応しています。 |
Assert
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、実行時のアサーション(Assertion)のためのライブラリエフェクトとして Assert を提供しています。Assert エフェクトにはデフォルトハンドラがあるため、main で明示的に runWithIO を呼び出す必要はありません。
基本的なアサーション
Assert モジュールは、いくつかのアサーション関数を提供しています。
mod Assert {
/// `cond` が `true` であることをアサートします。
def assertTrue(cond: Bool): Unit \ Assert
/// `cond` が `false` であることをアサートします。
def assertFalse(cond: Bool): Unit \ Assert
/// `expected` が `actual` と等しいことをアサートします。
def assertEq(expected: a, actual: a): Unit \ Assert with Eq[a], ToString[a]
/// `unexpected` が `actual` と等しくないことをアサートします。
def assertNeq(unexpected: a, actual: a): Unit \ Assert with Eq[a], ToString[a]
/// `o` が `Some` であることをアサートします。
def assertSome(o: Option[a]): Unit \ Assert
/// `o` が `None` であることをアサートします。
def assertNone(o: Option[a]): Unit \ Assert with ToString[a]
/// `r` が `Ok` であることをアサートします。
def assertOk(r: Result[e, a]): Unit \ Assert with ToString[e]
/// `r` が `Err` であることをアサートします。
def assertErr(r: Result[e, a]): Unit \ Assert with ToString[a]
/// `ma` が空であることをアサートします。
def assertEmpty(ma: m[a]): Unit \ Assert with Foldable[m]
/// `ma` が空でないことをアサートします。
def assertNonEmpty(ma: m[a]): Unit \ Assert with Foldable[m]
/// 指定されたメッセージ `msg` とともに無条件に成功します。
def success(msg: String): Unit \ Assert
/// 指定されたメッセージ `msg` とともに無条件に失敗します。
def fail(msg: String): Unit \ Assert
}
デフォルトハンドラで Assert を使う
デフォルトハンドラは失敗時に AssertionError をスローするため、明示的なハンドラは必要ありません。
use Assert.{assertTrue, assertFalse, assertEq, assertNeq}
def main(): Unit \ { Assert, IO } =
assertTrue(1 + 1 == 2);
assertFalse(1 > 2);
assertEq(expected = 4, 2 + 2);
assertNeq(unexpected = 0, 1 + 1);
println("All assertions passed!")
失敗を標準出力に表示する
runWithStdOut ハンドラは、アサーションの失敗を標準出力に表示しつつ、実行を継続させます。
use Assert.assertEq
def main(): Unit \ IO =
run {
assertEq(expected = 4, 2 + 2);
assertEq(expected = 10, 3 + 3);
println("Execution continued after failing assertion.")
} with Assert.runWithStdOut
Logger で失敗をログに記録する
runWithLogger ハンドラは、アサーションの失敗を Logger エフェクトに送ります。
use Assert.assertEq
def main(): Unit \ IO =
run {
assertEq(expected = 42, 21 + 21);
assertEq(expected = 10, 3 + 3);
println("Execution continued after failing assertion.")
} with Assert.runWithLogger
with Logger.runWithIO
Clock
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、現在の実時間(wall-clock time)を問い合わせるためのライブラリエフェクトとして Clock を提供しています。Clock エフェクトはデフォルトハンドラを持つため、main の中で明示的に runWithIO を呼び出す必要はありません。中心となるモジュールは Time.Clock です。
Clock エフェクト
Clock エフェクトは、エポック(epoch)からの経過時間を指定した単位で返す、単一の操作を持ちます。
pub eff Clock {
/// エポックからの経過時間を、指定された時間単位 `u` で返す。
def currentTime(u: TimeUnit): Int64
}
結果の粒度は TimeUnit enum によって決まります。
pub enum TimeUnit with Eq, ToString {
case Days,
case Hours,
case Microseconds,
case Milliseconds,
case Minutes,
case Nanoseconds,
case Seconds
}
現在時刻の取得
Clock のもっとも単純な使い方は、現在時刻を読み取って表示することです。
use Time.Clock
use Time.TimeUnit
def main(): Unit \ { Clock, IO } =
let timestamp = Clock.currentTime(TimeUnit.Milliseconds);
println("${timestamp} ms since the epoch")
Clock はデフォルトハンドラを持つため、このエフェクトは自動的に処理されます。
now 関数
Clock.now 関数は、Clock.currentTime(TimeUnit.Milliseconds) の短縮形です。
use Time.Clock
def main(): Unit \ { Clock, IO } =
let before = Clock.now();
// ... 何らかの処理を行う ...
let after = Clock.now();
println("Elapsed: ${after - before} ms")
Console
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、ターミナル I/O のためのライブラリエフェクトとして Console を提供しています。Console エフェクトにはデフォルトハンドラがあるため、main の中で明示的に runWithIO を呼び出す必要はありません。主要なモジュールは Sys.Console です。
Console エフェクト
Console エフェクトは、標準入力からの読み取りと、標準出力および標準エラー出力への書き込みをサポートしています:
pub eff Console {
/// コンソールから 1 行を読み取ります。
def readln(): String
/// 与えられた文字列 `s` を標準出力に出力します。
def print(s: String): Unit
/// 与えられた文字列 `s` を標準エラー出力に出力します。
def eprint(s: String): Unit
/// 与えられた文字列 `s` を標準出力に出力し、続けて改行を出力します。
def println(s: String): Unit
/// 与えられた文字列 `s` を標準エラー出力に出力し、続けて改行を出力します。
def eprintln(s: String): Unit
}
Console モジュール
Console モジュールは、Console エフェクトの上に構築された、いくつかの高レベルな関数を提供します:
mod Sys.Console {
/// プロンプト `p` を出力して 1 行を読み取り、入力が空の場合は `default` を返します。
def readlnWithDefault(p: a, default: String): String \ Console
/// プロンプト `p` を出力して 1 行を読み取り、入力に `f` を適用します。
/// `Err(msg)` の場合は再度プロンプトを表示し、`Ok(v)` の場合は `v` を返します。
def readlnWith(p: a, f: String -> Result[String, b]): b \ Console
/// yes/no のヒント付きでプロンプト `p` を出力し、ブール値の回答を読み取ります。
/// 入力が空または認識できない場合は `default` を返します。
def confirm(p: a, default: {default = Bool}): Bool \ Console
/// 番号付きの選択肢リストとともにプロンプト `p` を出力し、選択を読み取ります。
/// 入力が無効な場合は `None` を返します。
def pick(p: a, choices: List[b]): Option[b] \ Console
/// `pick` と同様ですが、ユーザーが有効な選択をするまで再度プロンプトを表示します。
def pickWith(p: a, choices: List[b]): b \ Console
}
基本的なコンソール I/O
Console の最もシンプルな使い方は、プロンプトを出力し、入力を読み取り、応答することです:
use Sys.Console
def main(): Unit \ Console =
Console.print("What is your name? ");
let name = Console.readln();
Console.println("Hello ${name}!")
確認付き入力
Console.confirm 関数は yes/no の質問をして Bool を返します。ユーザーが何も入力せずに Enter を押した場合に使われるデフォルト値を指定できます:
use Sys.Console
def main(): Unit \ Console =
let proceed = Console.confirm("Deploy to production?", default = true);
if (proceed)
Console.println("Deploying...")
else
Console.println("Aborted.")
バリデーション付き入力
Console.readlnWith 関数は、入力がバリデータを通過するまで繰り返しユーザーにプロンプトを表示します。バリデータは、成功時には Ok(value) を返し、再度プロンプトを表示させるには Err(message) を返します:
use Sys.Console
def main(): Unit \ Console =
let n = Console.readlnWith("Enter a number (1-10): ", s ->
match Int32.fromString(s) {
case Some(i) if i >= 1 and i <= 10 => Ok(i)
case _ => Err("Please enter a number between 1 and 10.")
}
);
Console.println("You entered: ${n}")
Env
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、環境変数、システムプロパティ、およびプラットフォーム情報にアクセスするためのライブラリエフェクト(Library effect)として Env を提供しています。Env エフェクトにはデフォルトハンドラがあるため、main の中で明示的に runWithIO を呼び出す必要はありません。主要なモジュールは Sys.Env です。
Env エフェクト
Env エフェクトは、プログラムの環境を読み取るための操作を提供します:
pub eff Env {
/// コマンドラインからプログラムに渡された引数を返します。
def getArgs(): List[String]
/// 現在のシステム環境の Map を返します。
def getEnv(): Map[String, String]
/// 指定された環境変数の値を返します。
def getVar(name: String): Option[String]
/// 名前で指定されたシステムプロパティを返します。
def getProp(name: String): Option[String]
/// オペレーティングシステム名を返します。
def getOsName(): Option[String]
/// オペレーティングシステムのアーキテクチャを返します。
def getOsArch(): Option[String]
/// オペレーティングシステムのバージョンを返します。
def getOsVersion(): Option[String]
/// ファイル区切り文字を返します。
def getFileSeparator(): String
/// パス区切り文字を返します。
def getPathSeparator(): String
/// システムの行区切り文字を返します。
def getLineSeparator(): String
/// ユーザーの現在の作業ディレクトリを返します。
def getCurrentWorkingDirectory(): Option[String]
/// デフォルトの一時ファイルのパスを返します。
def getTemporaryDirectory(): Option[String]
/// ユーザーのアカウント名を返します。
def getUserName(): Option[String]
/// ユーザーのホームディレクトリを返します。
def getUserHomeDirectory(): Option[String]
/// JVM が利用できる仮想プロセッサ数を返します。
def getVirtualProcessors(): Int32
}
ディレクトリと OS 情報
Env のもっとも簡単な使い方は、現在のディレクトリ、ホームディレクトリ、あるいはオペレーティングシステムを問い合わせることです:
use Sys.Env
def main(): Unit \ { Env, IO } =
let home = Env.getUserHomeDirectory();
println("Home: ${home}");
let cwd = Env.getCurrentWorkingDirectory();
println("CWD: ${cwd}");
let os = Env.getOsName();
println("OS: ${os}")
環境変数
単一の環境変数を読み取るには getVar を、すべての環境変数を Map として取得するには getEnv を使います:
use Sys.Env
def main(): Unit \ { Env, IO } =
let path = Env.getVar("PATH");
println("PATH: ${path}");
let all = Env.getEnv();
println("Total env vars: ${Map.size(all)}")
システム情報
Env エフェクトは、OS のアーキテクチャや利用可能なプロセッサ数といったプラットフォームの詳細情報も公開しています:
use Sys.Env
def main(): Unit \ { Env, IO } =
let name = Env.getOsName();
let arch = Env.getOsArch();
let version = Env.getOsVersion();
let cpus = Env.getVirtualProcessors();
println("OS: ${name}");
println("Arch: ${arch}");
println("Ver: ${version}");
println("CPUs: ${cpus}")
Exit
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、プログラムを終了させるためのライブラリエフェクト(Library effect)として Exit を提供しています。Exit エフェクトはデフォルトハンドラを持っているため、main の中で明示的に runWithIO を呼び出す必要はありません。中心となるモジュールは Sys.Exit です。
Exit エフェクト
Exit エフェクトはただ 1 つの操作を持ち、指定された終了コードで JVM を即座に停止させます。
pub eff Exit {
/// 指定された `exitCode` で JVM を即座に終了します。
def exit(exitCode: Int32): Void
}
戻り値の型が Void であることは、exit が正常に戻ることは決してないことを示しています。
プログラムの終了
Exit の最も単純な使い方は、特定の終了コードでプログラムを終了させることです。
use Sys.Exit
def main(): Unit \ { Exit, IO } =
println("Goodbye!");
Exit.exit(0)
慣例として、終了コードがゼロであれば成功を、非ゼロであればエラーを表します。
FileSystem
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、ファイルシステム操作のための一連のエフェクトを提供しています。主要なモジュールは次のとおりです:
Fs.FileSystem— 統合されたFileSystemエフェクト(全 29 操作)Fs.FileRead—FileReadエフェクト(read、readLines、readBytes)Fs.FileWrite—FileWriteエフェクト(write、append、delete、copy、move、mkdir など)Fs.FileStat—FileStatエフェクト(存在確認、種別判定、パーミッション、タイムスタンプ、サイズ)Fs.DirList—DirListエフェクト(ディレクトリ内容の一覧取得)Fs.Glob—Globエフェクト(パターンによるファイル検索)Fs.Size— ファイルサイズを扱うためのユーティリティ
これらのエフェクトはすべてデフォルトハンドラを持つため、main の中で明示的に runWithIO を呼び出す必要はありません。
さらに細粒度のリーフエフェクト(Leaf effect)(例:FileExists、ReadFile、WriteFile)も存在します。これらはデフォルトハンドラを持ちませんが、runWith ハンドラを使って親エフェクトへと実行を委ねることができます。詳細はエフェクト階層を参照してください。
ファイルの読み込み
FileRead.read を使うと、ファイル全体を文字列として読み込むことができます:
use Fs.FileRead
def main(): Unit \ { FileRead, IO } =
match FileRead.read("example.txt") {
case Ok(content) => println(content)
case Err(err) => println("Error: ${err}")
}
すべてのファイルシステム操作は Result[IoError, ...] を返します。IoError 型は ErrorKind とメッセージ文字列のペアです。ErrorKind enum は、何が問題だったのかを教えてくれます:
| ErrorKind | 説明 |
|---|---|
NotFound | ファイルまたはディレクトリが見つかりませんでした。 |
AlreadyExists | ファイルまたはディレクトリがすでに存在します。 |
PermissionDenied | アクセスが拒否されました(ミドルウェアでも使用されます)。 |
InvalidPath | パスの形式が不正です。 |
| … | その他。 |
注意: シグネチャに
IOエフェクトが現れるのは、printlnを使っているためです。
ファイルの書き込み
FileWrite.write を使うと、文字列をファイルに書き込むことができます:
use Fs.FileWrite
def main(): Unit \ { FileWrite, IO } =
match FileWrite.write(str = "Hello, Flix!", "greeting.txt") {
case Ok(_) => println("File written successfully.")
case Err(err) => println("Error: ${err}")
}
行単位の読み書き
readLines と writeLines を使うと、ファイルを行単位で扱うことができます:
use Fs.FileRead
use Fs.FileWrite
def main(): Unit \ { FileRead, FileWrite, IO } =
match FileWrite.writeLines(lines = List#{"Line 1", "Line 2", "Line 3"}, "data.txt") {
case Err(err) => println("Write error: ${err}")
case Ok(_) =>
match FileRead.readLines("data.txt") {
case Ok(lines) =>
foreach (line <- lines) {
println(line)
}
case Err(err) => println("Read error: ${err}")
}
}
注意: 読み込みと書き込みの両方を行うため、エフェクト集合には
FileRead、FileWrite、IOが含まれます。
バイト単位の読み書き
バイナリデータには readBytes と writeBytes を使うことができます:
use Fs.FileRead
use Fs.FileWrite
def main(): Unit \ { FileRead, FileWrite, IO } =
let data = Vector#{72i8, 101i8, 108i8, 108i8, 111i8};
match FileWrite.writeBytes(data, "binary.dat") {
case Err(err) => println("Write error: ${err}")
case Ok(_) =>
match FileRead.readBytes("binary.dat") {
case Ok(bytes) =>
println("Read ${Vector.length(bytes)} bytes.");
println("As string: ${String.fromBytes(bytes)}")
case Err(err) => println("Read error: ${err}")
}
}
ファイルへの追記
append を使うと、既存のファイルを上書きせずにテキストを追記できます。ファイルが存在しない場合は新規に作成されます:
use Fs.FileRead
use Fs.FileWrite
def main(): Unit \ { FileRead, FileWrite, IO } =
match FileWrite.write(str = "Line 1\n", "log.txt") {
case Err(err) => println("Write error: ${err}")
case Ok(_) =>
match FileWrite.append(str = "Line 2\n", "log.txt") {
case Err(err) => println("Append error: ${err}")
case Ok(_) =>
match FileRead.read("log.txt") {
case Ok(content) => println(content)
case Err(err) => println("Read error: ${err}")
}
}
}
appendLines や appendBytes といったバリエーションもあります。
ディレクトリの一覧取得
DirList.list を使うと、ディレクトリ内のすべてのファイルとディレクトリの名前を取得できます:
use Fs.DirList
def main(): Unit \ { DirList, IO } =
match DirList.list(".") {
case Ok(entries) =>
foreach (entry <- entries) {
println(entry)
}
case Err(err) => println("Error: ${err}")
}
Glob によるファイル検索
Glob.glob を使うと、基点となるディレクトリ以下からグロブパターンにマッチするファイルを検索できます:
use Fs.Glob
def main(): Unit \ { Glob, IO } =
match Glob.glob(".", "*.flix") {
case Ok(files) =>
foreach (file <- files) {
println(file)
}
case Err(err) => println("Error: ${err}")
}
ファイルのメタデータ
FileStat エフェクトを使うと、ファイルのメタデータ(存在の有無、種別、サイズ、パーミッション、タイムスタンプ)を調べることができます:
use Fs.FileStat
use Fs.FileWrite
def main(): Unit \ { FileStat, FileWrite, IO } =
let file = "example.txt";
match FileWrite.write(str = "Hello!", file) {
case Err(err) => println("Write error: ${err}")
case Ok(_) =>
match FileStat.exists(file) {
case Ok(b) => println("Exists: ${b}")
case Err(err) => println("Error: ${err}")
};
match FileStat.isRegularFile(file) {
case Ok(b) => println("Is regular file: ${b}")
case Err(err) => println("Error: ${err}")
};
match FileStat.isDirectory(file) {
case Ok(b) => println("Is directory: ${b}")
case Err(err) => println("Error: ${err}")
};
match FileStat.size(file) {
case Ok(s) => println("Size: ${s}")
case Err(err) => println("Error: ${err}")
};
match FileStat.modificationTime(file) {
case Ok(t) => println("Modification time: ${t}ms")
case Err(err) => println("Error: ${err}")
}
}
FileStat エフェクトは、4 つのサブエフェクトを組み合わせたものです:
| サブエフェクト | 操作 |
|---|---|
FileTest | exists、isDirectory、isRegularFile、isSymbolicLink |
FilePermission | isReadable、isWritable、isExecutable |
FileTime | accessTime、creationTime、modificationTime |
FileSize | size |
コピー・移動・削除
FileWrite エフェクトを使って、ファイルのコピー、移動、削除を行うこともできます:
use Fs.FileWrite
def main(): Unit \ { FileWrite, IO } =
match FileWrite.write(str = "Hello!", "original.txt") {
case Err(err) => println("Write error: ${err}")
case Ok(_) =>
// オプションなしでコピー。
match FileWrite.copy(src = "original.txt", "copy.txt") {
case Ok(_) => println("Copied.")
case Err(err) => println("Copy error: ${err}")
};
// オプションなしで移動(リネーム)。
match FileWrite.move(src = "copy.txt", "renamed.txt") {
case Ok(_) => println("Moved.")
case Err(err) => println("Move error: ${err}")
};
// 削除。
match FileWrite.delete("renamed.txt") {
case Ok(_) => println("Deleted.")
case Err(err) => println("Delete error: ${err}")
}
}
copy と move は、オプション集合を受け取る copyWith と moveWith を簡便に使うためのラッパーです:
CopyOption.CopyAttributes— ファイル属性を保持しますCopyOption.ReplaceExisting— コピー先が存在する場合は上書きしますMoveOption.AtomicMove— アトミックなリネームを行いますMoveOption.ReplaceExisting— 移動先が存在する場合は上書きします
ディレクトリの作成
単一のディレクトリを作成するには mkDir を、ディレクトリとその親ディレクトリすべてを作成するには mkDirs を、一時ディレクトリを作成するには mkTempDir を使うことができます:
use Fs.FileWrite
def main(): Unit \ { FileWrite, IO } =
match FileWrite.mkDirs("a/b/c") {
case Ok(_) => println("Created a/b/c.")
case Err(err) => println("Error: ${err}")
};
match FileWrite.mkTempDir("flix-") {
case Ok(path) => println("Temp dir: ${path}")
case Err(err) => println("Error: ${err}")
}
FileSystem エフェクト
FileSystem エフェクトは、すべてのファイルシステム操作を単一のエフェクトにまとめたものです。FileStat、FileRead、FileWrite、DirList、Glob のすべての操作を含みます。複数のカテゴリの操作をまとめて使いたい場合には FileSystem を使うとよいでしょう:
use Fs.FileSystem
def main(): Unit \ { FileSystem, IO } =
match FileSystem.write(str = "Hello!", "greeting.txt") {
case Err(err) => println("Write error: ${err}")
case Ok(_) =>
match FileSystem.read("greeting.txt") {
case Ok(content) => println("Read: ${content}")
case Err(err) => println("Read error: ${err}")
}
}
ミドルウェア
ミドルウェア(Middleware)とは、ファイルシステム操作をインターセプトするエフェクトハンドラのことです。run { ... } with FileSystem.<middleware>(または対応するサブエフェクトのモジュール)を使って適用し、複数の with 節を積み重ねることで合成できます。
ベースディレクトリ
withBaseDir は、相対パスをベースディレクトリを基準に解決します。絶対パスはそのまま通過します:
use Fs.FileSystem
def main(): Unit \ { FileSystem, IO } =
match FileSystem.mkDirs("/tmp/flix-basedir") {
case Err(err) => println("Setup error: ${err}")
case Ok(_) =>
run {
match FileSystem.write(str = "Hello", "greeting.txt") {
case Err(err) => println("Write error: ${err}")
case Ok(_) =>
match FileSystem.read("greeting.txt") {
case Ok(content) => println("Read: ${content}")
case Err(err) => println("Read error: ${err}")
}
}
} with FileSystem.withBaseDir("/tmp/flix-basedir")
}
Chroot
withChroot は、すべての操作をディレクトリのサブツリー内に制限します。chroot の外側のパスを対象とする操作は PermissionDenied エラーで失敗します:
use Fs.FileSystem
def main(): Unit \ { FileSystem, IO } =
match FileSystem.mkDirs("/tmp/flix-chroot") {
case Err(err) => println("Setup error: ${err}")
case Ok(_) =>
run {
match FileSystem.write(str = "Hello", "/tmp/flix-chroot/data.txt") {
case Ok(_) => println("Write inside chroot succeeded")
case Err(err) => println("Error: ${err}")
};
match FileSystem.read("/etc/hostname") {
case Ok(_) => println("Unexpected: read outside chroot succeeded")
case Err(err) => println("Read outside chroot blocked: ${err}")
}
} with FileSystem.withChroot("/tmp/flix-chroot")
}
ロギング
withLogging は、各ファイルシステム操作を Logger エフェクト経由でログに記録します。main の型シグネチャに Logger が現れる点に注意してください:
use Fs.FileSystem
def main(): Unit \ { FileSystem, Logger, IO } =
run {
match FileSystem.write(str = "Hello, Flix!", "greeting.txt") {
case Err(err) => println("Write error: ${err}")
case Ok(_) =>
match FileSystem.read("greeting.txt") {
case Ok(content) => println(content)
case Err(err) => println("Read error: ${err}")
}
}
} with FileSystem.withLogging
読み取り専用
withReadOnly は、すべての書き込み操作を PermissionDenied エラーでブロックします。読み込みおよびメタデータ取得(stat)の操作は通常どおり通過します:
use Fs.FileSystem
def main(): Unit \ { FileSystem, IO } =
run {
match FileSystem.write(str = "This will fail", "blocked.txt") {
case Ok(_) => println("Unexpected: write succeeded")
case Err(err) => println("Write blocked: ${err}")
};
match FileSystem.exists("blocked.txt") {
case Ok(b) => println("Exists: ${b}")
case Err(err) => println("Error: ${err}")
}
} with FileSystem.withReadOnly
ドライラン
withDryRun は、書き込み操作を実際には実行せず、Logger エフェクト経由でログに記録します。読み込み操作は通常どおり実行されます:
use Fs.FileSystem
def main(): Unit \ { FileSystem, Logger, IO } =
run {
match FileSystem.write(str = "This won't be written", "phantom.txt") {
case Err(err) => println("Write error: ${err}")
case Ok(_) =>
match FileSystem.exists("phantom.txt") {
case Ok(b) => println("Exists: ${b}")
case Err(err) => println("Error: ${err}")
}
}
} with FileSystem.withDryRun
アトミック書き込み
withAtomicWrite は、まずデータを一時ファイルに書き込み、その後アトミックなリネームによって目的の場所へ配置します。これにより、失敗時に書き込みが中途半端な状態になることを防ぎます。影響を受けるのは write、writeLines、writeBytes のみで、追記やその他の操作はそのまま通過します:
use Fs.FileSystem
def main(): Unit \ { FileSystem, IO } =
run {
match FileSystem.write(str = "Atomic content", "output.txt") {
case Ok(_) => println("Atomic write succeeded.")
case Err(err) => println("Write error: ${err}")
}
} with FileSystem.withAtomicWrite
バックアップ
withBackup は、既存のファイルを上書きする前にバックアップコピーを作成します。破壊的な操作(write、writeLines、writeBytes、truncate、delete、copyWith、moveWith)を行うたびに、既存のファイルが file + suffix へコピーされます:
use Fs.FileSystem
def main(): Unit \ { FileSystem, IO } =
match FileSystem.write(str = "Original content", "data.txt") {
case Err(err) => println("Setup error: ${err}")
case Ok(_) =>
run {
match FileSystem.write(str = "New content", "data.txt") {
case Ok(_) => println("Write succeeded; backup saved to data.txt.bak")
case Err(err) => println("Write error: ${err}")
}
} with FileSystem.withBackup(".bak")
}
親ディレクトリの自動作成
withMkParentDirs は、書き込みおよび追記の操作を行う前に、親ディレクトリを自動的に作成します。親ディレクトリがすでに存在する場合は何もしません:
use Fs.FileSystem
def main(): Unit \ { FileSystem, IO } =
run {
match FileSystem.write(str = "Hello", "deep/nested/path/greeting.txt") {
case Ok(_) => println("Write succeeded (parents created).")
case Err(err) => println("Write error: ${err}")
}
} with FileSystem.withMkParentDirs
競合チェック
withConflictCheck は、ファイルの変更時刻を追跡し、前回の操作以降にファイルが外部から変更されていた場合に書き込みを拒否します。これにより、外部プロセスとの書き込み同士の競合を検出できます:
use Fs.FileSystem
def main(): Unit \ { FileSystem, IO } =
run {
match FileSystem.write(str = "First write", "shared.txt") {
case Err(err) => println("Error: ${err}")
case Ok(_) =>
match FileSystem.write(str = "Second write", "shared.txt") {
case Ok(_) => println("No conflict detected.")
case Err(err) => println("Conflict: ${err}")
}
}
} with FileSystem.withConflictCheck
転送量の制限
withTransferLimit は、ペイロードが最大サイズを超える読み込みまたは書き込みの操作を拒否します:
use Fs.FileSystem
use Fs.Size
def main(): Unit \ { FileSystem, IO } =
run {
match FileSystem.write(str = "Small", "ok.txt") {
case Ok(_) => println("Small write succeeded.")
case Err(err) => println("Error: ${err}")
}
} with FileSystem.withTransferLimit(Size.megaBytes(10))
アクセス制御
Flix は、アクセス可能なパスを制限するためのミドルウェアを提供しています。次のものが利用できます:
withAllowList(dirs)— 列挙したディレクトリ内のパスのみを許可しますwithDenyList(dirs)— 列挙したディレクトリ内のパスをブロックしますwithAllowGlob(patterns)— 少なくとも 1 つのパターンにマッチするパスのみを許可しますwithDenyGlob(patterns)— いずれかのパターンにマッチするパスをブロックします
use Fs.FileSystem
def main(): Unit \ { FileSystem, IO } =
run {
match FileSystem.read("/tmp/safe/data.txt") {
case Ok(content) => println(content)
case Err(err) => println("Error: ${err}")
}
} with FileSystem.withAllowList(Nel.of("/tmp/safe"))
インメモリファイルシステム
withInMemoryFS ハンドラは、実際のファイルシステムを完全にメモリ上の実装で置き換えます。ファイルシステムは空の状態から始まり、書き込まれていないファイルを読み込むと NotFound が返ります。実際のファイルシステムへのアクセスは一切発生しません:
use Fs.FileSystem
use Time.Clock
def main(): Unit \ { Clock, IO } =
run {
let result = forM (
_ <- FileSystem.mkDirs("/data");
_ <- FileSystem.write(str = "Hello", "/data/hello.txt");
_ <- FileSystem.write(str = "World", "/data/world.txt");
entries <- FileSystem.list("/data");
content <- FileSystem.read("/data/hello.txt");
_ <- FileSystem.delete("/data/hello.txt");
exists <- FileSystem.exists("/data/hello.txt")
) yield (entries, content, exists);
match result {
case Err(err) => println("Error: ${err}")
case Ok((entries, content, exists)) =>
println("Files in /data:");
foreach (entry <- entries) {
println(" ${entry}")
};
println("Content: ${content}");
println("Exists after delete: ${exists}")
}
} with FileSystem.withInMemoryFS
withInMemoryFS は(ファイルのタイムスタンプのために)Clock エフェクトを必要としますが、FileSystem を完全に処理するため、エフェクトシグネチャからは FileSystem が取り除かれる点に注意してください。
メモリオーバーレイ
withMemoryOverlay ハンドラは、実際のファイルシステムの上に、メモリ上の書き込み可能なストアを重ねます。書き込みはメモリ上に記録され、以降の読み込みでは書き込まれたデータが見えますが、実際のファイルシステムが変更されることはありません。オーバーレイに存在しないファイルの読み込みは、実際のファイルシステムへとフォールスルーします:
use Fs.FileSystem
def main(): Unit \ { FileSystem, IO } =
run {
// この書き込みはメモリ上に記録され、ディスクには書き込まれません。
match FileSystem.write(str = "In-memory only", "virtual.txt") {
case Err(err) => println("Error: ${err}")
case Ok(_) =>
match FileSystem.read("virtual.txt") {
case Ok(content) => println("Read from overlay: ${content}")
case Err(err) => println("Error: ${err}")
}
}
} with FileSystem.withMemoryOverlay
ミドルウェアの合成
ミドルウェアは、with 節を積み重ねることで合成できます。最も内側のハンドラ(最初に書かれたもの)が元の操作をインターセプトし、続いてひとつ外側のハンドラへと委譲します。次の例では、ベースディレクトリ、親ディレクトリの自動作成、バックアップ、アトミック書き込み、競合チェック、ロギングを積み重ねています:
use Fs.FileSystem
def main(): Unit \ { FileSystem, Logger, IO } =
run {
match FileSystem.write(str = "Hello, Flix!", "data/greeting.txt") {
case Err(err) => println("Write error: ${err}")
case Ok(_) =>
match FileSystem.read("data/greeting.txt") {
case Ok(content) => println("Read: ${content}")
case Err(err) => println("Read error: ${err}")
}
}
} with FileSystem.withBaseDir("/tmp/flix-example")
with FileSystem.withMkParentDirs
with FileSystem.withConflictCheck
with FileSystem.withBackup(".bak")
with FileSystem.withAtomicWrite
with FileSystem.withLogging
FileSystem と Logger の両エフェクトはデフォルトハンドラを持つため、main の型シグネチャに現れると自動的に処理されます。
注意:
with節の順序は重要です。最も外側のハンドラ(最後に書かれたもの)は、すべての内側のハンドラを包み込みます。上の例ではwithLoggingが最も外側にあるため、競合チェックによるリトライやアトミック書き込みの一時ファイルも含めて、すべてのファイルシステム操作を観測します。ミドルウェアを合成する際には、どの層がどの操作を観測すべきかを考えてください。
ミドルウェア一覧
次の表は、どのミドルウェアがどのエフェクトで利用できるかを示しています(表全体を見るには右へスクロールしてください):
| ミドルウェア | FileTest | FilePermission | FileTime | FileStat | FileRead | DirList | Glob | FileWrite | FileSystem |
|---|---|---|---|---|---|---|---|---|---|
withLogging | x | x | x | x | x | x | x | x | x |
withBaseDir | x | x | x | x | x | x | x | x | x |
withChroot | x | x | x | x | x | x | x | x | x |
withAllowList | x | x | x | x | x | x | x | x | x |
withDenyList | x | x | x | x | x | x | x | x | x |
withAllowGlob | x | x | x | x | x | x | x | x | x |
withDenyGlob | x | x | x | x | x | x | x | x | x |
withFollowLinks | x | x | x | x | x | x | x | x | x |
withTransferLimit | x | x | x | ||||||
withChecksum | x | x | x | ||||||
withDryRun | x | x | |||||||
withReadOnly | x | x | |||||||
withAtomicWrite | x | x | |||||||
withBackup | x | x | |||||||
withConflictCheck | x | x | |||||||
withMkParentDirs | x | x | |||||||
withSizeRotation | x | x | |||||||
withMemoryOverlay | x | ||||||||
withInMemoryFS | x |
エフェクト階層
Flix のファイルシステムエフェクトは、エフェクト階層(Effect hierarchy)を成しています。最上位には全 29 操作を備えた FileSystem があります。その下には関連する操作をグループ化した中間のエフェクトがあり、最下層には個々の操作に対応するリーフエフェクトがあります:
FileSystem (29 ops — unified root)
├── FileStat (11 ops)
│ ├── FileTest (4 ops: exists, isDirectory, isRegularFile, isSymbolicLink)
│ ├── FilePermission (3 ops: isReadable, isWritable, isExecutable)
│ ├── FileTime (3 ops: accessTime, creationTime, modificationTime)
│ └── FileSize (1 op: size)
├── FileRead (3 ops: read, readLines, readBytes)
├── DirList (1 op: list)
├── Glob (1 op: glob)
└── FileWrite (13 ops: write, append, delete, copy, move, mkdir, etc.)
階層のどのレベルでも利用できます。例えば、exists だけが必要なら FileExists のようなリーフエフェクトを、ファイルの読み込みが必要なら FileRead を、すべてが必要なら FileSystem を使うことができます。
リーフエフェクトは、runWith ハンドラを使って親エフェクトへと実行を委ねることができます。例えば、FileExists を FileTest へ、ReadFile を FileRead へと委ねることができます:
use Fs.FileExists
use Fs.FileRead
use Fs.FileTest
use Fs.ReadFile
def main(): Unit \ { FileRead, FileTest, IO } =
run {
safeRead("example.txt")
} with FileExists.runWithFileTest
with ReadFile.runWithFileRead
def safeRead(file: String): Unit \ { FileExists, ReadFile, IO } =
match FileExists.exists(file) {
case Err(err) => println("Error: ${err}")
case Ok(false) => println("File does not exist")
case Ok(true) =>
match ReadFile.read(file) {
case Ok(content) => println(content)
case Err(err) => println("Read error: ${err}")
}
}
Http と Https
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、HTTP リクエストを送信するためのライブラリエフェクトとして Http と Https を提供しています。どちらのエフェクトにもデフォルトハンドラがあるため、main で明示的に runWithIO を呼び出す必要はありません。主なモジュールは次のとおりです:
Net.Http—Httpエフェクトと便利な関数群(get、post、sendなど)Net.Https—Httpsエフェクト(https://の URL を強制します)Net.HttpRequest— リクエストを構築するためのフルーエント APINet.HttpResponse— レスポンスを調べるためのアクセサ群Net.Retry— ミドルウェアと組み合わせて使うリトライ戦略
GET リクエストの送信
HTTP リクエストを送る最も簡単な方法は Http.get です。これは Result[IoError, HttpResponse] を返すため、Ok と Err でパターンマッチします:
use Net.Http
use Net.HttpResponse
def main(): Unit \ { Http, IO } =
match Http.get("http://example.com/") {
case Ok(resp) => println(HttpResponse.body(resp))
case Err(err) => println(err)
}
Http エフェクトは http:// と https:// の両方の URL に対応しています。このエフェクトは、main の型シグネチャに IO と並んで現れます。
レスポンスの調査
HttpResponse モジュールは、ステータスコード、ヘッダ、ボディの内容を取得するアクセサを提供します。expect 関数は 2xx 以外のステータスコードに対して Err を返すため、失敗したレスポンスをエラーとして扱いたい場合に便利です:
use Net.Http
use Net.HttpResponse
def main(): Unit \ { Http, IO } =
match Http.get("https://flix.dev/") {
case Ok(resp) =>
println("Status: ${HttpResponse.status(resp)}");
println("Is success? ${HttpResponse.isSuccess(resp)}");
println("Is client err? ${HttpResponse.isClientError(resp)}");
println("Is server err? ${HttpResponse.isServerError(resp)}");
println("Content-Type: ${HttpResponse.contentType(resp)}");
println("Content-Length: ${HttpResponse.contentLength(resp)}");
println("Server header: ${HttpResponse.headerValue("server", resp)}");
match HttpResponse.expect(resp) {
case Ok(r) =>
println("Body length: ${String.length(HttpResponse.body(r))}")
case Err(e) =>
println("Unexpected status: ${e}")
}
case Err(err) =>
println("Error: ${err}")
}
POST リクエストと JSON
ボディ付きの POST リクエストを作るには HttpRequest.post を使います。リクエストビルダーは、よく使われるヘッダを設定するための withContentType と withAccept に対応しています。構築したリクエストは Http.send で送信します:
use Net.Http
use Net.HttpRequest
use Net.HttpResponse
def main(): Unit \ { Http, IO } =
let body = "{\"name\": \"Asterix\", \"age\": 35}";
let req = HttpRequest.post("https://flix.dev/api/users", body)
|> HttpRequest.withContentType("application/json")
|> HttpRequest.withAccept("application/json");
match Http.send(req) {
case Ok(resp) =>
println("Status: ${HttpResponse.status(resp)}");
println("Body: ${HttpResponse.body(resp)}")
case Err(err) =>
println("Error: ${err}")
}
リクエストの構築
HttpRequest モジュールは、リクエストを構築するためのフルーエント API を提供します。|> の呼び出しをパイプラインでつなぐことで、クエリパラメータ、認証トークン、カスタムヘッダ、タイムアウトを追加できます:
use Net.Http
use Net.HttpRequest
use Net.HttpResponse
use Time.Duration.seconds
def main(): Unit \ { Http, IO } =
let req =
HttpRequest.get("https://flix.dev/api/search")
|> HttpRequest.withQueryParam("q", "flix programming language")
|> HttpRequest.withQueryParams(Map#{
"page" => "1", "per_page" => "25", "sort" => "relevance"
})
|> HttpRequest.withBearerToken("ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ")
|> HttpRequest.withHeader("User-Agent", "MyApp/1.0")
|> HttpRequest.withTimeout(seconds(5));
match Http.send(req) {
case Ok(resp) =>
println("Status: ${HttpResponse.status(resp)}");
println("Body: ${String.takeLeft(80, HttpResponse.body(resp))}")
case Err(err) =>
println("Error: ${err}")
}
コンストラクタはすべての HTTP メソッドに対して用意されています:HttpRequest.get、HttpRequest.post、HttpRequest.put、HttpRequest.patch、HttpRequest.delete です。
Https エフェクト
Https エフェクトは Http と同じように動作しますが、すべての URL が https:// スキームを使うことを強制します。http:// の URL を渡すと、リクエストは拒否されます。安全な接続のみが行われることを型システムに保証させたい場合は、Https を使ってください:
use Net.Https
use Net.HttpResponse
def main(): Unit \ { Https, IO } =
match Https.get("https://example.com/") {
case Ok(resp) => println(HttpResponse.body(resp))
case Err(err) => println(err)
}
なお、Http はすでに https:// の URL に対応しています。Https は、エフェクトシグネチャによってセキュリティの保証を明示したい場合のためのものです。
ミドルウェア
ミドルウェア(Middleware)は、Http(または Https)のリクエストに割り込むエフェクトハンドラです。run { ... } with Http.<ミドルウェア> の形で適用し、複数の with 句を積み重ねることで合成できます。
ベース URL
withBaseUrl は、相対パスの先頭にベース URL を付加します。絶対 URL(:// を含むもの)はベースを迂回し、そのまま送信されます:
use Net.Http
use Net.HttpResponse
def main(): Unit \ { Http, IO } =
run {
match Http.get("/api/users") {
case Ok(resp) => println("/api/users -> ${HttpResponse.status(resp)}")
case Err(err) => println("/api/users -> ${err}")
};
match Http.get("/api/posts") {
case Ok(resp) => println("/api/posts -> ${HttpResponse.status(resp)}")
case Err(err) => println("/api/posts -> ${err}")
};
// 絶対 URL はベースを迂回します。
match Http.get("https://flix.dev/other") {
case Ok(resp) => println("absolute -> ${HttpResponse.status(resp)}")
case Err(err) => println("absolute -> ${err}")
}
} with Http.withBaseUrl("https://flix.dev")
デフォルトヘッダ
withDefaultHeaders は、すべてのリクエストにヘッダを注入します。リクエストにすでに存在するヘッダは上書きされません:
use Net.Http
use Net.HttpResponse
def main(): Unit \ { Http, IO } =
let defaults = Map#{
"Accept" => List#{"application/json"},
"Authorization" => List#{"Bearer ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789"}
};
run {
match Http.get("https://flix.dev/api/users") {
case Ok(resp) => println("Status: ${HttpResponse.status(resp)}")
case Err(err) => println("Error: ${err}")
}
} with Http.withDefaultHeaders(defaults)
ロギング
withLogging は、Logger エフェクトを通じて各リクエストとレスポンスをログに記録します。Logger が main の型シグネチャに現れている点に注目してください:
use Net.Http
use Net.HttpResponse
def main(): Unit \ { Http, Logger, IO } =
run {
match Http.get("https://flix.dev/") {
case Ok(resp) => println("Status: ${HttpResponse.status(resp)}")
case Err(err) => println("Error: ${err}")
};
match Http.get("https://notfound.flix.dev/") {
case Ok(resp) => println("Status: ${HttpResponse.status(resp)}")
case Err(err) => println("Error: ${err}")
}
} with Http.withLogging
リトライ
withRetry は、Net.Retry モジュールの戦略を使った自動リトライを追加します。各戦略は、試行回数と結果(トランスポートエラーまたは HTTP エラー)に基づいてリトライするかどうかを決定する関数です:
use Net.Http
use Net.Retry
use Net.HttpResponse
use Time.Duration.milliseconds
def main(): Unit \ { Http, IO } =
// 線形: 100ms の遅延で最大 3 回までリトライします。
run {
println("--- Linear retry ---");
match Http.get("https://notfound.flix.dev/") {
case Ok(resp) => println("Status: ${HttpResponse.status(resp)}")
case Err(err) => println("Error: ${err}")
}
} with Http.withRetry(Retry.linear(maxRetries = 3, delay = milliseconds(100)));
// 指数: 100ms を基準遅延として最大 3 回までリトライします。
run {
println("--- Exponential retry ---");
match Http.get("https://notfound.flix.dev/") {
case Ok(resp) => println("Status: ${HttpResponse.status(resp)}")
case Err(err) => println("Error: ${err}")
}
} with Http.withRetry(Retry.exponential(maxRetries = 3, baseDelay = milliseconds(100)));
// トランスポートのみ: HTTP エラーではなく、接続失敗の場合のみリトライします。
run {
println("--- Transport-only retry ---");
match Http.get("https://notfound.flix.dev/") {
case Ok(resp) => println("Status: ${HttpResponse.status(resp)}")
case Err(err) => println("Error: ${err}")
}
} with Http.withRetry(Retry.retryTransportOnly(maxRetries = 2, delay = milliseconds(100)))
利用できる戦略は次のとおりです:
Retry.linear— リトライ間の遅延が一定Retry.exponential— リトライごとに遅延が倍増Retry.retryAfter— 429/503 レスポンスのRetry-Afterレスポンスヘッダに従うRetry.retryTransportOnly— HTTP エラーではなく、接続失敗の場合のみリトライ
戦略は Retry.withJitter でラップして遅延にランダムなジッターを加えたり、Retry.withDeadline でラップして全リトライを通した合計時間の上限を設けたりできます。
サーキットブレーカー
withCircuitBreaker は、連鎖的な障害から保護します。failureThreshold 回連続で失敗(トランスポートエラーまたは 5xx レスポンス)すると、サーキット(Circuit)が開き、cooldown の期間中はリクエストを即座に拒否します。リクエストが成功すると失敗カウンタはリセットされます。Clock エフェクトが型シグネチャに現れている点に注目してください:
use Net.Http
use Net.HttpResponse
use Time.Clock
use Time.Duration.seconds
def main(): Unit \ { Clock, Http, IO } =
run {
let urls = List#{"/a", "/b", "/c", "/d", "/e", "/f", "/g", "/h"};
foreach (url <- urls) {
match Http.get(url) {
case Ok(resp) => println("${url} -> ${HttpResponse.status(resp)}")
case Err(err) => println("${url} -> ${err}")
}
}
} with Http.withCircuitBreaker(failureThreshold = 3, cooldown = seconds(5))
with Http.withBaseUrl("https://notfound.flix.dev")
レート制限
Flix は 3 種類のレート制限戦略を提供しています:
withMinInterval— 連続するリクエストの間に一定の最小遅延を強制しますwithTokenBucket— 最初にバースト的なリクエストを許可し、その後は一定のレートに制限しますwithSlidingWindow— 任意のローリングタイムウィンドウ内で最大 N 件のリクエストを許可します
3 つとも Clock エフェクトを必要とします:
use Net.Http
use Net.HttpResponse
use Time.Clock
use Time.Duration.{milliseconds, seconds}
def main(): Unit \ { Clock, Http, IO } =
// 最小間隔: 連続するリクエストの間に少なくとも 100ms を空けます。
run {
println("--- Min interval ---");
let urls = List#{"/a", "/b", "/c", "/d"};
foreach (url <- urls) {
match Http.get(url) {
case Ok(resp) => println("${url} -> ${HttpResponse.status(resp)}")
case Err(err) => println("${url} -> ${err}")
}
}
} with Http.withMinInterval(interval = milliseconds(100))
with Http.withBaseUrl("https://flix.dev");
// トークンバケット: バーストで 2 件、その後は 100ms あたり 1 リクエストです。
run {
println("--- Token bucket ---");
let urls = List#{"/a", "/b", "/c", "/d"};
foreach (url <- urls) {
match Http.get(url) {
case Ok(resp) => println("${url} -> ${HttpResponse.status(resp)}")
case Err(err) => println("${url} -> ${err}")
}
}
} with Http.withTokenBucket(burstSize = 2, interval = milliseconds(100))
with Http.withBaseUrl("https://flix.dev");
// スライディングウィンドウ: 1000ms のウィンドウあたり最大 2 リクエストです。
run {
println("--- Sliding window ---");
let urls = List#{"/a", "/b", "/c", "/d"};
foreach (url <- urls) {
match Http.get(url) {
case Ok(resp) => println("${url} -> ${HttpResponse.status(resp)}")
case Err(err) => println("${url} -> ${err}")
}
}
} with Http.withSlidingWindow(maxRequests = 2, window = seconds(1))
with Http.withBaseUrl("https://flix.dev")
ミドルウェアの合成
ミドルウェアは with 句を積み重ねることで合成できます。各 with はその手前のブロックを包み込むため、最も外側のハンドラが最初に実行されます。以下は、ベース URL、デフォルトヘッダ、リトライ、サーキットブレーカー、レート制限、ロギングを積み重ねた例です:
use Net.Http
use Net.Retry
use Net.HttpResponse
use Time.Clock
use Time.Duration.{milliseconds, seconds}
def main(): Unit \ { Clock, Http, Logger, IO } =
let defaultHeaders = Map#{
"Accept" => List#{"application/json"},
"Authorization" => List#{"Bearer tok123"}
};
run {
let urls = List#{"/api/users", "/api/posts"};
foreach (url <- urls) {
match Http.get(url) {
case Ok(resp) => println("${url} -> ${HttpResponse.status(resp)}")
case Err(err) => println("${url} -> ${err}")
}
};
match Http.get("https://notfound.flix.dev/") {
case Ok(resp) => println("notfound -> ${HttpResponse.status(resp)}")
case Err(err) => println("notfound -> ${err}")
}
} with Http.withBaseUrl("https://flix.dev")
with Http.withDefaultHeaders(defaultHeaders)
with Http.withRetry(Retry.linear(maxRetries = 2, delay = milliseconds(100)))
with Http.withCircuitBreaker(failureThreshold = 3, cooldown = seconds(5))
with Http.withSlidingWindow(maxRequests = 2, window = seconds(1))
with Http.withLogging
Http、Logger、Clock の各エフェクトにはいずれもデフォルトハンドラがあるため、main の型シグネチャに現れると自動的に処理されます。
注意:
with句の順序は重要です。最も外側のハンドラ(最後に書かれたもの)が、内側のすべてのハンドラを包み込みます。上の例ではwithLoggingが最も外側にあるため、リトライやサーキットブレーカーによる試行も含めて、すべての HTTP リクエストを観測します。もしwithLoggingをwithRetryより前に移動すると、リトライは観測されず、元のリクエストだけが観測されます。ミドルウェアを合成するときは、どの層がどのリクエストを観測すべきかを考えてください。
Logger
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、構造化ログ(Structured logging)のためのライブラリエフェクトとして Logger を提供しています。Logger エフェクトにはデフォルトハンドラがあるため、main の中で明示的に runWithIO を呼び出す必要はありません。鍵となるモジュールは Logger です。
Logger エフェクト
Logger エフェクトは、指定された重大度(Severity)でメッセージをログに記録する、単一の操作を持ちます。
pub eff Logger {
/// 指定されたメッセージ `m` を、指定された重大度 `s` でログに記録します。
def log(s: Severity, m: RichString): Unit
}
Severity enum は、低いものから高いものまで、5 つのレベルを定義しています。
pub enum Severity with Eq, Order, ToString {
case Trace
case Debug
case Info
case Warn
case Fatal
}
Logger モジュール
Logger モジュールは便利な関数を提供しています。
mod Logger {
/// メッセージ `m` を Trace レベルでログに記録します。
def trace(m: a): Unit \ Logger with Formattable[a]
/// メッセージ `m` を Debug レベルでログに記録します。
def debug(m: a): Unit \ Logger with Formattable[a]
/// メッセージ `m` を Info レベルでログに記録します。
def info(m: a): Unit \ Logger with Formattable[a]
/// メッセージ `m` を Warn レベルでログに記録します。
def warn(m: a): Unit \ Logger with Formattable[a]
/// メッセージ `m` を Fatal レベルでログに記録します。
def fatal(m: a): Unit \ Logger with Formattable[a]
注意: ログ記録用の関数は、
Formattableトレイトを実装している任意の型を受け取ります。ほとんどの標準的な型(String、Int32、Boolなど)はFormattableを実装しているため、通常の値をそのままログに記録できます。このトレイトは値をRichStringに変換します。RichStringは、スタイル付きのターミナル出力(色、太字など)をサポートしています。
メッセージのログ記録
これらの便利な関数は、Formattable を実装している任意の値を受け取ります。
def main(): Unit \ { Logger } =
Logger.info("Application started");
Logger.debug("Loading configuration...");
Logger.warn("Cache size exceeds threshold");
Logger.fatal("Unrecoverable error")
デフォルトハンドラは、各メッセージを色付きの重大度プレフィックス付きで標準出力に出力します。例: [Info] Application started
Process
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、OS プロセスの起動と管理のためのライブラリエフェクト(library effect)として Process を提供しています。Process エフェクトにはデフォルトハンドラがあるため、main の中で明示的に runWithIO を呼び出す必要はありません。中心となるモジュールは Sys.Process です。
Process エフェクト
Process エフェクトは、プロセスの起動、その入出力ストリームへのアクセス、そして終了の待機をサポートしています。
pub eff Process {
/// コマンド `cmd` を、引数 `args`、作業ディレクトリ `cwd`、環境変数 `env` で実行します。
def execWithCwdAndEnv(cmd: String, args: List[String],
cwd: Option[String], env: Map[String, String]):
Result[IoError, ProcessHandle]
/// プロセス `ph` の終了値を返します。
def exitValue(ph: ProcessHandle): Result[IoError, Int32]
/// プロセス `ph` が生存しているかどうかを返します。
def isAlive(ph: ProcessHandle): Result[IoError, Bool]
/// プロセス `ph` の PID を返します。
def pid(ph: ProcessHandle): Result[IoError, Int64]
/// プロセス `ph` の標準入力ストリームを返します。
def stdin(ph: ProcessHandle): Result[IoError, StdIn]
/// プロセス `ph` の標準出力ストリームを返します。
def stdout(ph: ProcessHandle): Result[IoError, StdOut]
/// プロセス `ph` の標準エラーストリームを返します。
def stderr(ph: ProcessHandle): Result[IoError, StdErr]
/// プロセス `ph` を停止します。
def stop(ph: ProcessHandle): Result[IoError, Unit]
/// プロセス `ph` の終了を待機し、その終了値を返します。
def waitFor(ph: ProcessHandle): Result[IoError, Int32]
/// プロセス `ph` の終了を最大 `time`(単位は `tUnit`)だけ待機します。
/// プロセスが終了した場合は `true` を、タイムアウトした場合は `false` を返します。
def waitForTimeout(ph: ProcessHandle, time: Int64, tUnit: TimeUnit):
Result[IoError, Bool]
}
Process モジュール
Process モジュールは、Process エフェクトの上に構築された便利な関数を提供しています。
mod Process {
/// コマンド `cmd` を引数 `args` で実行します。
def exec(cmd: String, args: List[String]):
Result[IoError, ProcessHandle] \ Process
/// `cmd` を、引数 `args` と作業ディレクトリ `cwd` で実行します。
def execWithCwd(cmd: String, args: List[String], cwd: Option[String]):
Result[IoError, ProcessHandle] \ Process
/// `cmd` を、引数 `args` と環境変数 `env` で実行します。
def execWithEnv(cmd: String, args: List[String], env: Map[String, String]):
Result[IoError, ProcessHandle] \ Process
}
コマンドの実行
OS プロセスを起動する最も簡単な方法は Process.exec を使うことです。この関数はコマンドと引数のリストを受け取り、Result[IoError, ProcessHandle] を返します。
use Sys.Process
def main(): Unit \ { Process, IO } =
match Process.exec("java", "-version" :: Nil) {
case Result.Ok(_) => println("Process started successfully.")
case Result.Err(err) => println("Unable to execute process: ${err}")
}
プロセス出力の読み取り
プロセスを起動した後、Process.stdout と Process.stderr でその出力ストリームにアクセスできます。返される StdOut 型と StdErr 型は Readable を実装しているため、そこからバイト列を読み取ることができます。
use Sys.Process
def main(): Unit \ { Process, IO } = region rc {
match Process.exec("java", "-version" :: Nil) {
case Result.Err(err) => println("exec failed: ${err}")
case Result.Ok(ph) =>
match Process.stderr(ph) {
case Result.Err(err) => println("stderr failed: ${err}")
case Result.Ok(err) =>
let buf = Array.repeat(rc, 1024, (0i8: Int8));
match Readable.read(buf, err) {
case Result.Err(e) => println("read failed: ${e}")
case Result.Ok(n) => println("Read ${n} bytes from stderr.")
}
}
}
}
注意:
java -versionは標準出力ではなく標準エラーに書き込みます。
終了の待機
プロセスが終了するまでブロックするには Process.waitFor を使います。この関数は終了コードを Int32 として返します。
use Sys.Process
def main(): Unit \ { Process, IO } =
match Process.exec("java", "-version" :: Nil) {
case Result.Err(err) => println("exec failed: ${err}")
case Result.Ok(ph) =>
match Process.waitFor(ph) {
case Result.Err(err) => println("waitFor failed: ${err}")
case Result.Ok(code) => println("Process exited with code: ${code}")
}
}
作業ディレクトリと環境変数
Process.execWithCwd は、特定の作業ディレクトリを指定してプロセスを起動します。Process.execWithEnv は、追加の環境変数を渡します。
use Sys.Process
def main(): Unit \ { Process, IO } =
match Process.execWithCwd("java", "-version" :: Nil, Some("/tmp")) {
case Result.Ok(_) => println("execWithCwd succeeded.")
case Result.Err(err) => println("execWithCwd failed: ${err}")
};
match Process.execWithEnv("java", "-version" :: Nil, Map#{"MY_VAR" => "hello"}) {
case Result.Ok(_) => println("execWithEnv succeeded.")
case Result.Err(err) => println("execWithEnv failed: ${err}")
}
Random
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、擬似乱数(Pseudorandom number)を生成するためのライブラリエフェクトとして Random を提供しています。Random エフェクトはデフォルトハンドラを持っているため、main の中で明示的に runWithIO を呼び出す必要はありません。主要なモジュールは Math.Random です。
Random エフェクト
Random エフェクトには 2 つの操作があります:
pub eff Random {
/// [0.0, 1.0] の範囲の擬似乱数(64 ビット浮動小数点数)を返します。
def randomFloat64(): Float64
/// 擬似乱数(64 ビット整数)を返します。
def randomInt64(): Int64
}
ランダムな値の生成
Random の最も簡単な使い方は、値を生成してそれに応じた処理を行うことです:
use Math.Random
def main(): Unit \ { Random, IO } =
let flip = Random.randomFloat64() > 0.5;
if (flip)
println("heads")
else
println("tails")
Random はデフォルトハンドラを持っているため、このエフェクトは新しいランダムシードを使って自動的に処理されます。
シード付きの乱数
runWithSeed ハンドラは固定のシード(Seed)を使用するため、実行するたびに同じ乱数列が生成されます。これは、再現可能なテストやベンチマークに役立ちます:
use Math.Random
def main(): Unit \ IO =
run {
let a = Random.randomFloat64();
let b = Random.randomFloat64();
println("a = ${a}, b = ${b}")
} with Random.runWithSeed(42i64)
シード付きの乱数生成器は完全に決定的であるため、runWithSeed は IO を導入することなく Random エフェクトを除去します。
Sleep
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、現在のスレッドを一時停止するためのライブラリエフェクトとして Sleep を提供しています。Sleep エフェクトにはデフォルトハンドラがあるため、main の中で明示的に runWithIO を呼び出す必要はありません。中心となるモジュールは Time.Sleep です。
Sleep エフェクト
Sleep エフェクトは、次の 1 つの操作を持ちます。
pub eff Sleep {
/// 現在のスレッドを、指定された期間 `d` だけスリープさせます。
def sleep(d: Duration): Unit
}
Duration(期間)は、Time.Duration モジュールの seconds、milliseconds、minutes などのヘルパー関数を使って作成します。
基本的なスリープ
Sleep のもっとも単純な使い方は、一定の期間だけ一時停止することです。
use Time.Duration.seconds
use Time.Sleep
def main(): Unit \ { Sleep, IO } =
println("Going to sleep...");
Sleep.sleep(seconds(1));
println("Woke up!")
何もしないスリープ
withNoOp ハンドラはすべてのスリープをスキップします。これは、遅延を含むコードを実際に待つことなくテストしたい場合に便利です。
use Time.Duration.seconds
use Time.Sleep
def main(): Unit \ IO =
run {
println("Going to sleep...");
Sleep.sleep(seconds(10));
println("Woke up instantly!")
} with Sleep.withNoOp
withNoOp は Sleep エフェクトを完全に処理するため、結果の型にはもはや Sleep は含まれません。
Middleware
Time.Sleep モジュールは、いくつかの Middleware(ミドルウェア)ハンドラを提供しています。これらは、スリープの期間をインターセプトして変換したうえで、背後にある Sleep エフェクトへ転送します。Middleware は Sleep を再送出(re-raise)するため、複数の層を合成できます。
| Middleware | 説明 |
|---|---|
withConstant | すべてのスリープ期間を固定値に置き換えます。 |
withScale | 各期間に係数を掛けます。 |
withMaxSleep | 個々のスリープを最大値までに制限します。 |
withMinSleep | 各スリープが最小値以上になるようにします。 |
withMaxTotalSleep | すべての呼び出しにわたる累積スリープを上限内に抑えます。 |
withJitter | 各期間にランダムなジッター(±係数)を加えます。 |
withLogging | Logger エフェクトを介して各スリープ期間をログに記録します。 |
withCollect | スリープする代わりに、すべての期間をリストに収集します。 |
たとえば、withMaxSleep は各スリープを最大値までに制限し、withLogging は各期間をログに記録します。
use Time.Duration.{milliseconds, seconds}
use Time.Sleep
def main(): Unit \ { Logger, Sleep, IO } =
run {
println("Sleeping for 2 seconds (capped to 500ms)...");
Sleep.sleep(seconds(2));
println("Done!")
} with Sleep.withMaxSleep(milliseconds(500))
with Sleep.withLogging
Middleware の合成
各 Middleware は Sleep を再送出するため、自然に積み重ねる(スタックする)ことができます。次の例では、各スリープに ±20% のランダムなジッターを加え、その結果の期間をログに記録します。
use Math.Random
use Time.Duration.{seconds}
use Time.Sleep
/// `withJitter` と `withLogging` を合成して、スリープ期間に ±20% の
/// ランダムなジッターを加え、各スリープを `Logger` エフェクトを介してログに記録します。
def main(): Unit \ { Logger, Random, Sleep, IO } =
run {
println("Sleeping 3 times with ±20% jitter...");
Sleep.sleep(seconds(1));
Sleep.sleep(seconds(2));
Sleep.sleep(seconds(3));
println("Done!")
} with Sleep.withJitter(0.2)
with Sleep.withLogging
順序が重要です。withJitter は元の期間をインターセプトしてジッターを適用し、Sleep を再送出します。その後、withLogging ハンドラはジッターが適用された期間を見ることになります。
モジュール
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、他の多くのプログラミング言語で知られているような階層的モジュール(Hierarchical module)をサポートしています。
モジュールの宣言と使用
モジュールは、mod キーワードに続けてモジュールの名前空間と名前を書くことで宣言します。
例えば、次のようにモジュールを宣言できます:
mod Math {
pub def sum(x: Int32, y: Int32): Int32 = x + y
}
ここでは、sum という関数を内部に持つ Math というモジュールを宣言しました。モジュールの外側からは、完全修飾名(Fully-qualified name)を使って sum 関数を参照できます:
def main(): Unit \ IO =
let result = Math.sum(123, 456);
println(result)
あるいは、use を使って sum 関数をローカルスコープに持ち込むこともできます:
def main(): Unit \ IO =
use Math.sum;
let result = sum(123, 456);
println(result)
モジュール内の複数の宣言を使用する
モジュール内に複数の宣言がある場合:
mod Math {
pub def sum(x: Int32, y: Int32): Int32 = x + y
pub def mul(x: Int32, y: Int32): Int32 = x * y
}
もちろん、それぞれの宣言を個別に use することもできます:
use Math.sum;
use Math.mul;
def main(): Unit \ IO =
mul(42, 84) |> sum(21) |> println
しかし、複数の use を 1 つにまとめる、より短い書き方もあります:
use Math.{sum, mul};
def main(): Unit \ IO =
mul(42, 84) |> sum(21) |> println
注意: Flix はワイルドカードによる use をサポートしていません。これは、分かりにくいバグにつながる可能性があるためです。
リネームによる名前衝突の回避
同じ名前を持つ宣言同士の名前衝突は、リネーム(Renaming)を使って回避できます。
例えば、次の 2 つのモジュールがあるとします:
mod A {
pub def concat(x: String, y: String): String = x + y
}
mod B {
pub def concat(xs: List[Int32], ys: List[Int32]): List[Int32] = xs ::: ys
}
このとき、それぞれの concat 関数を一意な名前で use できます。例えば:
use A.{concat => concatStrings}
use B.{concat => concatLists}
def main(): Unit \ IO =
concatStrings("Hello", " World!") |> println
この機能は強力ですが、多くの場合は完全修飾名を使う方が適切かもしれません。
モジュールと Enum
モジュールの内部で enum を定義できます。例えば:
mod Zoo {
pub enum Animal {
case Cat,
case Dog,
case Fox
}
}
ここで Zoo モジュールは、Cat、Dog、Fox という 3 つのケースを持つ Animal という enum 型を含んでいます。
型とケースには、完全修飾名を使ってアクセスできます:
def says(a: Zoo.Animal): String = match a {
case Zoo.Animal.Cat => "Meow"
case Zoo.Animal.Dog => "Woof"
case Zoo.Animal.Fox => "Roar"
}
def main(): Unit \ IO =
println("A cat says ${says(Zoo.Animal.Cat)}!")
あるいは、Animal 型とそのケースの両方を use することもできます:
use Zoo.Animal
use Zoo.Animal.Cat
use Zoo.Animal.Dog
use Zoo.Animal.Fox
def says(a: Animal): String = match a {
case Animal.Cat => "Meow"
case Animal.Dog => "Woof"
case Animal.Fox => "Roar"
}
def main(): Unit \ IO =
println("A cat says ${says(Cat)}!")
use Zoo.Animal は Animal の型をスコープに持ち込むのに対し、use Zoo.Animal.Cat は Cat というケースをスコープに持ち込むことに注意してください。
モジュールとトレイト
モジュールの内部でトレイトを定義することもできます。その仕組みは、モジュール内の enum と同様です。
例えば、次のように書けます:
mod Zoo {
pub trait Speakable[t] {
pub def say(x: t): String
}
}
enum Animal with ToString {
case Cat,
case Dog,
case Fox
}
instance Zoo.Speakable[Animal] {
pub def say(a: Animal): String = match a {
case Cat => "Meow"
case Dog => "Woof"
case Fox => "Roar"
}
}
完全修飾名を使えば次のように書けます:
def speak(x: t): Unit \ IO with Zoo.Speakable[t], ToString[t] =
println("A ${x} says ${Zoo.Speakable.say(x)}!")
def main(): Unit \ IO =
speak(Animal.Cat)
あるいは、Zoo.Speakable トレイトと Zoo.Speakable.say 関数を use することもできます:
use Zoo.Speakable
use Zoo.Speakable.say
def speak(x: t): Unit \ IO with Speakable[t], ToString[t] =
println("A ${x} says ${say(x)}!")
モジュールの宣言
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
すでに見てきたように、モジュールは mod キーワードを使って宣言できます:
mod Museum {
// ... メンバー ...
}
モジュールは、他のモジュールの中に入れ子にすることができます:
mod Museum {
mod Entrance {
pub def buyTicket(): Unit \ IO =
println("Museum.Entrance.buyTicket() was called.")
}
mod Restaurant {
pub def buyMeal(): Unit \ IO =
println("Museum.Restaurant.buyMeal() was called.")
}
mod Giftshop {
pub def buyGift(): Unit \ IO =
println("Museum.Giftshop.buyGift() was called.")
}
}
これらのメソッドは、次のように呼び出せます:
def main(): Unit \ IO =
Museum.Entrance.buyTicket();
Museum.Restaurant.buyMeal();
Museum.Giftshop.buyGift()
あるいは、次のように呼び出すこともできます:
use Museum.Entrance.buyTicket;
use Museum.Restaurant.buyMeal;
use Museum.Giftshop.buyGift;
def main(): Unit \ IO =
buyTicket();
buyMeal();
buyGift()
アクセシビリティ
モジュール A の中で宣言されたモジュールメンバー m は、次のいずれかの場合に別のモジュール B からアクセスできます:
- メンバー
mが公開(pub)として宣言されている。 - モジュール
BがAのサブモジュール(sub-module)である。
例えば、次のコードは許可されます:
mod A {
mod B {
pub def g(): Unit \ IO = A.f() // OK
}
def f(): Unit \ IO = println("A.f() was called.")
}
ここで f はモジュール A に対してプライベートです。しかし、B は A のサブモジュールであるため、B の内部から f にアクセスできます。一方、次のコードは許可されません:
mod A {
mod B {
def g(): Unit \ IO = println("A.B.g() was called.")
}
pub def f(): Unit \ IO = A.B.g() // NOT OK
}
なぜなら、g は B に対してプライベートだからです。
モジュールの使用
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
すでに見てきたように、use 構文はモジュールのメンバーをローカルスコープに持ち込みます。
例えば、次のプログラムがあるとします:
mod A {
mod B {
pub enum Color {
case Red, Green, Blue
}
pub type alias Hue = Color
pub def isWarm(c: Color): Bool =
match c {
case Color.Red => true
case Color.Green => false
case Color.Blue => false
}
}
}
このとき、以下の use はすべて意味を持ちます:
use A.B.Color
use A.B.Color.{Red, Green, Blue}
use A.B.Hue
use A.B.isWarm
さまざまな種類の use
Flix は、以下を含む複数の種類の use をサポートしています:
- 名前の修飾付き use:
use A.B.Color - 複数の名前の修飾付き use:
use A.B.Color.{Red, Green, Blue} - リネームを伴う修飾付き use:
use A.B.Color => AColor - 複数のリネームを伴う修飾付き use:
use A.B.Color.{Red => R, Green => G, Blue => B}
注意: Flix はワイルドカードをサポートしていません。
use はどこに書けるのか?
Flix では、次の 2 つの場所で use を使えます:
- モジュールの内部
- 関数の内部
例えば:
mod A {
use Chain
use Chain.Empty
use Chain.Chain
use Int32.max
pub def maxValue(c: Chain[Int32]): Int32 =
match c {
case Empty => 0
case One(x) => x
case Chain(x, y) => max(maxValue(x), maxValue(y))
}
}
これは次のように書くこともできます:
mod A {
use Chain
pub def maxValue(c: Chain[Int32]): Int32 =
use Chain.Empty;
use Chain.Chain;
use Int32.max;
match c {
case Empty => 0
case One(x) => x
case Chain(x, y) => max(maxValue(x), maxValue(y))
}
}
式の内部ではセミコロンが使われている点に注意してください。
デフォルトの use
Flix では、いくつかの組み込みコンストラクタが常にスコープ内にあります:
List.NilとList.ConsResult.OkとResult.Err
コンパニオンモジュール
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
モジュールの内部では、そのモジュールと同じ名前を持つ enum、struct、エフェクト、またはトレイトを宣言できます。このような宣言を、そのモジュールの companion(コンパニオン) と呼びます。
例:
mod Color {
pub enum Color {
case Red,
case Green,
case Blue
}
}
ここでは、Color enum が Color モジュールの companion です。
companion の名前はモジュールからエクスポートされます。つまり、Color はモジュールと enum の両方を指すことができます。case は Color.Red としても Color.Color.Red としても参照できます。
companion は、そのモジュール内の他のどの宣言よりも前に置かなければなりません。そうでない場合、コンパイラはエラーを報告します。
enum のコンパニオン
enum がモジュールの companion として宣言されている場合、その型と case はモジュール全体で自動的に利用可能になります:
mod Color {
pub enum Color {
case Red,
case Green,
case Blue
}
pub def isWarm(c: Color): Bool = match c {
case Red => true
case Green => false
case Blue => false
}
}
ここでは、companion である Color モジュールの内部で、Color 型と Red、Green、Blue の各 case がスコープに入っています。
struct のコンパニオン
struct もモジュールの companion として宣言できます。struct のフィールドはそのコンパニオンモジュールの内部からしか見えないため、フィールドを読み書きする関数はすべてそこに置く必要があります。
例:
mod Point {
pub struct Point[r] {
x: Int32,
mut y: Int32
}
pub def area(p: Point[r]): Int32 \ r = p->x * p->y
}
ここで area が x と y のフィールドにアクセスできるのは、それが Point のコンパニオンモジュールの内部にあるからです。フィールドの可視性の詳細については、構造体を参照してください。
エフェクトのコンパニオン
エフェクトもモジュールの companion として宣言できます。そのエフェクトにデフォルトハンドラがある場合、それは同じコンパニオンモジュールに置きます:
mod Fs.Glob {
pub eff Glob {
def glob(base: String, pattern: String): Result[IoError, List[String]]
}
// エフェクトのハンドラや補助関数はここに置きます。
}
トレイトのコンパニオン
トレイトもモジュールの companion として宣言できます。通常、トレイトに関連する機能を格納する場所としてコンパニオンモジュールを使います:
mod Addable {
pub trait Addable[t] {
pub def add(x: t, y: t): t
}
pub def add3(x: t, y: t, z: t): t with Addable[t] = add(add(x, y), z)
}
Addable のメンバーにアクセスする際、Flix はトレイト宣言とそのコンパニオンモジュールの両方を自動的に探索します。その結果、Addable.add はトレイトのメンバーである add を指し、Addable.add3 は Addable モジュール内の関数を指します。
注意すべき点として、トレイトのコンパニオンモジュールに定義された関数は、そのトレイトのインスタンスによって再定義することができません。したがって、後から再定義するつもりのないメンバーだけをコンパニオンモジュールに置くべきです。
コンパニオンモジュール内のインスタンス
トレイトのインスタンスは、その型のコンパニオンモジュール内で宣言できます。例えば、Size enum に対する Add、Sub、ToString のインスタンスは、enum 自体と並べて配置します:
mod Fs.Size {
pub enum Size(Int64) with Eq, Order, Hash
instance Add[Size] {
pub def add(x: Size, y: Size): Size =
let Size(x1) = x;
let Size(y1) = y;
Size(x1 + y1)
}
pub def zero(): Size = Size(0i64)
}
トレイトが別の場所で定義されている場合、インスタンスを置く場所としてはここが推奨されます。
トレイト
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
トレイト(Trait)は型クラスとしても知られており、抽象化とモジュール性を支える仕組みです。Flix のトレイトシステムは Haskell や Rust のものと似ていますが、同一ではありません。Flix のトレイトは、関連型、関連エフェクト、高カインド型をサポートしています。
例を使ってトレイトを説明します。
Option[Int32] に対する等価性は、次のように定義できます:
def equals(x: Option[Int32], y: Option[Int32]): Bool =
match (x, y) {
case (None, None) => true
case (Some(v1), Some(v2)) => v1 == v2
case _ => false
}
同様に、List[Int32] に対する等価性も次のように定義できます:
def equals(x: List[Int32], y: List[Int32]): Bool =
match (x, y) {
case (Nil, Nil) => true
case (v1 :: xs, v2 :: ys) => v1 == v2 and equals(xs, ys)
case _ => false
}
しかし、等価性をサポートするデータ型に対する共通の抽象化が欲しい場合はどうすればよいでしょうか?
ここでトレイトの出番です。Equatable トレイトを次のように定義できます:
trait Equatable[t] {
pub def equals(x: t, y: t): Bool
}
このトレイトは、単一の equals というトレイトシグネチャを持ちます。このトレイトは型パラメータ t について多相的であるため、Option[t] と List[t] の両方に対して Equatable を実装できます:
instance Equatable[Option[t]] with Equatable[t] {
pub def equals(x: Option[t], y: Option[t]): Bool =
match (x, y) {
case (None, None) => true
case (Some(v1), Some(v2)) => Equatable.equals(v1, v2)
case _ => false
}
}
ここで注目してほしいのは、Equatable を Option[Int32] に対して実装したのではなく、t 自体が等価比較可能である限り任意の Option[t] に対して実装したという点です。さらに、v1 と v2 を == で直接比較する代わりに、それらに対して Equatable.equals を呼び出しています。
List[t] に対しても Equatable を実装できます:
instance Equatable[List[t]] with Equatable[t] {
pub def equals(x: List[t], y: List[t]): Bool =
use Equatable.equals;
match (x, y) {
case (Nil, Nil) => true
case (v1 :: xs, v2 :: ys) => equals(v1, v2) and equals(xs, ys)
case _ => false
}
}
Int32 に対しても Equatable を実装したと仮定すると、Equatable を使って 2 つの Option[Int32] 値が等しいかどうかを計算できます。それだけでなく、2 つの Option[List[Int32]] 値が等しいかどうかも計算できるのです!これは抽象化の力を示しています。Option[t] と List[t] に対するインスタンス(instance)を一度実装すれば、それらのインスタンスをあらゆる場所で再利用できます。
新しく定義した Equatable トレイトを使って、多相的な関数を書くことができます。
例えば、ある要素がリストに含まれているかどうかを計算する関数を定義できます:
def memberOf(x: t, l: List[t]): Bool with Equatable[t] =
match l {
case Nil => false
case y :: ys => Equatable.equals(x, y) or memberOf(x, ys)
}
要素の型が Equatable を実装してさえいれば、任意の型のリストに対して memberOf を使うことができます。
注意: Flix 標準ライブラリでは、
EquatableトレイトはEqという名前です。さらに、==演算子はトレイトシグネチャEq.eqの糖衣構文です。
Sealed トレイト
トレイトを sealed として宣言すると、そのトレイトを誰が実装できるかを制限できます。
例えば:
mod Zoo {
sealed trait Animal[a] {
pub def isMammal(x: a): Bool
}
instance Animal[Giraffe] {
pub def isMammal(_: Giraffe): Bool = true
}
instance Animal[Penguin] {
pub def isMammal(_: Penguin): Bool = false
}
pub enum Giraffe
pub enum Penguin
}
ここでは Animal と Giraffe のインスタンスを実装できます。それらが Animal トレイトと同じモジュール内に存在するからです。しかし、Zoo モジュールの外側から Animal を実装することはできません。試しに次のように書くと:
mod Lake {
pub enum Swan
instance Zoo.Animal[Swan] {
pub def isMammal(_: Swan): Bool = false
}
}
Flix は次のように報告します:
❌ -- Resolution Error --------------------------------------------------
>> Trait 'Zoo.Animal' is sealed from the module 'Lake'.
21 | instance Zoo.Animal[Swan] {
^^^^^^^^^^
sealed trait.
不正な形のトレイト
トレイトは C# や Java スタイルのインターフェースではありません。具体的には:
- すべてのトレイトはちょうど 1 つの型パラメータを持たなければならず、
- すべてのシグネチャはその型パラメータに言及しなければなりません。
例えば、次のトレイトは正しくありません:
trait Animal[a] {
pub def isMammal(x: a): Bool // OK -- a に言及している。
pub def numberOfGiraffes(): Int32 // NG -- a に言及していない。
}
上記のトレイトをコンパイルすると、Flix は次のように報告します:
❌ -- Resolution Error --------------------------------------------------
>> Unexpected signature 'numberOfGiraffes' which does not mention the type
>> variable of the trait.
7 | pub def numberOfGiraffes(): Int32
^^^^^^^^^^^
unexpected signature.
問題は、numberOfGiraffes のシグネチャが型パラメータ a に言及していないことです。
複雑なインスタンス
トレイトのインスタンスは、次の条件を満たす型に対して定義しなければなりません:
- ちょうど 1 つの型コンストラクタであり、
- それが 0 個以上の相異なる型変数に適用されていること。
例えば、先ほどの Equatable トレイトがあるとします:
trait Equatable[t] {
pub def equals(x: t, y: t): Bool
}
次のような型に対してはインスタンスを実装できます:
Option[a]List[a](a, b)
しかし、次のような型に対してはインスタンスを実装できません:
Option[Int32]List[String](a, Bool)Map[Int32, v]
例えば List[Int32] に対してインスタンスを実装しようとすると、Flix は次のように報告します:
❌ -- Instance Error --------------------------------------------------
>> Complex instance type 'List[Int32]' in 'Equatable'.
6 | instance Equatable[List[Int32]] {
^^^^^^^^^
complex instance type
An instance type must be a type constructor applied to zero or more
distinct type variables.
重複するインスタンス
重複する型に対して、同じトレイトのインスタンスを 2 つ実装することはできません。
例えば、List[t] に対して Equatable のインスタンスを 2 つ実装しようとすると:
instance Equatable[List[t]] {
pub def equals(x: List[t], y: List[t]): Bool = ???
}
instance Equatable[List[t]] {
pub def equals(x: List[t], y: List[t]): Bool = ???
}
Flix は次のように報告します:
❌ -- Instance Error --------------------------------------------------
>> Overlapping instances for 'Equatable'.
1 | instance Equatable[List[t]] {
^^^^^^^^^
the first instance was declared here.
4 | instance Equatable[List[t]] {
^^^^^^^^^
the second instance was declared here.
必須のトレイト
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix で実践的なプログラミングを行うには、少なくとも 3 つのトレイト(Trait)、すなわち Eq、Order、ToString についての知識が必要です。
Eq トレイト
Eq トレイトは、ある特定の型の 2 つの値が等しいのはどのようなときかを表現します:
trait Eq[a] {
///
/// `x` が `y` と等しい場合、かつその場合に限り `true` を返します。
///
pub def eq(x: a, y: a): Bool
// ... その他のメンバーは省略 ...
}
Eq を実装するには、eq 関数を実装するだけで済みます。eq を実装すると、Eq.neq の実装が自動的に得られます。
Order トレイト
Order トレイトは、ある値が同じ型の別の値以下であるのはどのようなときかを表現します:
trait Order[a] with Eq[a] {
///
/// `x` < `y` の場合は `Comparison.LessThan` を、
/// `x` == `y` の場合は `Equal` を、
/// `x` > `y` の場合は `Comparison.GreaterThan` を返します。
///
pub def compare(x: a, y: a): Comparison
// ... その他のメンバーは省略 ...
}
Order トレイトを実装するには、Comparison 型の値を返す compare 関数を実装しなければなりません。Comparison データ型は次のように定義されています:
enum Comparison {
case LessThan
case EqualTo
case GreaterThan
}
compare を実装すると、Order.less、Order.lessThan、Order.greater、Order.greaterEqual、Order.max、Order.min の実装が自動的に得られます。
ToString トレイト
ToString トレイトは、特定の値の文字列表現を得るために使用されます:
trait ToString[a] {
///
/// 与えられた `x` の文字列表現を返します。
///
pub def toString(x: a): String
}
Flix は文字列補間(String interpolation)において ToString トレイトを使用します。
例えば、次の補間された文字列
"Good morning ${name}, it is ${hour} o'clock."
は、実際には次の式に対する糖衣構文です:
"Good morning " + ToString.toString(name) + ", it is "
+ ToString.toString(hour) + " o'clock."
続くサブセクションでは、Eq、Order、ToString トレイトの実装を自動的に導出する方法について説明します。
自動導出
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、いくつかのトレイトに対する自動導出(Automatic derivation)をサポートしています。これには以下が含まれます:
Eq— 型の値に対する構造的等価性を導出します。Order— 型の値に対する全順序を導出します。ToString— 型の値に対する人間が読みやすい文字列表現を導出します。Coerce- 単純なデータ型をその基となる表現に変換します。
Eq と Order の導出
enum 宣言の with 節を使うことで、Eq トレイトと Order トレイトのインスタンスを自動的に導出できます。例えば:
enum Shape with Eq, Order {
case Circle(Int32)
case Square(Int32)
case Rectangle(Int32, Int32)
}
導出された実装は構造的であり、case 宣言の順序に依存します:
def main(): Unit \ IO =
println(Circle(123) == Circle(123)); // `true` を出力
println(Circle(123) != Square(123)); // `true` を出力
println(Circle(123) <= Circle(123)); // `true` を出力
println(Circle(456) <= Square(123)) // `true` を出力
注意:
EqとOrderの自動導出には、enumの内部の型自身がEqとOrderを実装していることが必要です。
ToString の導出
ToString インスタンスも自動的に導出できます:
enum Shape with ToString {
case Circle(Int32)
case Square(Int32)
case Rectangle(Int32, Int32)
}
これにより、文字列補間を活用して次のように書けます:
def main(): Unit \ IO =
let c = Circle(123);
let s = Square(123);
let r = Rectangle(123, 456);
println("A ${c}, ${s}, and ${r} walk into a bar.")
これは次のように出力します:
A Circle(123), Square(123), and Rectangle(123, 456) walk into a bar.
Coerce の導出
Coerce トレイトの実装も自動的に導出できます。
Coerce トレイトは、単純な(case が1つの)データ型をその基となる実装に変換します。
enum Shape with Coerce {
case Circle(Int32)
}
def main(): Unit \ IO =
let c = Circle(123);
println("The radius is ${coerce(c)}")
case が2つ以上ある enum に対して Coerce を導出することはできません。
例えば、次のように書こうとすると:
enum Shape with Coerce {
case Circle(Int32)
case Square(Int32)
}
Flix コンパイラはコンパイルエラーを出力します:
❌ -- Derivation Error --------------------------------------------------
>> Cannot derive 'Coerce' for the non-singleton enum 'Shape'.
1 | enum Shape with Coerce {
^^^^^^
illegal derivation
'Coerce' can only be derived for enums with exactly one case.
関連型
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
関連型(Associated type)とは、トレイトの型メンバであり、各トレイトインスタンスごとに指定されるものです。関連型は、多引数型クラスに代わる、より自然な選択肢と見なされることがよくあります。
例を使って関連型を説明します。
加算できる型のためのトレイトを次のように定義できます:
trait Addable[t] {
pub def add(x: t, y: t): t
}
浮動小数点数、整数、文字列などの型に対して、Addable トレイトの複数のインスタンスを実装できます。例えば、Int32 のインスタンスは次のようになります:
instance Addable[Int32] {
pub def add(x: Int32, y: Int32): Int32 = x + y
}
String のインスタンスは次のとおりです:
instance Addable[String] {
pub def add(x: String, y: String): String = "${x}${y}"
}
しかし、Set に要素を追加したい場合はどうでしょうか?
直感的には、次のように書きたくなります:
instance Addable[Set[a]] with Order[a] {
pub def add(s: Set[a], x: a): Set[a] = Set.insert(x, s)
}
しかし、この add のシグネチャは Addable で宣言されたシグネチャと一致しません。
この問題は、関連型を使って Addable の柔軟性を高めることで解決できます:
trait Addable[t] {
type Rhs
pub def add(x: t, y: Addable.Rhs[t]): t
}
Addable トレイトは Rhs という名前の関連型を持つようになりました。add のシグネチャに見られるように、この関連型は Addable.Rhs[t] として参照します。Addable のインスタンスを宣言するときは、必ず関連型を指定しなければなりません。
これまでどおり、整数や文字列に対するインスタンスも実装できます。例えば:
instance Addable[Int32] {
type Rhs = Int32
pub def add(x: Int32, y: Int32): Int32 = x + y
}
さらに、Set に要素を追加できるインスタンスも実装できます:
instance Addable[Set[a]] with Order[a] {
type Rhs = a
pub def add(s: Set[a], x: a): Set[a] = Set.insert(x, s)
}
重要なのは、_各トレイトインスタンスが関連型を指定する_という点です。
ここで、Set[a] に対して2つのインスタンス、すなわち (a) 上記のように Set に要素を追加するもの、(b) 2つの Set を足し合わせるもの、を指定できるのではないかと考えるかもしれません:
instance Addable[Set[a]] with Order[a] {
type Rhs = Set[a]
pub def add(x: Set[a], y: Set[a]): Set[a] = Set.union(x, y)
}
しかし、それぞれのインスタンスは単独では有効であるものの、両方を同時に持つことはできません:
❌ -- Instance Error --------------------------------------------------
>> Overlapping instances for 'Addable'.
...
このような重複したインスタンスが存在すると、Addable.add(Set#{}, Set#{}) のような式が曖昧になってしまいます。2つの Set を足し合わせているのでしょうか?それとも、空の Set を Set に追加しているのでしょうか?
例: ForEach トレイト
関連型を使って、forEach 関数を持つコレクションのためのトレイトを定義できます:
trait ForEach[t] {
type Elm
pub def forEach(f: ForEach.Elm[t] -> Unit \ ef, x: t): Unit \ ef
}
ここで t はコレクションの型であり、関連型 Elm はその要素の型です。ForEach に対していくつかのインスタンスを実装できます。例えば、List[a] のインスタンスを実装できます:
instance ForEach[List[a]] {
type Elm = a
pub def forEach(f: a -> Unit \ ef, x: List[a]): Unit \ ef = List.forEach(f, x)
}
Map[k, v] のインスタンスも実装できます:
instance ForEach[Map[k, v]] {
type Elm = (k, v)
pub def forEach(f: ((k, v)) -> Unit \ ef, x: Map[k, v]): Unit \ ef =
Map.forEach(k -> v -> f((k, v)), x)
}
興味深く、また有用なのは、要素型をキーと値のペアとして定義できる点です。f の引数にはペアを受け取らせたいので、引数の周りに追加の括弧が必要になります。
String に対しては、個々の文字を1つずつ反復処理できるインスタンスを実装できます:
instance ForEach[String] {
type Elm = Char
pub def forEach(f: Char -> Unit \ ef, x: String): Unit \ ef =
x |> String.toList |> List.forEach(f)
}
例: Collection トレイト
別の例として、コレクションのためのトレイトを定義できます:
trait Collection[t] {
type Elm
pub def empty(): t
pub def insert(x: Collection.Elm[t], c: t): t
pub def toList(c: t): List[Collection.Elm[t]]
}
ここで t はコレクションの型であり、Elm はその要素の型です。すべてのコレクションは、empty、insert、toList という3つの操作をサポートしなければなりません。
Vector[a] に対する Collection のインスタンスを実装できます:
instance Collection[Vector[a]] {
type Elm = a
pub def empty(): Vector[a] = Vector.empty()
pub def insert(x: a, c: Vector[a]): Vector[a] = Vector.append(c, Vector#{x})
pub def toList(c: Vector[a]): List[a] = Vector.toList(c)
}
そして、Set[a] に対する Collection のインスタンスも実装できます:
instance Collection[Set[a]] with Order[a] {
type Elm = a
pub def empty(): Set[a] = Set.empty()
pub def insert(x: a, c: Set[a]): Set[a] = Set.insert(x, c)
pub def toList(c: Set[a]): List[a] = Set.toList(c)
}
等値制約
多相関数を書くとき、関連型を_制限_したい場合があります。
例えば、先ほどの Collection トレイトの例に戻ると、要素型が Int32 であることを要求する関数を書くことができます。これにより、合計を計算する関数を書けるようになります:
def sum(c: t): Int32 with Collection[t] where Collection.Elm[t] ~ Int32 =
Collection.toList(c) |> List.sum
ここで where 節には、_型の等値制約(Type equality constraint)_のリストが含まれています。具体的には、等値制約 Collection.Elm[t] ~ Int32 は、Collection のインスタンスが存在する任意の型 t について、そのインスタンスの要素型が Int32 に等しい限り、sum を使用できることを表明しています。この制限により、コレクションの要素が整数であることが保証され、List.sum を呼び出せるようになります。
デフォルト型
関連型にデフォルト型を定義できます。
Addable に戻ると、関連型 Rhs のデフォルトを t として定義できます:
trait Addable[t] {
type Rhs = t // デフォルト型を持つ関連型
pub def add(x: t, y: Addable.Rhs[t]): t
}
ここでは、インスタンスの実装で Rhs が定義されていない場合、デフォルトで t になることを指定しています。その結果、Int32 のインスタンスを次のように定義できます:
instance Addable[Int32] {
pub def add(x: Int32, y: Int32): Int32 = x + y
}
type Rhs = Int32 を明示的に定義する必要はありません。
関連エフェクト
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
これまで、関連型(Associated types)によって、各インスタンスが関連型に対する具体的な型を指定できるようになり、トレイトの柔軟性が高まることを見てきました。関連エフェクト(Associated effects)も同じ仕組みで動作しますが、対象となるのはエフェクトです。
関連エフェクトの必要性を、簡単な例で説明します。
割り算ができる型のためのトレイトを定義できます:
trait Dividable[t] {
pub def div(x: t, y: t): t
}
そして、例えば Float32 や Int32 に対してこのトレイトを実装できます:
instance Dividable[Float32] {
pub def div(x: Float32, y: Float32): Float32 = x / y
}
instance Dividable[Int32] {
pub def div(x: Int32, y: Int32): Int32 = x / y
}
しかし、ゼロ除算はどうすればよいのでしょうか?例外を発生させ、それを型・エフェクトシステムに追跡させたいとします。次のように書きたいところです:
pub eff DivByZero {
pub def raise(): Void
}
instance Dividable[Int32] {
pub def div(x: Int32, y: Int32): Int32 \ DivByZero =
if (y == 0) DivByZero.raise() else x / y
}
しかし残念ながら、これはうまくいきません:
❌ -- Type Error --------------------------------------------------
>> Mismatched signature 'div' required by 'Dividable'.
14 | pub def div(x: Int32, y: Int32): Int32 \ DivByZero =
^^^
...
コンパイラが説明しているとおり、問題はトレイト Dividable における div の定義が純粋(pure)として宣言されていることです。そのため、例外を発生させることは許されません。Dividable.div のシグネチャを変更することもできますが、それは Float32 インスタンスにとって問題になります。なぜなら、Float32 のゼロ除算は NaN を返すのであって、例外を発生させないからです。
解決策は、関連エフェクトを使うことです。そうすれば、Int32 のインスタンスは DivByZero 例外が発生する可能性があることを指定でき、一方で Float32 のインスタンスは純粋のままでいられます。Dividable に関連エフェクト Aef を追加します:
trait Dividable[t] {
type Aef: Eff
pub def div(x: t, y: t): t \ Dividable.Aef[t]
}
そして、Float32 と Int32 のインスタンスを再実装します:
instance Dividable[Float32] {
type Aef = { Pure } // 例外なし。ゼロ除算は NaN を返します。
pub def div(x: Float32, y: Float32): Float32 = x / y
}
instance Dividable[Int32] {
type Aef = { DivByZero }
pub def div(x: Int32, y: Int32): Int32 \ DivByZero =
if (y == 0) DivByZero.raise() else x / y
}
関連エフェクトとリージョン
関連エフェクトは、リージョンと組み合わせて使いたくなることがよくあります。
以前登場した ForEach トレイトがあるとします:
trait ForEach[t] {
type Elm
pub def forEach(f: ForEach.Elm[t] -> Unit \ ef, x: t): Unit \ ef
}
これまで見てきたように、このトレイトは例えば List[t] や Map[k, v] に対して実装できます。しかし、MutList[t, r] や MutSet[t, r] などに対して実装したい場合はどうでしょうか。試してみましょう:
instance ForEach[MutList[t, r]] {
type Elm = t
pub def forEach(f: t -> Unit \ ef, x: MutList[t, r]): Unit \ ef =
MutList.forEach(f, x)
}
しかし、Flix は次のように報告します:
❌ -- Type Error --------------------------------------------------
>> Unable to unify the effect formulas: 'ef' and 'ef + r'.
9 | MutList.forEach(f, x)
^^^^^^^^^^^^^^^^^^^^^
mismatched effect formulas.
問題は、MutList.forEach がリージョン r におけるエフェクトを持っているのに対して、トレイトの forEach のシグネチャは関数 f に由来する ef エフェクトしか許可していないことです。
ForEach トレイトを関連エフェクトで拡張することで、この問題を解決できます:
trait ForEach[t] {
type Elm
type Aef: Eff
pub def forEach(f: ForEach.Elm[t] -> Unit \ ef, x: t): Unit \ ef + ForEach.Aef[t]
}
Aef がエフェクトであることを、カインド注釈(Kind annotation) Aef: Eff によって指定する必要があります。カインドを指定しない場合はデフォルトで Type になりますが、ここで求めているのはそれではありません。
更新した ForEach トレイトを使えば、List[t] と MutList[t] の両方に対して実装できます:
instance ForEach[List[t]] {
type Elm = t
type Aef = { Pure }
pub def forEach(f: t -> Unit \ ef, x: List[t]): Unit \ ef = List.forEach(f, x)
}
そして、
instance ForEach[MutList[t, r]] {
type Elm = t
type Aef = { r }
pub def forEach(f: t -> Unit \ ef, x: MutList[t, r]): Unit \ ef + r =
MutList.forEach(f, x)
}
List[t] の実装では関連エフェクトが純粋であると指定しているのに対し、MutList[t, r] の実装ではリージョン r におけるヒープエフェクトがあると指定していることに注目してください。
高カインド型
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は高カインド型(Higher-kinded types)をサポートしています。そのため、トレイトは*型コンストラクタ(Type constructor)*を抽象化できます。
例えば、t[a] という形をした任意のコレクションに対する反復処理を表現するトレイトを書くことができます。ここで、t はカインド Type -> Type の型コンストラクタであり、a はカインド Type の要素型です:
trait ForEach[t: Type -> Type] {
pub def forEach(f: a -> Unit \ ef, x: t[a]): Unit \ ef
}
高カインド型を使うには、Flix はカインド注釈(Kind annotation)を書くことを要求します。つまり、ForEach が型コンストラクタを抽象化していることを Flix に伝えるために、t: Type -> Type と書く必要がありました。
ForEach トレイトのインスタンスを Option に対して実装できます:
instance ForEach[Option] {
pub def forEach(f: a -> Unit \ ef, o: Option[a]): Unit \ ef = match o {
case None => ()
case Some(x) => f(x)
}
}
また、List に対するインスタンスも実装できます:
instance ForEach[List] {
pub def forEach(f: a -> Unit \ ef, l: List[a]): Unit \ ef = List.forEach(f, l)
}
Flix のカインド
Flix は以下のカインドをサポートしています:
Type: Flix の型のカインド。- 例:
Int32、String、List[Int32]。
- 例:
RecordRow: レコードで使われる行のカインド。- 例:
{x = Int32, y = Int32 | r}において、型変数rはカインドRecordRowを持ちます。
- 例:
SchemaRow: 第一級 Datalog 制約で使われる行のカインド。- 例:
#{P(Int32, Int32) | r}において、型変数rはカインドSchemaRowを持ちます。
- 例:
Flix は通常、カインドを推論できます。例えば、次のように書くと:
def sum(r: {x = t, y = t | r}): t with Add[t] = r#x + r#y
t: Type と r: RecordRow というカインドが自動的に推論されます。
次のように明示的に指定することもできます:
def sum[t: Type, r: RecordRow](r: {x = t, y = t | r}): t with Add[t] = r#x + r#y
しかし、このスタイルは慣用的とは見なされません。
Flix が明示的なカインド注釈を要求するのは、次の 4 つの状況です:
- enum 上の、
Type以外のカインドを持つ型パラメータ。 - 型エイリアス上の、
Type以外のカインドを持つ型パラメータ。 - トレイト上の、
Type以外のカインドを持つ型パラメータ。 - トレイト内の、
Type以外のカインドを持つ型メンバ。
カインド注釈が必要になる最も一般的なシナリオは、型パラメータや型メンバがエフェクトを取りうるようにしたい場合です。
高カインド型と関連型の比較
実際のところ、高カインド型と関連型は、よく似た抽象を定義するために使うことができます。
例えば、これまで見てきたように、ForEach トレイトは 2 つの異なる方法で定義できます。
高カインド型を使う方法:
trait ForEach[t: Type -> Type] {
pub def forEach(f: a -> Unit \ ef, x: t[a]): Unit \ ef
}
そして、関連型を使う方法:
trait ForEach[t] {
type Elm
pub def forEach(f: ForEach.Elm[t] -> Unit \ ef, x: t): Unit \ ef
}
ForEach の場合は、関連型を使った定義の方が柔軟です。関連する要素型を Char として、String に対するインスタンスを実装できるからです。しかし、高カインド型も依然として有用です。例えば、Flix 標準ライブラリは Functor トレイトを次のように定義しています:
trait Functor[m : Type -> Type] {
pub def map(f: a -> b \ ef, x: m[a]): m[b] \ ef
}
注目すべきは、m のカインドによって、すべての Functor 実装が構造を保存することが保証される点です。つまり、例えば Option[a] に対して map を適用すると、必ず Option[b] が返ってくることが分かります。
構造化並行性
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、Go と Rust に着想を得た、チャネルとプロセスによる CSP スタイルの並行処理をサポートしています。
プロセスの生成
spawn キーワードを使って、プロセス(Process)を生成できます:
def main(): Unit \ IO = region rc {
spawn println("Hello from thread") @ rc;
println("Hello from main")
}
生成されたプロセスは、必ずリージョンに関連付けられます。リージョンは、それに関連付けられたすべてのプロセスが完了するまで終了しません:
def main(): Unit \ IO =
region r1 {
region r2 {
spawn println("Hello from r1") @ r1;
spawn println("Hello from r2") @ r2
};
println("r2 is now complete")
};
println("r1 is now complete")
これはつまり、Flix が構造化並行性をサポートしているということです。生成されたプロセスには、明確に定義された開始点と終了点があります。
チャネルによる通信
プロセス間で通信するには、チャネルを使用します。*チャネル(Channel)*を使うと、2つ以上のプロセスが互いにイミュータブルなメッセージを送り合うことで、データを交換できます。
チャネルには、*バッファ付き(Buffered)とバッファなし(Unbuffered)*の2つの種類があります。チャネルは必ずリージョンに関連付けられます。
バッファ付きチャネルは、作成時に設定されるサイズを持ち、その数だけメッセージを保持できます。満杯のバッファ付きチャネルにプロセスがメッセージを入れようとすると、そのプロセスは空きができるまでブロックされます。逆に、空のチャネルからプロセスがメッセージを取り出そうとすると、そのプロセスはチャネルにメッセージが入れられるまでブロックされます。
バッファなしチャネルは、サイズ0のバッファ付きチャネルのように動作します。取り出し(get)と書き込み(put)が成立するためには、送信側から受信側へメッセージが渡されるまで、両方のプロセスがランデブー(ブロック)しなければなりません。
チャネルを介してメッセージを送受信する例を示します:
def main(): Int32 \ {Chan, NonDet, IO} = region rc {
let (tx, rx) = Channel.unbuffered();
spawn Channel.send(42, tx) @ rc;
Channel.recv(rx)
}
ここで main 関数は、Sender チャネル tx と Receiver チャネル rx を返すバッファなしチャネルを作成し、send 関数を spawn して、チャネルからのメッセージを待ちます。
この例が示すように、チャネルは Sender(センダー) と Receiver(レシーバー) という2つのエンドポイントで構成されます。予想される通り、メッセージの送信は Sender からのみ、受信は Receiver からのみ行えます。
チャネルに対する select
select 式を使うと、複数のチャネルの集まりからメッセージを受信できます。例えば:
def meow(tx: Sender[String]): Unit \ Chan =
Channel.send("Meow!", tx)
def woof(tx: Sender[String]): Unit \ Chan =
Channel.send("Woof!", tx)
def main(): Unit \ {Chan, NonDet, IO} = region rc {
let (tx1, rx1) = Channel.buffered(1);
let (tx2, rx2) = Channel.buffered(1);
spawn meow(tx1) @ rc;
spawn woof(tx2) @ rc;
select {
case m <- recv(rx1) => m
case m <- recv(rx2) => m
} |> println
}
生産者・消費者(producer-consumer)やロードバランサーなど、多くの重要な並行処理パターンを select 式で表現できます。
デフォルトケース付きの select
場合によっては、メッセージが届くまでブロックして、永遠に待ち続けるかもしれない状況を避けたいことがあります。そのような場合は、メッセージがすぐに利用できないときに代わりのアクションを取りたくなります。これは、以下に示すように*デフォルトケース(Default case)*で実現できます:
def main(): String \ {Chan, NonDet} = region rc {
let (_, rx1) = Channel.buffered(1);
let (_, rx2) = Channel.buffered(1);
select {
case _ <- recv(rx1) => "one"
case _ <- recv(rx2) => "two"
case _ => "default"
}
}
ここでは、r1 にも r2 にもメッセージが送信されることはありません。select 式はすべてのケースを試し、どのチャネルも準備できていなければ、直ちにデフォルトケースを選択します。したがって、デフォルトケースを使うことで、select 式が永遠にブロックすることを防げます。
タイムアウト付きの select
デフォルトケースの代わりに、*ティッカー(Ticker)とタイマー(Timer)*を使って、select 式の中であらかじめ定められた時間だけ待つこともできます。
例えば、次のプログラムには、チャネルにメッセージを送るまでに1分かかる遅い関数がありますが、select 式は Channel.timeout を利用して、5 秒だけ待って諦めるようになっています:
def slow(tx: Sender[String]): Unit \ {Chan, NonDet, IO} =
let delay = Channel.timeout(60, Time.TimeUnit.Seconds);
Channel.recv(delay);
Channel.send("I am very slow", tx)
def main(): Unit \ {Chan, NonDet, IO} = region rc {
let (tx, rx) = Channel.buffered(1);
spawn slow(tx) @ rc;
let timeout = Channel.timeout(5, Time.TimeUnit.Seconds);
select {
case m <- recv(rx) => m
case _ <- recv(timeout) => "timeout"
} |> println
}
このプログラムは、5秒後に文字列 "timeout" を出力します。
チャネルのエフェクト
お気づきかもしれませんが、チャネルを使うと Chan と NonDet というエフェクトが現れます。
チャネルに対するあらゆる操作は Chan エフェクトを持ちます。このエフェクトは、プログラムがチャネルのグローバルな状態を変更または参照していることを表します。
チャネルに対する recv 操作は NonDet エフェクトを持ちます。これは、受け取る値が一般には非決定的であり、スレッドスケジューラの選択に依存するためです。2つのスレッドが同時にチャネルへ値を送信する準備ができていることがあり、どちらが先に送信できるかはスケジューラ次第です。
並列性
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
これまでに、spawn 式を使うことで式を新しいスレッドで評価できることを見てきました:
region rc {
spawn (1 + 2) @ rc
}
これにより、構造化並行性を用いて並行・並列プログラムを書くことができます。ただし欠点として、スレッド間の通信をチャネルを使って手動で調整しなければなりません。
並行性は不要で並列性だけが欲しい場合には、より軽量な方法として par-yield 式を使うことができます:
par (x <- e1; y <- e2; z <- e3)
yield x + y + z
この式は e1、e2、e3 を並列に評価し、それらの結果を x、y、z に束縛します。
par-yield を使うと、並列版の List.map 関数を書くことができます:
def parMap(f: a -> b, l: List[a]): List[b] = match l {
case Nil => Nil
case x :: xs =>
par (r <- f(x); rs <- parMap(f, xs))
yield r :: rs
}
この関数は f(x) と parMap(f, xs) を並列に評価します。
注意:
par-yield構文は純粋な式に対してのみ機能します。
エフェクトを伴う操作を並列に実行したい場合は、明示的なリージョンとスレッドを使う必要があります。
Java との相互運用
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は Java Virtual Machine(JVM)ベースのプログラミング言語です。したがって:
- Flix プログラムは効率的な JVM バイトコードにコンパイルされます。
- Flix プログラムは任意の Java Virtual Machine 上で動作します1。
- Flix プログラムは Java コードを呼び出すことができます。
Flix は、相互運用(Interoperability)に必要な Java の機能のほとんどをサポートしています:
- クラスからのオブジェクト生成
- クラスやオブジェクトのメソッド呼び出し
- クラスやオブジェクトのフィールドの読み書き
- クラスやインターフェースの匿名拡張
- 内部クラスへのアクセス
- 例外のキャッチとスロー
- プリミティブ値のボックス化とアンボックス化
このため、Flix プログラムは Java クラスライブラリを再利用できます。さらに、Flix のパッケージマネージャは Maven をサポートしています。
Flix と Java は同じ基本型を共有していますが、次の表に示すように名前が異なります:
| Flix の型 | Java の型 |
|---|---|
| Bool | boolean |
| Char | char |
| Float32 | float |
| Float64 | double |
| Int8 | byte |
| Int16 | short |
| Int32 | int |
| Int64 | long |
| String | String |
Flix では、プリミティブ型は常にアンボックス化されています。そのため、java.lang.Integer を期待する Java メソッドを呼び出すには、Flix の Int32 を持っている場合、java.lang.Integer.valueOf を呼び出してボックス化する必要があります。
-
Flix には少なくとも Java 21 が必要です。 ↩
オブジェクトの生成
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix では、Java に似た構文でオブジェクトを生成できます。
例えば:
import java.io.File
def main(): Unit \ IO =
let f = new File("foo.txt");
println("Hello World!")
ここでは java.io.File クラスをインポートし、new キーワードを使ってコンストラクタの1つを呼び出すことで、File オブジェクトをインスタンス化しています。
File クラスには複数のコンストラクタがあるため、次のように書くこともできます:
import java.io.File
def main(): Unit \ IO =
let f1 = new File("foo.txt");
let f2 = new File("bar", "foo.txt");
println("Hello World!")
Flix は、引数の個数とその型に基づいてコンストラクタを解決します。
別の例として、次のように書けます:
import java.io.File
import java.net.URI
def main(): Unit \ IO =
let f1 = new File("foo.txt");
let f2 = new File("bar", "foo.txt");
let f3 = new File(new URI("file://foo.txt"));
println("Hello World!")
Java の名前と Flix のモジュールが衝突する場合は、リネームインポート(renaming import) を使って解決できます:
import java.lang.{String => JString}
def main(): Unit \ IO =
let s = new JString("Hello World");
println("Hello World!")
ここで JString は Java クラスの java.lang.String を指し、String は Flix のモジュールを指します。なお、内部的には Flix の文字列と Java の文字列は同じものです。
スーパーコンストラクタの呼び出し
Java クラスの匿名サブクラス(anonymous subclass)を生成する際には、super を使って親のコンストラクタを呼び出すコンストラクタを定義できます:
import java.lang.Thread
def main(): Unit \ IO =
let t = new Thread {
def new(): Thread \ IO = super("my-thread")
def run(_this: Thread): Unit \ IO =
println("Hello from ${Thread.currentThread().getName()}")
};
t.start()
ここでは Thread を継承し、スレッド名として "my-thread" を親のコンストラクタに渡しています。コンストラクタは def new() で定義し、その本体は厳密に super(...) の呼び出しでなければなりません。1つの new 式につき、定義できるコンストラクタは最大1つです。
コンストラクタが定義されていない場合、Flix は自動的に親の引数なしコンストラクタを呼び出します。
注意: Java コードとのやり取りは、常に
IOエフェクトを伴います。
注意: Flix では、Java クラスは使用する前に
importされている必要があります。特に、new java.io.File(...)のように書くことは できません。
オブジェクトメソッドの呼び出し
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix では、Java に似た構文を使って、Java オブジェクトのメソッドを呼び出すことができます。
例えば:
import java.io.File
def main(): Unit \ IO =
let f = new File("foo.txt");
println(f.getName())
ここでは java.io.File クラスをインポートし、File オブジェクトをインスタンス化して、そのオブジェクトの getName メソッドを呼び出しています。
コンストラクタの場合と同様に、Flix は引数の数と型に基づいてメソッドを解決します。
別の例を示します:
import java.io.File
def main(): Unit \ IO =
let f = new File("foo.txt");
if (f.exists())
println("The file ${f.getName()} exists!")
else
println("The file ${f.getName()} does not exist!")
さらに大きな例を示します:
import java.io.File
import java.io.FileWriter
def main(): Unit \ IO =
let f = new File("foo.txt");
let w = new FileWriter(f);
w.append("Hello World\n");
w.close()
上記の例では、発生しうる IOException をキャッチしたい場合があるでしょう:
import java.io.File
import java.io.FileWriter
import java.io.IOException
def main(): Unit \ IO =
let f = new File("foo.txt");
try {
let w = new FileWriter(f);
w.append("Hello World\n");
w.close()
} catch {
case ex: IOException =>
println("Unable to write to file: ${f.getName()}");
println("The error message was: ${ex.getMessage()}")
}
静的メソッドの呼び出し
Flix では、Java に似た構文を使って、静的メソッド(すなわちクラスメソッド)を呼び出すことができます。
例えば:
import java.lang.Math
def main(): Unit \ IO =
let n = Math.sin(3.14);
println(n)
コンストラクタやメソッドの場合と同様に、Flix は引数の数と型に基づいて静的メソッドを解決します。
別の例を示します:
import java.lang.Math
def main(): Unit \ IO =
println(Math.abs(-123i32));
println(Math.abs(-123i64));
println(Math.abs(-123.456f32));
println(Math.abs(-123.456f64))
コンストラクタやメソッドの解決に失敗する場合
場合によっては、どの Java コンストラクタやメソッドが呼び出されるのかを Flix コンパイラが決定できないことがあります。
例えば、次のプログラムでは:
import java.lang.{String => JString}
def f(): String \ IO =
let o = ???;
JString.valueOf(o)
o の型が不明であるため、String.valueOf(boolean)、String.valueOf(char)、String.valueOf(double)、あるいはその他のオーバーロードされたバージョンのどれを呼び出したいのか、Flix には分かりません。
解決策は、該当する引数に型注釈(type ascription)を付けることです:
import java.lang.{String => JString}
def f(): String \ IO =
let o = ???;
JString.valueOf((o: Bool))
この型注釈は o が Bool 型であることを指定しており、これによってメソッド解決を正常に完了できるようになります。なお、追加の括弧のペアが必要であることに注意してください。
純粋であると分かっている Java メソッドの呼び出し
Java オブジェクトの生成、Java メソッドの呼び出し、Java 静的メソッドの呼び出しを行う Flix の式は、いずれも IO エフェクトを持ちます。これは当然のことです。Java のコンストラクタやメソッドは、任意の副作用を持ちうるからです。
Java のコンストラクタやメソッドの呼び出しが副作用を持たないと確実に分かっている場合は、unsafe ブロックを使って、その式を純粋なものとして扱うよう Flix に伝えることができます。
例えば:
import java.lang.Math
def pythagoras(x: Float64, y: Float64): Float64 = // 純粋、IO エフェクトなし
unsafe Math.sqrt((Math.pow(x, 2.0) + Math.pow(y, 2.0)))
def main(): Unit \ IO =
println(pythagoras(3.0, 4.0))
ここでは Math.pow と Math.sqrt が純粋な関数であると確実に分かっているため、それらを unsafe ブロックの中に置くことができます。これにより、Flix の pythagoras 関数を純粋なもの、すなわち IO エフェクトを持たないものとして型検査できます。
警告: どのような状況であっても、副作用を持つ式に
unsafeを使わないでください。そうしてしまうと型・エフェクトシステムが破綻し、誤ったコンパイラ最適化が行われ、プログラムの意味が微妙に、あるいは破滅的に変わってしまう可能性があります!
Java コンストラクタとメソッドの部分適用
Flix は Flix 関数の部分適用(partial application)をサポートしています。しかし、Java のコンストラクタやメソッドを部分適用することはできません。この制限は、明示的なラムダを導入することで克服できます。
例えば:
import java.lang.{String => JString}
def main(): Unit \ IO =
def replaceAll(s, src, dst) = s.replaceAll(src, dst);
let f = replaceAll("Hello World");
let s1 = f("World")("Galaxy");
let s2 = f("World")("Universe");
println(s1);
println(s2)
ここでは String.replaceAll を呼び出す Flix 関数 replaceAll を導入しています。replaceAll は Flix 関数なので、例に示すように部分適用することができます。
フィールドの読み書き
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、標準的な Java の構文によるオブジェクトフィールド(Object field)および静的フィールド(Static field)(クラスフィールド)の読み取りをサポートしています。
オブジェクトフィールドの読み取り
オブジェクトフィールドは次のように読み取ることができます:
import java.awt.Point
def area(p: Point): Int32 \ IO = p.x * p.y
静的フィールドの読み取り
静的フィールドは次のように読み取ることができます:
import java.lang.Math
def area(radius: Float64): Float64 = (unsafe Math.PI) * radius * radius
ここでは java.lang.Math クラスをインポートし、静的な PI フィールドにアクセスしています。
PI フィールドは決して変化しないことが分かっているため、unsafe を使ってエフェクトをキャストして取り除いています。
クラスとインターフェースの拡張
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix では、Java のクラスを継承したり、Java のインターフェースを実装したりするオブジェクトを作成できます。
この機能は、概念的には Java の匿名クラス(Anonymous Classes)に似ています。つまり、インターフェースを実装する、あるいはクラスを継承する(名前のない)クラスを定義し、そのクラスのオブジェクトを生成する、という一連の処理を 1 つの式で行うことができます。
例えば、java.lang.Runnable インターフェースを実装するオブジェクトは次のように作成できます:
import java.lang.Runnable
def newRunnable(): Runnable \ IO = new Runnable {
def $run(_this: Runnable): Unit \ IO =
println("I am running!")
}
newRunnable を呼び出すたびに、java.lang.Runnable を実装した新しいオブジェクトが得られます。
注意: 暗黙の
this引数は、new 式の中では常に第 1 引数として明示的に渡されます。
別の例として、java.io.Closeable インターフェースを実装するオブジェクトも作成できます:
import java.io.Closeable
def newClosable(): Closeable \ IO = new Closeable {
def close(_this: Closeable): Unit \ IO =
println("I am closing!")
}
クラスを継承することもできます。例えば、hashCode メソッドと toString メソッドをオーバーライドした java.lang.Object を作成できます:
def newObject(): Object \ IO = new Object {
def hashCode(_this: Object): Int32 = 42
def toString(_this: Object): String = "Hello World!"
}
スーパーメソッドの呼び出し
匿名サブクラスでメソッドをオーバーライドするとき、super.methodName(args) を使って親クラスの実装を呼び出すことができます。
例えば、Thread を継承して toString をオーバーライドし、親クラスのデフォルトの表現を含めることができます:
import java.lang.Thread
def main(): Unit \ IO =
let t = new Thread {
def new(): Thread \ IO = super("my-thread")
def run(_this: Thread): Unit \ IO =
println("Hello from ${Thread.currentThread().getName()}")
def toString(_this: Thread): String \ IO =
"MyThread(" + super.toString() + ")"
};
println(t)
ここで super.toString() は、(親クラスである)Thread が定義する toString メソッドを呼び出し、その結果を "MyThread(...)" で包んでいます。
注意: スーパーメソッド呼び出しは
new式の内部、すなわち匿名サブクラスを定義するときにのみ使用できます。
ネストクラスと内部クラス
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Java は、ネストされた static クラスと、非 static な内部クラス(Inner class)をサポートしています。
例えば:
package Foo.Bar;
class OuterClass {
...
class InnerClass {
...
}
static class StaticInnerClass {
public static String hello() { return "Hi"; }
}
}
Flix では、import 文を使って StaticInnerClass にアクセスできます:
import Foo.Bar.{OuterClass$StaticInnerClass => Inner}
def main(): Unit \ IO =
println(Inner.hello())
典型的な例として、Map.Entry クラスへのアクセスが挙げられます:
import java.util.{Map$Entry => Entry}
注意: Flix は、ネストされた非 static な内部クラスへのアクセスをサポートしていません。
例外
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix では、try-catch 構文を使って Java の例外(Exception)を捕捉できます。この構文は Java のものと似ていますが、文法が少し異なります。
例えば:
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.io.FileNotFoundException
import java.io.IOException
def main(): Unit \ IO =
let f = new File("foo.txt");
try {
let r = new BufferedReader(new FileReader(f));
let l = r.readLine();
println("The first line of the file is: ${l}");
r.close()
} catch {
case _: FileNotFoundException =>
println("The file does not exist!")
case ex: IOException =>
println("The file could not be read!");
println("The error message was: ${ex.getMessage()}")
}
ここでは、new FileReader()、r.readLine()、r.close() の呼び出しが IOException を投げる可能性があります。これらの例外を捕捉するために try-catch ブロックを使っています。また、FileNotFoundException 例外に対しては専用の case を追加しています。
注意: Flix のプログラムでは例外を使うべきではありません。これは悪いスタイルとみなされています。代わりに、プログラムでは
Result[e, t]型を使うべきです。try-catch構文は、Flix と Java のコードの境界でのみ使用してください。
注意: Flix は(まだ)
finallyブロックをサポートしていません。
ボックス化とアンボックス化
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Java とは異なり、Flix が値の暗黙的なボックス化(Boxing)やアンボックス化(Unboxing)を行うことは決してありません。
私たちは、自動ボックス化は設計上の欠陥であると考えており、サポートする予定はありません。したがって、プリミティブ値は手動でボックス化・アンボックス化する必要があります。
ボックス化
次の例は、プリミティブな整数をボックス化する方法を示しています:
def f(x: Int32): String \ IO =
let i = Box.box(x); // Integer
i.toString()
ここで Box.box(x) の呼び出しは Integer オブジェクトを返します。i はオブジェクトなので、toString を呼び出すことができます。ボックス化は純粋な操作ですが、toString の呼び出しは IO エフェクトを持ちます。
アンボックス化
次の例は、2 つの Java の Integer オブジェクトをアンボックス化する方法を示しています:
import java.lang.Integer
def sum(x: Integer, y: Integer): Int32 =
Box.unbox(x) + Box.unbox(y)
ここで Box.unbox の呼び出しは Int32 のプリミティブ値を返します。
アンボックス化は純粋な操作です。
Java のコレクション
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は Java のコレクションとの相互変換をサポートしています。
以下では、次のインポートエイリアスを使用します:
import java.util.{List => JList}
import java.util.{LinkedList => JLinkedList}
import java.util.{ArrayList => JArrayList}
import java.util.{Set => JSet}
import java.util.{TreeSet => JTreeSet}
import java.util.{Map => JMap}
import java.util.{TreeMap => JTreeMap}
次の関数は Adaptor モジュールで利用できます:
Flix から Java へ
次の関数は Flix のコレクションを Java のコレクションに変換します:
///
/// リスト
///
def toList(ma: m[a]): JList[a] \ IO + Aef[m] with Foldable[m]
def toArrayList(ma: m[a]): JArrayList[a] \ IO + Aef[m] with Foldable[m]
def toLinkedList(ma: m[a]): JLinkedList[a] \ IO + Aef[m] with Foldable[m]
///
/// セット
///
def toSet(ma: m[a]): JSet[a] \ IO + Aef[m] with Order[a], Foldable[m]
def toTreeSet(ma: m[a]): JTreeSet[a] \ IO + Aef[m] with Order[a], Foldable[m]
///
/// マップ
///
def toMap(m: Map[k, v]): JMap[k, v] \ IO with Order[k]
def toTreeMap(m: Map[k, v]): JTreeMap[k, v] \ IO with Order[k]
各関数は新しいコレクションを構築し、すべての要素をそこにコピーします。そのため、各操作には少なくとも線形時間がかかります。結果は通常の Java コレクションであり、変更することもできます。
Java から Flix へ
次の関数は Java のコレクションを Flix のコレクションに変換します:
/// リスト
def fromList(l: JList[a]): List[a]
/// セット
def fromSet(l: JSet[a]): Set[a] with Order[a]
/// マップ
def fromMap(m: JMap[k, v]): Map[k, v] with Order[k]
各関数は Java のコレクションから新しい Flix のコレクションを構築します。そのため、各操作には少なくとも線形時間がかかります。なお、Set と Map については、Flix のコレクションは a に定義された Order[a] インスタンスを使用します。これは Java が使用する順序と同じであるとは限りません。
警告: プリミティブ値を持つ Flix や Java のコレクションを変換する際には、特別な注意が必要です。特に、変換の前に値を手動でボックス化(boxing)またはアンボックス化(unboxing)しなければなりません。
不動点
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix のユニークな機能のひとつに、*関係に対する制約(constraint on relations)および束に対する制約(constraint on lattices)*の不動点計算(Fixpoint computation)を言語組み込みでサポートしていることが挙げられます。
ここでは読者がすでに Datalog に精通していることを前提とし、Flix 固有の機能に焦点を当てます。
関係に対する制約を Flix で解く
Flix では、関数の内部で不動点計算を実行できます。
例えば、辺の集合 s、始点ノード src、終点ノード dst が与えられたとき、src から dst への経路が存在するかどうかを計算してみましょう。この問題は次のようにエレガントに解くことができます:
def isConnected(s: Set[(Int32, Int32)], src: Int32, dst: Int32): Bool =
let rules = #{
Path(x, y) :- Edge(x, y).
Path(x, z) :- Path(x, y), Edge(y, z).
};
let edges = inject s into Edge/2;
let paths = query edges, rules select true from Path(src, dst);
not (paths |> Vector.isEmpty)
def main(): Unit \ IO =
let s = Set#{(1, 2), (2, 3), (3, 4), (4, 5)};
let src = 1;
let dst = 5;
if (isConnected(s, src, dst)) {
println("Found a path between ${src} and ${dst}!")
} else {
println("Did not find a path between ${src} and ${dst}!")
}
isConnected 関数は他の関数とまったく同じように振る舞います。辺の集合(Int32 のペア)、Int32 の始点ノード、Int32 の終点ノードを渡して呼び出すことができます。isConnected の興味深い点は、その実装が小さな Datalog プログラムを使って目的のタスクを解いていることです。
isConnected 関数の中で、ローカル変数 rules は、Path 関係を定義する 2 つのルールからなる Datalog プログラムの断片を保持しています。述語シンボルである Edge と Path は明示的に導入する必要がなく、単に使うだけでよいことに注意してください。ローカル変数 edges は、集合 s のすべてのタプルを Edge ファクトに変換して得られる、辺のファクトのコレクションを保持しています。次に、ローカル変数 paths は、これらのファクトとルール(edges と rules)の不動点を計算し、Path(src, dst) というファクトが存在する場合にブール値 true を選択した結果を保持します。ここでの src と dst は、レキシカルに束縛された関数のパラメータであることに注意してください。したがって、paths は空の配列(経路が見つからなかった)か、要素が 1 つの配列(経路が見つかった)のいずれかになり、これをそのまま結果として返します。
Flix は強く型付けされた言語です。誤った型の項(あるいは誤ったアリティ)で述語シンボルを使おうとすると、型検査器によって検出されます。また、Flix は型推論をサポートしているため、Edge や Path の型を宣言する必要がなかったことにも注目してください。
第一級制約によるプログラミング
Flix のもうひとつのユニークな機能が、*第一級制約(First-class constraints)*のサポートです。第一級制約とは、構築し、受け渡し、他の制約と合成し、最終的に解くことができる値のことです。制約システムの解はまた別の制約システムであり、それをさらに合成することができます。例えば:
def getParents(): #{ ParentOf(String, String) | r } = #{
ParentOf("Pompey", "Strabo").
ParentOf("Gnaeus", "Pompey").
ParentOf("Pompeia", "Pompey").
ParentOf("Sextus", "Pompey").
}
def getAdoptions(): #{ AdoptedBy(String, String) | r } = #{
AdoptedBy("Augustus", "Caesar").
AdoptedBy("Tiberius", "Augustus").
}
def withAncestors(): #{ ParentOf(String, String),
AncestorOf(String, String) | r } = #{
AncestorOf(x, y) :- ParentOf(x, y).
AncestorOf(x, z) :- AncestorOf(x, y), AncestorOf(y, z).
}
def withAdoptions(): #{ AdoptedBy(String, String),
AncestorOf(String, String) | r } = #{
AncestorOf(x, y) :- AdoptedBy(x, y).
}
def main(): Unit \ IO =
let c = false;
if (c) {
query getParents(), getAdoptions(), withAncestors()
select (x, y) from AncestorOf(x, y) |> println
} else {
query getParents(), getAdoptions(), withAncestors(), withAdoptions()
select (x, y) from AncestorOf(x, y) |> println
}
このプログラムは ParentOf、AncestorOf、AdoptedBy という 3 つの述語シンボルを使っています。getParents 関数は生物学上の親を表すファクトのコレクションを返し、一方 getAdoptions 関数は養子縁組を表すファクトのコレクションを返します。withAncestors 関数は、ParentOf 関係を使って AncestorOf 関係を導出する 2 つの制約を返します。withAdoptions 関数は、AdoptedBy 関係を使って ParentOf 関係を導出する制約を返します。
main 関数では、ローカル変数 c によって、生物学上の親のみを考慮する Datalog プログラムに問い合わせるか、養子縁組も含めるかを制御しています。
見てのとおり、これらの関数の型は行多相(Row-polymorphic)です。例えば getParents のシグネチャは def getParents(): #{ ParentOf | r } であり、ここで r は、この関数の結果と合成できる残りの述語を表す行多相型変数です。
設計ノート
行多相型は、制約システムに現れうる述語の過大近似として理解するのが最も適切です。 例えば、ある制約システムが
#{ A(String), B(Int32, Int32) }という型を持つ場合、 それは述語シンボルAやBを使うファクトやルールが必ず含まれることを意味するわけではありませんが、 述語シンボルCを参照するファクトやルールが一切含まれないことは保証されます。
多相な第一級制約
Flix のさらにもうひとつのユニークな機能が、多相的な第一級制約のサポートです。つまり、1 つ以上の制約が、その項の型について多相であるような制約です。例えば:
def edgesWithNumbers(): #{ LabelledEdge(String, Int32 , String) | r } = #{
LabelledEdge("a", 1, "b").
LabelledEdge("b", 1, "c").
LabelledEdge("c", 2, "d").
}
def edgesWithColor(): #{ LabelledEdge(String, String, String) | r } = #{
LabelledEdge("a", "red", "b").
LabelledEdge("b", "red", "c").
LabelledEdge("c", "blu", "d").
}
def closure(): #{ LabelledEdge(String, l, String),
LabelledPath(String, l, String) } with Order[l] = #{
LabelledPath(x, l, y) :- LabelledEdge(x, l, y).
LabelledPath(x, l, z) :- LabelledPath(x, l, y), LabelledPath(y, l, z).
}
def main(): Unit \ IO =
query edgesWithNumbers(), closure()
select (x, l, z) from LabelledPath(x, l, z) |> println;
query edgesWithColor(), closure()
select (x, l, z) from LabelledPath(x, l, z) |> println
ここでは LabelledEdge と LabelledPath という 2 つの述語シンボルを使っています。各述語は l という型パラメータを持ち、辺や経路に付随する「ラベル」の型について多相になっています。edgesWithNumbers はラベルが整数である辺ファクトのコレクションを返し、一方 edgesWithColor はラベルが文字列であるファクトのコレクションを返していることに注目してください。closure 関数は多相であり、同じラベルを持つ辺の推移閉包(Transitive closure)を計算する 2 つのルールを返します。
Flix の型システムにより、異なる型のラベルを持つ辺(や経路)を誤って混在させることはできないようになっています。
Datalog へのファクトの注入
Flix には、関数型のデータ構造(リスト、セット、マップなど)を Datalog のファクトに変換できる柔軟なメカニズムが用意されています。
例えば、ペアの Flix リストが与えられたとき、それを Datalog のファクトのコレクションに変換できます:
let l = (1, 2) :: (2, 3) :: Nil;
let p = inject l into Edge/2;
ここで l の型は List[(Int32, Int32)] です。inject 式は l を、型 #{ Edge(Int32, Int32) | ... } の Datalog 制約集合 p に変換します。この式には述語のアリティを指定します:Edge/2。一般的な形式は Predicate/Arity です。
inject 式は、Foldable トレイトを実装する任意の型に対して使えます。そのため、リスト、セット、マップなどで利用できます。
inject 式は複数のコレクションを同時に扱うこともできます。例えば:
let names = "Lucky Luke" :: "Luke Skywalker" :: Nil;
let jedis = "Luke Skywalker" :: Nil;
let p = inject names, jedis into Name/1, Jedi/1;
ここで p の型は #{ Name(String), Jedi(String) | ... } です。
不動点計算のパイプライン
制約システムの解(すなわち不動点)は、また別の制約システムです。これを利用して、不動点計算のパイプラインを構築できます。つまり、ある不動点計算の結果を別の不動点計算に入力として渡すことができます。例えば:
def main(): Unit \ IO =
let f1 = #{
ColorEdge(1, "blue", 2).
ColorEdge(2, "blue", 3).
ColorEdge(3, "red", 4).
};
let r1 = #{
ColorPath(x, c, y) :- ColorEdge(x, c, y).
ColorPath(x, c, z) :- ColorPath(x, c, y), ColorEdge(y, c, z).
};
let r2 = #{
ColorlessPath(x, y) :- ColorPath(x, _, y).
};
let m = solve f1, r1 project ColorPath;
query m, r2 select (x, y) from ColorlessPath(x, y) |> println
このプログラムは ColorEdge、ColorPath、ColorlessPath という 3 つの述語を使っています。目標は、色付きの辺の推移閉包を計算し、その後で辺に色のないグラフを構築することです。
このプログラムはまず f1 と r1 の不動点を計算し、ColorPath ファクトを取り出します。その結果は m に格納されます。次に、m と r2 に問い合わせて、すべての ColorlessPath ファクトを選択します。
層化否定
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、ルール本体で否定を制限付きで使用できるようにする 層化否定(stratified negation) をサポートしています。例えば:
def main(): Unit \ IO =
let movies = #{
Movie("The Hateful Eight").
Movie("Interstellar").
};
let actors = #{
StarringIn("The Hateful Eight", "Samuel L. Jackson").
StarringIn("The Hateful Eight", "Kurt Russel").
StarringIn("The Hateful Eight", "Quentin Tarantino").
StarringIn("Interstellar", "Matthew McConaughey").
StarringIn("Interstellar", "Anne Hathaway").
};
let directors = #{
DirectedBy("The Hateful Eight", "Quentin Tarantino").
DirectedBy("Interstellar", "Christopher Nolan").
};
let rule = #{
MovieWithoutDirector(title) :-
Movie(title),
DirectedBy(title, name),
not StarringIn(title, name).
};
query movies, actors, directors, rule
select title from MovieWithoutDirector(title) |> println
このプログラムは、映画・俳優・監督に関する情報を保持する 3 つのローカル変数を定義しています。ローカル変数 rule には、監督がその映画に出演していないすべての映画を捉えるルールが含まれています。このルールで否定が使われている点に注目してください。クエリは、文字列 "Interstellar" を含む配列を返します。これは、Christopher Nolan がその映画に出演していないためです。
注意: Flix は、プログラムが層化されていること、すなわち、否定が使用されている箇所に再帰的な依存関係が存在しないことを強制します。もし存在する場合、Flix コンパイラはそのプログラムを拒否します。
ローカル述語
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、local predicates(ローカル述語) と呼ばれる抽象化の仕組みをサポートしています。ローカル述語は、ローカル変数と同じように、外部からは見えません。
ローカル述語を理解するために、次の例を考えてみましょう。グラフに閉路があるかどうかを計算する Datalog プログラム値を返す関数を書くことができます:
def cyclic(): #{Edge(Int32, Int32), Path(Int32, Int32), Cyclic()} = #{
Path(x, y) :- Edge(x, y).
Path(x, z) :- Path(x, y), Edge(y, z).
Cyclic() :- Path(x, x).
}
def main(): Unit \ IO =
let db = #{
Edge(1, 2).
Edge(2, 3).
Edge(3, 1).
};
query db, cyclic() select true from Cyclic() |> println
ここで cyclic 関数は、辺からなるグラフの推移閉包と、ある頂点からその頂点自身への経路が存在するかどうかを計算する 3 つのルールで構成された、Datalog program value(Datalog プログラム値) を返します。main の中で cyclic 関数を使って、db で与えられる小さなグラフに閉路があるかどうかを判定しています。このプログラムをコンパイルして実行すると、Vector#{true} が出力されます。
cyclic に話を戻すと、その型は次のようになっています:
def cyclic(): #{Edge(Int32, Int32), Path(Int32, Int32), Cyclic()} = ...
この Datalog プログラム値は述語シンボル Edge、Path、Cyclic をそれぞれの型で使用しているので、この型は妥当です。しかし、もう少し考えてみると、Path 述語は実際にはこの計算にとってローカルなものであることに気づきます。外部から見えることを意図したものではなく、実装の詳細なのです!本当に望ましいのは、Edge(Int32, Int32) が 入力 であり、Cyclic() が 出力 であることです。さらに重要なのは、Path(Int32, Int32) は外部から見えてはならず、型の一部であってもならないということです。これは predicate abstraction(述語抽象) によって実現できます:
def cyclic(): #{Edge(Int32, Int32), Cyclic()} =
#(Edge, Cyclic) -> #{
Path(x, y) :- Edge(x, y).
Path(x, z) :- Path(x, y), Edge(y, z).
Cyclic() :- Path(x, x).
}
ここでは #(Edge, Cyclic) -> v という構文を使って、v の中の述語のうち Edge と Cyclic だけ を外部から見えるようにすることを指定しています。これにより、cyclic の戻り値の型から Path(Int32, Int32) を省略できます。さらに、この Datalog プログラム値には、参照可能な Path 述語シンボルはもはや含まれていません。このことは、次のプログラムを観察することで確かめられます:
def main(): Unit \ IO =
let db = #{
Edge(1, 2).
Edge(2, 3).
Edge(3, 1).
};
query db, cyclic() select (x, y) from Path(x, y) |> println
このプログラムは空のベクター Vector#{} を出力します。述語抽象によって Path がローカルになっているためです。
関数述語
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
論理述語を使いたいものの、そのタプルをすべて網羅的に列挙することは避けたい、という状況に遭遇することがあります。
例えば、prime が範囲 [from; to] 内の素数であるときに成り立つ述語 PrimeInRange(from, to, prime) が欲しい状況を考えてみましょう。このような述語を思い描くことはできますが、実際に計算するのは現実的ではありません。その代わりに多くの場合で望まれるのは、PrimeInRange を 関数述語(Functional)、すなわち from と to を 入力 として受け取り、素数の集合を 出力 として生成する関数として扱うことです。具体的には、次のようなルールを書きたいとします。
R(p) :- P(x), Q(y), PrimeInRange(x, y, p).
ただし、すべての x、y、p に対して PrimeInRange を評価することは避けたいのです。
これは次のようにして実現できます。まず、関数を書きます。
def primesInRange(b: Int32, e: Int32): Vector[Int32] =
Vector.range(b, e) |> Vector.filter(isPrime)
重要なのは、primesInRange が、開始インデックス b と終了インデックス e を受け取ってタプル(この場合は単一要素)の Vector を返す 関数 であるという点です。これにより、primesInRange は私たちが関心を持つタプルを効率的に計算できます。これをルールの中で使うには、次のように書きます。
R(p) :- P(b), Q(e), let p = primesInRange(b, e).
ここでは、b と e が primesInRange の入力として、p がその出力として明確に識別されています。具体的には、Flix は b と e が正に束縛されている(Positively bound)こと(すなわち、非否定のボディアトム——この場合は P と Q ——によって束縛されていること)を要求します。この例では primesInRange は Int32 の Vector を返しますが、一般に関数述語はタプルの Vector を返すことができます。
注意: 現在の実装における重要な制限として、関数述語の左辺(LHS)にある変数は再束縛してはいけません。つまり、関数述語が
let (a, b) = f(x, y)という形式である場合、aとbはそのルールの中で再束縛されてはいけません。
Flix を使って束上の制約を解く
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、関係に対する制約 だけでなく、束(Lattice)に対する制約 もサポートしています。このような制約を作るには、まず束の演算(半順序(Partial order)、最小上界(Least upper bound)など)を関数として定義し、それらをある型に関連付け、そして束意味論(Lattice semantics)を持つ述語シンボルを宣言する必要があります。
まず、Sign データ型の定義から始めます:
use Sign.{Top, Neg, Zer, Pos, Bot};
enum Sign {
case Top,
case Neg,
case Zer,
case Pos,
case Bot
}
この新しい型に対して、いつもの Eq・Order・ToString の各インスタンスを定義する必要があります。この Order インスタンスは、後で定義する半順序のインスタンスとは無関係であり、単に整形出力などのために要素をソートする目的で使われる点に注意してください。
instance Eq[Sign] {
pub def eq(x: Sign, y: Sign): Bool = match (x, y) {
case (Bot, Bot) => true
case (Neg, Neg) => true
case (Zer, Zer) => true
case (Pos, Pos) => true
case (Top, Top) => true
case _ => false
}
}
instance Order[Sign] {
pub def compare(x: Sign, y: Sign): Comparison =
let num = w -> match w {
case Bot => 0
case Neg => 1
case Zer => 2
case Pos => 3
case Top => 4
};
num(x) <=> num(y)
}
instance ToString[Sign] {
pub def toString(x: Sign): String = match x {
case Bot => "Bot"
case Neg => "Neg"
case Zer => "Zer"
case Pos => "Pos"
case Top => "Top"
}
}
これらのトレイトインスタンスが揃ったので、Sign に対する束の演算を定義できるようになりました。
ボトム要素(Bottom element)と半順序を定義します:
instance LowerBound[Sign] {
pub def minValue(): Sign = Bot
}
instance PartialOrder[Sign] {
pub def lessEqual(x: Sign, y: Sign): Bool =
match (x, y) {
case (Bot, _) => true
case (Neg, Neg) => true
case (Zer, Zer) => true
case (Pos, Pos) => true
case (_, Top) => true
case _ => false
}
}
次に、最小上界と最大下界(Greatest lower bound)を定義します:
instance JoinLattice[Sign] {
pub def leastUpperBound(x: Sign, y: Sign): Sign =
match (x, y) {
case (Bot, _) => y
case (_, Bot) => x
case (Neg, Neg) => Neg
case (Zer, Zer) => Zer
case (Pos, Pos) => Pos
case _ => Top
}
}
instance MeetLattice[Sign] {
pub def greatestLowerBound(x: Sign, y: Sign): Sign =
match (x, y) {
case (Top, _) => y
case (_, Top) => x
case (Neg, Neg) => Neg
case (Zer, Zer) => Zer
case (Pos, Pos) => Pos
case _ => Bot
}
}
これらの定義がすべて揃えば、束意味論を持つ Datalog 制約を書く準備は完了です。しかし先に進む前に、単調関数(Monotone function)をひとつ書いておきましょう:
def sum(x: Sign, y: Sign): Sign = match (x, y) {
case (Bot, _) => Bot
case (_, Bot) => Bot
case (Neg, Zer) => Neg
case (Zer, Neg) => Neg
case (Zer, Zer) => Zer
case (Zer, Pos) => Pos
case (Pos, Zer) => Pos
case (Pos, Pos) => Pos
case _ => Top
}
これでようやく、すべてを組み合わせて使うことができます:
pub def main(): Unit \ IO =
let p = #{
LocalVar("x"; Pos).
LocalVar("y"; Zer).
LocalVar("z"; Neg).
AddStm("r1", "x", "y").
AddStm("r2", "x", "y").
AddStm("r2", "y", "z").
LocalVar(r; sum(v1, v2)) :-
AddStm(r, x, y), LocalVar(x; v1), LocalVar(y; v2).
};
query p select (r, v) from LocalVar(r; v) |> println
束意味論を示すために ; が注意深く使われている点に注目してください。
束意味論を使った最短経路の計算
束意味論は、最短経路の計算にも使えます。
鍵となるのは、独自の新しいデータ型 D を定義することです。これは単なる Int32 ですが、整数の逆順によって束をなします(つまり、最小の要素は Int32.maxValue() です)。
use D.D;
pub enum D with Eq, Order, ToString {
case D(Int32)
}
instance PartialOrder[D] {
pub def lessEqual(x: D, y: D): Bool =
let D(n1) = x;
let D(n2) = y;
n1 >= n2 // 注意:順序が反転しています。
}
instance LowerBound[D] {
// 注意:順序が反転しているため、最大の値が最小の要素になります。
pub def minValue(): D = D(Int32.maxValue())
}
instance UpperBound[D] {
// 注意:順序が反転しているため、最小の値が最大の要素になります。
pub def maxValue(): D = D(Int32.minValue())
}
instance JoinLattice[D] {
pub def leastUpperBound(x: D, y: D): D =
let D(n1) = x;
let D(n2) = y;
D(Int32.min(n1, n2)) // 注意:順序が反転しています。
}
instance MeetLattice[D] {
pub def greatestLowerBound(x: D, y: D): D =
let D(n1) = x;
let D(n2) = y;
D(Int32.max(n1, n2)) // 注意:順序が反転しています。
}
def shortestPath(g: Set[(t, Int32, t)], o: t): Map[t, D] with Order[t] =
let db = inject g into Edge/3;
let pr = #{
Dist(o; D(0)).
Dist(y; add(d1 , D(d2))) :- Dist(x; d1), Edge(x, d2, y).
};
query db, pr select (x , d) from Dist(x; d) |> Vector.toMap
def add(x: D, y: D): D =
let D(n1) = x;
let D(n2) = y;
D(n1 + n2)
def main(): Unit \ IO =
let g = Set#{
("Aarhus", 200, "Flensburg"),
("Flensburg", 150, "Hamburg")
};
println(shortestPath(g, "Aarhus"))
実は、Flix には D のような型が組み込みで用意されています。それは Down と呼ばれ、基となる型の順序を単純に反転させます。これを使うと、プログラムは次のように書けます:
use Down.Down;
def shortestPaths(g: Set[(t, Int32, t)], o: t): Map[t, Down[Int32]] with Order[t] =
let db = inject g into Edge/3;
let pr = #{
Dist(o; Down(0)).
Dist(y; add(d1 , Down(d2))) :- Dist(x; d1), Edge(x, d2, y).
};
query db, pr select (x , d) from Dist(x; d) |> Vector.toMap
def add(x: Down[Int32], y: Down[Int32]): Down[Int32] =
let Down(n1) = x;
let Down(n2) = y;
Down(n1 + n2)
def main(): Unit \ IO =
let g = Set#{
("Aarhus", 200, "Flensburg"),
("Flensburg", 150, "Hamburg")
};
println(shortestPaths(g, "Aarhus"))
日常のプログラミング
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
この章では、日常のプログラミングで役立つ機能をいくつか紹介します:
main関数のシグネチャ。- 標準出力と標準エラー出力への出力方法。
- 文字列補間の使い方。
- 未完成のプログラムで匿名ホールおよび名前付きホールを使う方法。
- 型注釈(Type ascription)を使って、コンパイラに一部の型を明示的に伝える方法。
main 関数
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
すべての Flix プログラムのエントリーポイント(Entry point)は main 関数です。main 関数は引数を取らず、Unit を返す必要があります:
def main(): Unit \ IO =
println("Hello World!")
main のエフェクト
main 関数では、次のものを自由に組み合わせて使うことができます:
- プリミティブエフェクト:
IOとNonDet。 - デフォルトハンドラを持つ任意のエフェクト: 例えば
Env、Exit、Clock、Loggerなど。
デフォルトハンドラを持つエフェクトは、Flix コンパイラによって自動的に IO へと変換されます。詳細はデフォルトハンドラを参照してください。
例えば、main では Env と Exit エフェクトを使うことができます:
use Sys.Env
use Sys.Exit
def main(): Unit \ {Env, Exit} =
let args = Env.getArgs();
match List.head(args) {
case None =>
println("Missing argument.");
Exit.exit(1)
case Some(a) =>
println("Hello ${a}!")
}
コマンドライン引数へのアクセス
プログラムに渡されたコマンドライン引数には、Env エフェクトを通じて Env.getArgs() を呼び出すことでアクセスできます:
use Sys.Env
def main(): Unit \ {Env, IO} =
let args = Env.getArgs();
println("Arguments: ${args}")
プログラムの終了
Exit.exit を使うと、特定の終了コード(Exit code)を指定してプログラムを終了できます:
use Sys.Exit
def main(): Unit \ Exit =
Exit.exit(0)
なぜ main はエフェクトを持たなければならないのか?
Flix は、main がエフェクトを持つことを要求します。もし main が純粋であれば、そのプログラムを実行する理由がないからです。通常、この要件は main がコンソールへ出力したり、その他の副作用を持つことで満たされます。
標準出力への出力
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix の Prelude(プレリュード)には、標準出力へ出力する println 関数が定義されています。例えば:
println("Hello World")
println 関数は、型が ToString トレイトを実装している値、すなわち String に変換できる値であれば、どのような値でも出力できます。例えば:
let o = Some(123);
let l = 1 :: 2 :: 3 :: Nil;
println(o);
println(l)
println 関数は当然ながらエフェクトを持つ関数であるため、純粋関数から呼び出すことはできません。純粋関数をデバッグするには、組み込みのデバッグ機能を使用してください。
Console エフェクト
Console エフェクトは、ターミナルからの読み取りとターミナルへの書き込みを行う操作を定義しています:
eff Console {
def readln(): String
def print(s: String): Unit
def eprint(s: String): Unit
def println(s: String): Unit
def eprintln(s: String): Unit
}
文字列補間
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix の文字列は補間をサポートしています。文字列の中で "${e}" という形式を書くと、e を評価して値にし、ToString トレイトを使って文字列に変換します。例えば:
let fstName = "Lucky";
let lstName = "Luke";
"Hello Mr. ${lstName}. Do you feel ${fstName}, punk?"
文字列補間(String interpolation)は、ToString インスタンスを実装している任意の型に対して機能します。例えば:
let i = 123;
let o = Some(123);
let l = 1 :: 2 :: 3 :: Nil;
"i = ${i}, o = ${o}, l = ${l}"
文字列補間には任意の式を含めることができます。例えば:
let x = 1;
let y = 2;
"${x + y + 1}"
2 つの文字列を連結するには、文字列補間を使うのが推奨される方法です:
let x = "Hello";
let y = "World";
"${x}${y}" // x + y と同等
値を文字列に変換する場合も、文字列補間を使うのが推奨される方法です:
let o = Some(123);
"${o}"
これは、ToString トレイトの toString 関数を明示的に使うのと同等です:
ToString.toString(o)
文字列補間はネストさせることもできますが、推奨されません。
末尾再帰
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix では、そして一般に関数型プログラミングでは、反復処理は再帰によって表現されます。
例えば、リストがある要素を含むかどうかを判定したい場合、再帰関数を次のように書くことができます:
def memberOf(x: a, l: List[a]): Bool with Eq[a] =
match l {
case Nil => false
case y :: ys => if (x == y) true else memberOf(x, ys)
}
memberOf 関数はリスト l に対してパターンマッチを行います。リストが空であれば false を返します。そうでなければ、要素 y とリストの残り ys が得られます。x == y であれば要素が見つかったので true を返します。そうでなければ、リストの残り ys に対して 再帰 します。
memberOf への再帰呼び出しは 末尾位置(Tail position) にあります。つまり、memberOf 関数の中で最後に行われる処理です。これには 2 つの重要な利点があります。(a) Flix コンパイラが memberOf を(関数呼び出しよりも効率的な)通常のループに書き換えられること、そしてより重要なのは (b) 呼び出しスタックの高さが決して増えないため、memberOf の呼び出しでスタックがオーバーフローすることが あり得ない ことです。
ヒント: Flix は完全な末尾呼び出し除去をサポートしています。これは、末尾位置にある再帰呼び出しがスタックの高さを決して増やさず、したがってスタックオーバーフローを引き起こし得ないことを意味します!
特筆すべき点として、Flix が備えているのは単なる末尾呼び出し最適化ではなく、完全な 末尾呼び出し除去です。これにより、次のプログラムは正常にコンパイルされ、実行されます:
def isOdd(n: Int32): Bool =
if (n == 0) false else isEvn(n - 1)
def isEvn(n: Int32): Bool =
if (n == 0) true else isOdd(n - 1)
def main(): Unit \ IO =
isOdd(12345) |> println
これは他の多くのプログラミング言語では成り立たないことです。
非末尾呼び出しとスタックオーバーフロー
Flix コンパイラは末尾呼び出しがスタックをオーバーフローさせないことを 保証 しますが、末尾位置にない関数呼び出しについては同じことは言えません。
例えば、次の階乗関数の実装は呼び出しスタックをオーバーフローさせます:
def factorial(n: Int32): Int32 = match n {
case 0 => 1
case _ => n * factorial(n - 1)
}
それは次のプログラムで確認できます:
def main(): Unit \ IO =
println(factorial(1_000_000))
これをコンパイルして実行すると、次の出力が得られます:
java : Exception in thread "main" java.lang.StackOverflowError
at Cont%Int32.unwind(Cont%Int32)
at Def%factorial.invoke(Unknown Source)
at Cont%Int32.unwind(Cont%Int32)
at Def%factorial.invoke(Unknown Source)
at Cont%Int32.unwind(Cont%Int32)
... many more frames ...
よく知られたテクニックとして、factorial をアキュムレータ(Accumulator)を使う形に書き換える方法があります:
def factorial(n: Int32): Int32 =
def visit(x, acc) = match x {
case 0 => acc
case _ => visit(x - 1, x * acc)
};
visit(n, 1)
ここでは visit 関数が末尾再帰になっているため、スタックをオーバーフローさせることはありません。
@Tailrec アノテーション
Flix は @Tailrec アノテーションを提供しています。これは、関数内のすべての自己再帰呼び出しが末尾位置にあることを検証するようコンパイラに指示するものです。このアノテーションは省略可能で、実行時の挙動を変えることはありません。ドキュメントおよび検証のためのツールとして機能します。
例えば、アキュムレータスタイルの sum 関数は末尾再帰です:
@Tailrec
def sum(l: List[Int32], acc: Int32): Int32 = match l {
case Nil => acc
case x :: xs => sum(xs, acc + x)
}
sum への再帰呼び出しは関数内の最後の操作であり、その結果に対してそれ以上の処理は行われないため、コンパイラはこれを受理します。
対照的に、次の関数は拒否されます:
@Tailrec
def length(l: List[Int32]): Int32 = match l {
case Nil => 0
case _ :: xs => length(xs) + 1
}
ここでは length(xs) の結果が加算(+ 1)に使われているため、再帰呼び出しは末尾位置に ありません。コンパイラは次のエラーを報告します:
>> Non-tail recursive call in @Tailrec function 'length'.
... length(xs) + 1
^^^^^^^^^^
non-tail recursive call
これを修正するには、前述のようにアキュムレータを使う形に関数を書き換えます。
ヒント:
@Tailrecアノテーションは純粋にコンパイル時のチェックです。コード生成には影響しません。Flix はアノテーションの有無にかかわらず、末尾位置にあるあらゆる呼び出しに対して既に完全な末尾呼び出し除去を行います。コードが進化しても関数が末尾再帰の ままである ことをコンパイラに保証してほしい場合に、@Tailrecを使ってください。
匿名ホールと名前付きホール
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix では、開発中の未完成なコードに Hole(ホール)を使うことが推奨されています。例えば:
def sum(x: Int32, y: Int32): Int32 = ???
3 つ並んだクエスチョンマーク ??? は匿名ホールを表し、式が期待される場所ならどこでも使えます。上記のコードでは、??? はまだ書かれていない関数本体を表していますが、式の内部でも使えます。例えば:
def length(l: List[a]): Int32 = match l {
case Nil => 0
case x :: xs => ???
}
プログラムに複数のホールがある場合は、それぞれに名前を付けると便利です。例えば:
def length(l: List[a]): Int32 = match l {
case Nil => ?base
case x :: xs => ?step
}
Flix では、各名前付きホールは一意な名前を持つ必要があります。
変数ホールと自動補完
Flix は、型駆動の自動補完候補を提示できる特別な 変数ホール をサポートしています。例えば、次のプログラムで:
def main(): Unit \ IO =
let s: String = "Hello World";
let n: Int32 = s?;
println("The length of ${s} is ${n}!")
カーソルを s? の上に置いて自動補完候補を要求すると、Flix は次のような候補を提示します:
String.length(s: String): Int32String.countSubstring(substr: {substr: String}, s: String): Int32String.levenshtein(s: String, t: String): Int32- …
これらは String を Int32 に変換できる関数だからです。
別の例として、次のプログラムで:
def main(): Unit \ IO =
let l: List[Int32] = List.range(1, 10);
let n: Int32 = l?;
println("The value of `n` is ${n}.")
カーソルを l? の上に置くと、Flix は次のような候補を提示します:
List.product(l: List[Int32]): Int32List.sum(l: List[Int32]): Int32List.fold(l: List[Int32]): Int32List.length(l: List[Int32]): Int32List.count(f: a -> Bool \ ef, l: List[a]): a \ ef- …
これらは List[Int32] を Int32 に変換できる関数だからです。
型注釈
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix はローカル型推論をサポートしていますが、式や let 束縛にその型を注釈しておくと便利な場合があります。このような注釈を型注釈(Type ascription)と呼びます。型注釈によって式の型を変えることはできず、型安全性を破るために使うこともできません。
型注釈は式の後ろに置くことができます:
(("Hello" :: "World" :: Nil) : List[String])
ただし、他の式と区別するために括弧で囲む必要があります。
let 束縛に対しては、括弧なしで型注釈を置くこともできます:
let l: List[String] = "Hello" :: "World" :: Nil
カインド注釈
Flix はカインド注釈(Kind ascription)もサポートしています。型注釈が_式_の_型_を指定するのに対して、カインド注釈は_型_の_カインド_を指定します。
カインド注釈は型パラメータに対して使うことができます。例えば:
def fst1[a: Type, b: Type](p: (a, b)): a = let (x, _) = p; x
ここでは、2 つの型パラメータ a と b の_カインド_が Type であることを指定しています。このようなカインドは推論できるため、通常は指定する必要はありません。
代数的データ型にカインド注釈を付けることもできます:
enum A[t: Type] {
case A(t, t)
}
トレイトにも付けられます:
trait MyTrait[t: Type] {
// ...
}
カインド注釈は通常、高カインド型に対してのみ使用します。
冗長性
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix コンパイラは、未使用の要素を含むプログラムを積極的に拒否します。これは、プログラマーが分かりにくいバグを避けられるように支援するためのものです[^1]。慣れるまで少し時間がかかるかもしれませんが、それに見合う価値のあるトレードオフだと私たちは考えています。
具体的には、Flix コンパイラはプログラムが次のものを含まないことを保証します:
- 未使用のローカル変数:宣言されているものの、一度も使用されないローカル変数。
- シャドーイングされたローカル変数:他のローカル変数をシャドーイングするローカル変数。
- 無意味な式:値が破棄される純粋な式。
- 使用必須の値:値が未使用であるものの、その型が
@MustUseとしてマークされている式。
未使用のローカル変数(Unused Local Variables)
Flix は未使用の変数を含むプログラムを拒否します。
例えば、次のプログラムは拒否されます:
def main(): Unit \ IO =
let x = 123;
let y = 456;
println("The sum is ${x + x}")
エラーメッセージは次のとおりです:
❌ -- Redundancy Error -------------------------------------------------- Main.flix
>> Unused local variable 'y'. The variable is not referenced within its scope.
3 | let y = 456;
^
unused local variable.
未使用のローカル変数は、アンダースコア _ を接頭辞として付けることで、このエラーを抑制できます。例えば、y を _y に置き換えると、上記のプログラムはコンパイルできます:
def main(): Unit \ IO =
let x = 123;
let _y = 456; // OK
println("The sum is ${x + x}")
シャドーイングされたローカル変数(Shadowed Local Variables)
Flix はシャドーイング(Shadowing)された変数を含むプログラムを拒否します。
例えば、次のプログラムは拒否されます:
def main(): Unit \ IO =
let x = 123;
let x = 456;
println("The value of x is ${x}.")
エラーメッセージは次のとおりです:
❌ -- Redundancy Error -------------------------------------------------- Main.flix
>> Shadowed variable 'x'.
3 | let x = 456;
^
shadowing variable.
The shadowed variable was declared here:
2 | let x = 123;
^
shadowed variable.
無意味な式(Useless Expressions)
Flix は、結果が破棄される純粋な式を含むプログラムを拒否します。
例えば、次のプログラムは拒否されます:
def main(): Unit \ IO =
123 + 456;
println("Hello World!")
エラーメッセージは次のとおりです:
❌ -- Redundancy Error -------------------------------------------------- Main.flix
>> Useless expression: It has no side-effect(s) and its result is discarded.
2 | 123 + 456;
^^^^^^^^^
useless expression.
The expression has type 'Int32'
副作用を持たず、かつ結果も使用されない式は疑わしいものです。なぜなら、その式はプログラムの意味を変えることなく、そのまま削除できてしまうからです。
使用必須の値(Must Use Values)
Flix は、値が破棄されるにもかかわらず、その型に @MustUse アノテーションが付けられている式を含むプログラムを拒否します。関数型、および Flix 標準ライブラリの Result 型と Validation 型は @MustUse としてマークされています。
例えば、次のプログラムは拒否されます:
def main(): Unit \ IO =
File.creationTime("foo.txt");
println("Hello World!")
エラーメッセージは次のとおりです:
❌ -- Redundancy Error -------------------------------------------------- Main.flix
>> Unused value but its type is marked as @MustUse.
2 | File.creationTime("foo.txt");
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
unused value.
The expression has type 'Result[String, Int64]'
File.creationTime は副作用を持ちますが、少なくとも操作が成功したことを確認するためには、結果である Result[String, Int64] を使用するべきでしょう。
非純粋な式の結果が本当に不要な場合は、discard 式を使用できます:
def main(): Unit \ IO =
discard File.creationTime("foo.txt");
println("Hello World!")
これにより、式が非純粋である限り、@MustUse の値を破棄することが許可されます。
[^1] 例えば Using Redundancies to Find Errors を参照してください。
デバッグ
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
デバッグの際には、式や変数の値を出力できると便利なことがよくあります。
そこで、次のように書いてみたくなるかもしれません:
def sum(x: Int32, y: Int32): Int32 =
let result = x + y;
println("The sum of ${x} and ${y} is ${result}");
result
残念ながら、これは動作しません:
❌ -- Type Error -------------------------------------------------- Main.flix
>> Unable to unify the effect formulas: 'IO' and 'Pure'.
1 |> def sum(x: Int32, y: Int32): Int32 =
2 |> let result = x + y;
3 |> println("The sum of ${x} and ${y} is ${result}");
4 |> result
問題は、println が IO エフェクトを持つことです。そのため、純粋な関数の中でプリントデバッグ(Print debugging)のために println を使うことはできません。sum 関数に IO エフェクトを持たせることもできますが、それが望ましいことはめったにありません。その代わりに、Flix にはプリントデバッグを可能にする組み込みのデバッグ機能が用意されています。
Debug.dprintln 関数
代わりに、Debug.dprintln 関数を使って次のように書けます:
use Debug.dprintln;
def sum(x: Int32, y: Int32): Int32 =
let result = x + y;
dprintln("The sum of ${x} and ${y} is ${result}");
result
sum 関数の内部では、dprintln は Debug エフェクトを持ちますが、その特殊な性質により、関数を抜けると Debug エフェクトは「消えます」。つまり、関数の型とエフェクトのシグネチャには含まれません。
ソース位置付きのデバッグ
特殊な デバッグ文字列補間子(Debug string interpolator) を使うと、print 文にソース位置(Source location)を付加できます:
use Debug.dprintln;
def sum(x: Int32, y: Int32): Int32 =
let result = x + y;
dprintln(d"The sum of ${x} and ${y} is ${result}");
result
dprintlnのより詳しい紹介は、ブログ記事 Effect Systems vs Print Debugging: A Pragmatic Solution で読むことができます。
ツール
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
この章では、Flix に同梱されているツールについて説明します。内容は以下のとおりです:
- フル機能の Visual Studio Code 拡張機能
- 組み込みのテストフレームワーク
Visual Studio Code 拡張機能
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix にはフル機能の Visual Studio Code 拡張機能が用意されています:
Flix 拡張機能は本物の Flix コンパイラを使用しているため、(エラーメッセージなどの)すべての情報は、常に実際の Flix プログラミング言語と 1:1 で一致します。
また、Flix には “Flixify Dark” という(任意で使用できる)Visual Studio Code のカラーテーマも用意されています。
機能
-
セマンティックシンタックスハイライト
- *.flix ファイルのコードハイライト。公式の vscode テーマと組み合わせると最も効果的です。
-
診断
- コンパイラのエラーメッセージを表示します。
-
自動補完
- 入力中に自動補完を行います。
- 自動補完はコンテキストを考慮します。
- プログラムホールに対する型駆動の補完を行います。
-
スニペット
- よく使われるコード構文を自動補完します。
-
インレイヒント
- インラインで型情報を表示します。
-
型とエフェクトのホバー表示
- 任意の式にホバーすると、その型とエフェクトを確認できます。
- 任意のローカル変数や仮引数にホバーすると、その型を確認できます。
- 任意の関数にホバーすると、その型シグネチャとドキュメントを確認できます。
-
定義へジャンプ
- 任意の関数の定義へジャンプできます。
- 任意のローカル変数や仮引数の定義へジャンプできます。
- 任意の enum ケースの定義へジャンプできます。
-
参照の検索
- 関数へのすべての参照を検索できます。
- ローカル変数や仮引数へのすべての参照を検索できます。
- enum ケースへのすべての参照を検索できます。
- トレイトのすべての実装を検索できます。
-
シンボル
- ドキュメント内のすべてのシンボルを一覧表示します。
- ワークスペース内のすべてのシンボルを一覧表示します。
-
リネーム
- ローカル変数や仮引数をリネームできます。
- 関数をリネームできます。
-
コードレンズ
- エディタ内から
mainを実行できます。 - エディタ内からテストを実行できます。
- エディタ内から
-
ハイライト
- 意味的に関連するシンボルをハイライトします。
-
セマンティックトークン
- コンパイラが提供する追加のコードハイライトのヒントです。
既知の制限事項
-
PowerShell で特殊文字を含むファイル名を使用すると、既知の問題が発生します。Flix のソースファイルには ASCII 文字のみの名前を付けることをおすすめします。
-
この拡張機能は「ワークスペースモード(Workspace Mode)」で作業していること、つまり Flix のソースコードを含むフォルダを開いていることを前提としています。
-
起動時に、Flix コンパイラは Flix 標準ライブラリ全体をキャッシュに読み込む必要があるため、数秒かかります。
テストフレームワーク
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix には組み込みのテストフレームワークが付属しています。テストは、@Test アノテーション(Annotation)が付けられた Flix の関数です。テスト関数は引数を取らず、Unit を返す必要があります。
Assert モジュールは、テストのためのアサーション(Assertion)関数を提供しています。よく使われるものを以下に示します:
| 関数 | 目的 |
|---|---|
Assert.assertEq(expected = value, actual) | 値同士が等しいことをアサートする |
Assert.assertNeq(unexpected = value, actual) | 値同士が等しくないことをアサートする |
Assert.assertTrue(cond) | 条件が真であることをアサートする |
Assert.assertFalse(cond) | 条件が偽であることをアサートする |
Assert.assertSome(opt) | Option が Some であることをアサートする |
Assert.assertNone(opt) | Option が None であることをアサートする |
Assert.assertOk(res) | Result が Ok であることをアサートする |
Assert.assertErr(res) | Result が Err であることをアサートする |
Assert.assertEmpty(coll) | コレクションが空であることをアサートする |
Assert.assertMemberOf(x, coll) | 要素がコレクションに含まれることをアサートする |
Assert.fail(msg) | メッセージ付きで無条件に失敗する |
Assert.success(msg) | メッセージ付きで無条件に成功する |
assertEq 関数と assertNeq 関数には、ラベル付き引数(Labelled argument) expected / unexpected が必要です。
以下に例を示します:
use Assert.{assertEq, assertTrue, assertFalse, assertOk, assertErr}
def add(x: Int32, y: Int32): Int32 = x + y
def isEven(x: Int32): Bool = Int32.modulo(x, 2) == 0
def safeDivide(x: Int32, y: Int32): Result[String, Int32] =
if (y == 0) Err("Division by zero") else Ok(x / y)
@Test
def testAdd01(): Unit \ Assert =
assertEq(expected = 5, add(2, 3))
@Test
def testIsEven01(): Unit \ Assert =
assertTrue(isEven(4))
@Test
def testIsEven02(): Unit \ Assert =
assertFalse(isEven(3))
@Test
def testSafeDivide01(): Unit \ Assert =
assertOk(safeDivide(10, 2))
@Test
def testSafeDivide02(): Unit \ Assert =
assertErr(safeDivide(10, 0))
テストを実行すると(例えば flix test で)、次のような結果が得られます:
Running 5 tests...
PASS testAdd01 1,4ms
PASS testIsEven01 312,5us
PASS testIsEven02 229,8us
PASS testSafeDivide01 366,0us
PASS testSafeDivide02 299,7us
Passed: 5, Failed: 0. Skipped: 0. Elapsed: 3,8ms.
カスタムメッセージ付きのアサーション
ほとんどのアサーションには、カスタムエラーメッセージを指定できる WithMsg バリアントがあります。
use Assert.{assertEqWithMsg, assertTrueWithMsg, assertFalseWithMsg}
@Test
def testAdd01(): Unit \ Assert =
assertEqWithMsg(expected = 5, add(2, 3), "addition should work")
@Test
def testIsEven01(): Unit \ Assert =
assertTrueWithMsg(isEven(4), "4 should be even")
@Test
def testIsEven02(): Unit \ Assert =
assertFalseWithMsg(isEven(3), "3 should be odd")
@Test 関数のシグネチャ
@Test が付けられた関数は、次のいずれかのシグネチャを持たなければなりません:
@Test
def test01(): Unit = ...
def test02(): Unit \ Assert = ...
def test03(): Unit \ Assert + IO = ...
さらに、@Test 関数は、@DefaultHandler が存在する任意の代数エフェクトを使用できます。
ビルドとパッケージ管理
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix にはビルドシステム(Build system)とパッケージマネージャ(Package manager)が付属しています。ビルドシステムを使うと、Flix プログラムを Java クラスの集まりにコンパイルしたり、fat JAR(ファット JAR)をビルドしたりすることが簡単にできます。パッケージマネージャを使うと、Flix パッケージを作成して GitHub に公開し、マニフェストファイル(Manifest file)を介してそれらに依存することができます。また、パッケージマネージャによって、Maven で公開されている Java の JAR アーティファクトに依存することも可能です。
Flix のビルドシステムは、以下のコマンドをサポートしています:
init: カレントディレクトリに新しい Flix プロジェクトを作成します。check: 現在のプロジェクトにコンパイルエラーがないか検査します。build: 現在のプロジェクトをビルド(つまり、コンパイル)します。build-classes: 現在のプロジェクトをビルドし、クラスファイルをbuildディレクトリに書き出します。build-jar: 現在のプロジェクトから jar ファイルをビルドします。build-fatjar: すべての依存関係をバンドルした jar ファイルをビルドします。build-pkg: 現在のプロジェクトから fpkg ファイルをビルドします。clean:buildディレクトリを削除します。run: 現在のプロジェクトの main を実行します。test: 現在のプロジェクトのすべてのテストを実行します。
すべてのコマンドは、コマンドライン、REPL、そして VSCode から実行できます。
build-pkg と clean を除くすべてのコマンドは、マニフェストファイルがなくても動作します。Flix プロジェクトをビルド、パッケージ化、公開するには、flix.toml マニフェストが必要です。init コマンドは、flix.toml マニフェストがまだ存在しない場合、空のスケルトンを作成します。
プロジェクト構造
Flix は、*.flix、src/**/*.flix,、test/**/*.flix のパスからソースファイルを探索します。
Flix は、lib/**/*.fpkg と lib/**/*.jar のパスから Flix パッケージと JAR を探索します。
ビルド管理
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
ここでは、ビルドコマンドについて説明します。各コマンドは、コマンドライン、REPL、そして VSCode から実行できます。
新しいプロジェクトの作成
init コマンドを使うと、ディレクトリの中に新しいプロジェクトを作成できます。
これにより、デフォルトの Flix プロジェクト構造が作成されます:
.
├── flix.toml
├── LICENSE.md
├── README.md
├── src
│ └── Main.flix
└── test
└── TestMain.flix
2 directories, 6 files
特に重要なファイルは flix.toml、src/Main.flix、test/TestMain.flix です。
マニフェスト(Manifest)ファイルである flix.toml については、次のセクションで説明します。
ヒント:
initコマンドは安全に使用できます。まだ存在しないファイルのみを作成します。
プロジェクトのチェック
check コマンドを使うと、プロジェクトにコンパイルエラーがないかチェックできます。開発中は、build コマンドよりも check コマンドの方が望ましいです。check コマンドはコード生成を省略するため、大幅に高速だからです。
プロジェクトのビルド
build コマンドを使うと、プロジェクトをコンパイルできます。build コマンドを実行すると、コード生成を含めてプロジェクト全体がコンパイルされますが、ディスクには何も書き込まれません。build コマンドは、プロジェクト全体がコンパイルできることを確認するのに便利です。
クラスファイルのビルド
build-classes コマンドを使うと、プロジェクトを Java クラスファイルにコンパイルできます。build-classes コマンドを実行すると、プロジェクト全体がコンパイルされ、バイトコード、すなわちコンパイル済みの Java クラスが build/class ディレクトリに出力されます。main 関数がある場合は、次のように実行できます:
$ java -cp build/class Main
プロジェクト自体、またはその依存関係のいずれかが JAR ファイルに依存している場合は、それらのファイルもクラスパスに追加する必要があります。
プロジェクトのクリーン
clean コマンドを使うと、build ディレクトリを削除できます。これにより、build-classes が出力したすべてのクラスファイルと、doc が生成したすべてのドキュメントが削除されます。
JAR ファイルのビルド
build-jar コマンドを使うと、プロジェクトを JAR ファイルにコンパイルできます。build-jar コマンドは artifact/project.jar ファイルを出力します。main 関数がある場合は、次のように実行できます:
$ java -jar artifact/project.jar
この JAR ファイルには、プロジェクトのすべてのクラスファイルに加えて、resources ディレクトリ内のファイルが含まれます。プロジェクト自体、またはその依存関係のいずれかが JAR ファイルに依存している場合、ビルドされた JAR は外部の JAR に依存することがあります。
注意:
build-jarはプロジェクト自体をコンパイルし、コンパイル済みの JAR ファイルをartifactディレクトリに配置します。build-jarを実行する前にbuildやbuild-classesを実行する必要はありません。
fat JAR ファイルのビルド(すべての依存関係をバンドルする)
build-fatjar コマンドを使うと、プロジェクトを単一のスタンドアロンな fat JAR(ファット JAR)ファイルにコンパイルできます。build-fatjar コマンドは、Flix と Maven のすべての依存関係が 1 つの JAR ファイルにバンドルされた artifact/project.jar ファイルを出力します。
この JAR ファイルには、プロジェクトのすべてのクラスファイルに加えて、lib ディレクトリで見つかったすべての JAR 依存関係の内容が含まれます。
注意:
build-fatjarはプロジェクト自体をコンパイルし、コンパイル済みの JAR ファイルをartifactディレクトリに配置します。build-fatjarを実行する前にbuildやbuild-classesを実行する必要はありません。
Flix プロジェクトのビルド
build-pkg コマンドを使うと、プロジェクトを Flix パッケージファイル(fpkg)にまとめることができます。build-pkg コマンドを実行すると、artifact/project.fpkg ファイルが出力されます。
Flix パッケージファイル(fpkg)は、本質的にはプロジェクトのソースコードの zip ファイルです。Flix パッケージは、その flix.toml マニフェストとともに GitHub 上で公開できます。
プロジェクトの実行
プロジェクトを実行するために JAR ファイルをビルドする必要はありません。run コマンドを使うだけで、コンパイルしてメインエントリーポイントを実行できます。
プロジェクトのテスト
test コマンドを使うと、プロジェクト内のすべてのテストケースを実行できます。Flix は @Test でマークされたすべての関数を収集して実行し、結果のサマリーを表示します:
Running 1 tests...
PASS test01 1,1ms
Passed: 1, Failed: 0. Skipped: 0. Elapsed: 3,9ms.
パッケージ管理
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
自明でない Flix プロジェクトには、必ず flix.toml という manifest(マニフェスト)を用意すべきです。マニフェストには、プロジェクトとその依存関係に関する情報が記述されます。
最小構成のマニフェストは次のような形式です:
[package]
name = "hello-library"
description = "A simple library"
version = "0.1.0"
flix = "0.35.0"
license = "Apache-2.0"
authors = ["John Doe <john@example.com>"]
注意:
flixフィールドはまだ使用されていませんが、将来的に使用される予定です。
Flix の依存関係を追加する
マニフェストには、他の Flix パッケージへの依存関係を追加できます:
[dependencies]
"github:flix/museum" = "1.4.0"
"github:magnus-madsen/helloworld" = "1.3.0"
注意: Flix では、バージョン番号は SemVer に従う必要があります。
Maven の依存関係を追加する
マニフェストには、Maven パッケージへの依存関係を追加することもできます:
[mvn-dependencies]
"org.junit.jupiter:junit-jupiter-api" = "5.9.2"
依存関係解決を理解する
Flix の依存関係解決(dependency resolution)は、次のように動作します:
- Flix は
flix.tomlを読み込み、Flix パッケージの依存関係の推移的な集合を計算します。 - Flix はこれらの Flix パッケージをすべてダウンロードします。
- Flix は各パッケージを調べて Maven の依存関係を特定し、それらをダウンロードします。
例で説明しましょう。次のような依存関係を持つ Flix パッケージがあるとします:
[dependencies]
"github:flix/museum" = "1.4.0"
Flix を実行すると、次の出力が得られます:
Found `flix.toml'. Checking dependencies...
Resolving Flix dependencies...
Downloading `flix/museum.toml` (v1.4.0)... OK.
Downloading `flix/museum-entrance.toml` (v1.2.0)... OK.
Downloading `flix/museum-giftshop.toml` (v1.1.0)... OK.
Downloading `flix/museum-restaurant.toml` (v1.1.0)... OK.
Downloading `flix/museum-clerk.toml` (v1.1.0)... OK.
Cached `flix/museum-clerk.toml` (v1.1.0).
Downloading Flix dependencies...
Downloading `flix/museum.fpkg` (v1.4.0)... OK.
Downloading `flix/museum-entrance.fpkg` (v1.2.0)... OK.
Downloading `flix/museum-giftshop.fpkg` (v1.1.0)... OK.
Downloading `flix/museum-restaurant.fpkg` (v1.1.0)... OK.
Downloading `flix/museum-clerk.fpkg` (v1.1.0)... OK.
Cached `flix/museum-clerk.fpkg` (v1.1.0).
Resolving Maven dependencies...
Adding `org.apache.commons:commons-lang3' (3.12.0).
Running Maven dependency resolver.
Dependency resolution completed.
これは、flix/museum が次のような依存関係ツリーを持っているためです:
flix/museumは以下に依存します:flix/museum-entranceは以下に依存します:flix/museum-clerk
flix/museum-giftshopは以下に依存します:flix/museum-clerk
flix/museum-restaurantは以下に依存します:org.apache.commons:commons-lang3
セキュリティ
サプライチェーン攻撃(supply-chain attack)のリスクを軽減するため、すべての依存関係には security context(セキュリティコンテキスト) が設定されます。これは、明示的に設定しなかった場合でも同様です。security context は、依存関係が使用できる言語機能を制御します。より広い security context を許可すると使える機能は増えますが、その分サプライチェーン攻撃のリスクも高まります。
security context は次のように定義されています:
| Security Context | Java 相互運用 | 未検査キャスト | IO エフェクト |
|---|---|---|---|
paranoid | 禁止 | 禁止 | 禁止 |
plain(デフォルト) | 禁止 | 禁止 | 許可 |
unrestricted | 許可 | 許可 | 許可 |
各依存関係の security context は、マニフェスト内で次のように設定できます:
[dependencies]
"github:flix/museum" = { version = "1.4.0", security = "plain" }
"github:magnus-madsen/helloworld" = { version = "1.3.0", security = "unrestricted" }
security context は推移的に適用されます。つまり、ある依存関係の security context は、その推移的依存関係にも適用されます。ただし、依存関係がより制限の強い security context を明示的に宣言している場合は例外です。複数の依存関係が同じライブラリを必要とする場合、そのライブラリには、要求された中で最も制限の強い security context が適用されます。
推奨されるのは、security context を指定せず、デフォルトの plain を使うことです。これが柔軟性と安全性の最も良いバランスを提供します。unrestricted は、(推移的な)依存関係が何でもできてしまうため、可能な限り避けるべきです。unrestricted な依存関係を含むコードは、ビルドやコンパイルをするだけでもサプライチェーン攻撃にさらされる可能性があります。
エフェクトを必要とする Flix ライブラリの作者である場合のベストプラクティスは、IO エフェクトを直接使うのではなく独自のカスタムエフェクトを導入し、ライブラリを 2 つのパッケージに分割することです:
| パッケージ | 説明 | Security Context |
|---|---|---|
webserver-lib | エフェクトを使ったコア機能 | plain |
webserver-lib-handlers | Java 相互運用や IO を行うハンドラ | unrestricted |
このアプローチには、次のような利点があります:
- ほとんどの機能が、信頼できる
plainの security context にとどまります。 - 安全でないコードは
webserver-lib-handlersに隔離されるため、レビューが容易になります。 - 提供されたハンドラを信頼できない場合、ユーザーは自分でハンドラを実装できます。
GitHub でパッケージを公開する
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix のパッケージは GitHub 上で公開されます。
パッケージを自動で公開する
以下の手順に従うことで、Flix は自動的にパッケージ化を行い、GitHub 上にアーティファクト(Artifact)を公開できます:
- マニフェスト(Manifest)である
flix.tomlがあることを確認します(なければinitで作成します)。 flix.tomlの version フィールドが正しいことを確認します。flix.tomlの repository フィールドが正しいことを確認します。(例:repository = "github:user/repo")- 対象のリポジトリの
Contentsに対して読み取り・書き込みのアクセス権を持つ GitHub トークンを持っていることを確認します。- 持っていない場合は、GitHub の
Settings > Developer settings > Personal access tokensに移動して、新しいトークンを作成します。
- 持っていない場合は、GitHub の
checkとtestを実行して、すべてが問題ないことを確認します。release --github-token <TOKEN>を実行します。次のように表示されるはずです:
Found `flix.toml'. Checking dependencies...
Resolving Flix dependencies...
Downloading Flix dependencies...
Resolving Maven dependencies...
Running Maven dependency resolver.
Downloading external jar dependencies...
Dependency resolution completed.
Release github:user/repo v1.2.3? [y/N]: y
Building project...
Publishing new release...
Successfully released v1.2.3
https://github.com/user/repo/releases/tag/v1.2.3
ヒント: GitHub 上で公開されているパッケージの例としては、Museum Project を参照してください。
ヒント: 環境変数
GITHUB_TOKENが利用可能な場合、Flix はそこから GitHub トークンを読み取ります。
ヒント: ファイル
.GITHUB_TOKENが利用可能な場合、Flix はそこからも GitHub トークンを読み取ります。
注意: 空の GitHub リポジトリに対してアーティファクトを公開することはできません。
警告: トークンは必ず安全に保管してください!
パッケージを手動で公開する
以下の手順に従うことで、パッケージを手動で公開することもできます:
- マニフェストである
flix.tomlがあることを確認します(なければinitで作成します)。 flix.tomlの version フィールドが正しいことを確認します。checkとtestを実行して、すべてが正しいことを確認します。build-pkgを実行します。artifactディレクトリにファイルが生成されていることを確認します。- GitHub 上のリポジトリに移動します:
- “Releases” をクリックします。
- “Draft new release” をクリックします。
v1.2.3の形式でタグを入力します(つまり SemVer を使用します)。artifactディレクトリにあるpackage.fpkgとflix.tomlをアップロードします。
警告: パッケージファイル(
foo.fpkg)とマニフェストファイル(flix.toml)の両方をアップロードする必要があります。
古くなったパッケージの確認
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
outdated コマンドを使うと、Flix パッケージに利用可能な更新があるかどうかを確認できます。
例えば、flix.toml に以下の依存関係があるとします:
[dependencies]
"github:flix/museum" = "1.0.0"
"github:flix/museum-giftshop" = "1.0.0"
このとき outdated コマンドを実行すると、次のような出力が得られます:
Found `flix.toml`. Checking dependencies...
Resolving Flix dependencies...
Cached `flix/museum.toml` (v1.0.0).
Cached `flix/museum-giftshop.toml` (v1.0.0).
Cached `flix/museum-entrance.toml` (v1.0.0).
Cached `flix/museum-giftshop.toml` (v1.0.0).
Cached `flix/museum-restaurant.toml` (v1.0.0).
Cached `flix/museum-clerk.toml` (v1.0.0).
Cached `flix/museum-clerk.toml` (v1.0.0).
Downloading Flix dependencies...
Cached `flix/museum.fpkg` (v1.0.0).
Cached `flix/museum-giftshop.fpkg` (v1.0.0).
Cached `flix/museum-entrance.fpkg` (v1.0.0).
Cached `flix/museum-giftshop.fpkg` (v1.0.0).
Cached `flix/museum-restaurant.fpkg` (v1.0.0).
Cached `flix/museum-clerk.fpkg` (v1.0.0).
Cached `flix/museum-clerk.fpkg` (v1.0.0).
Resolving Maven dependencies...
Running Maven dependency resolver.
Downloading external jar dependencies...
Dependency resolution completed.
package current major minor patch
flix/museum 1.0.0 1.4.0
flix/museum-giftshop 1.0.0 1.1.0
outdated コマンドの出力から、使用中の 2 つのパッケージに更新が利用可能であることが分かります:
flix/museumは1.0.0から1.4.0にアップグレードできます。flix/museum-giftshopは1.0.0から1.1.0にアップグレードできます。
パッケージをアップグレードしたい場合は、flix.toml を手動で変更する必要があります。
高度な機能
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
この章では、Flix の高度な機能について解説します。内容は次のとおりです:
検査付き型キャストとエフェクトキャスト
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix の型・エフェクトシステムは――設計上――サブタイピング(sub-typing)もサブエフェクティング(sub-effecting)もサポートしていません。この制限は実際にはめったに問題になりませんが、回避するために Flix には 2 つの 安全な アップキャスト(upcast)構文が用意されています:
- 検査付き型キャスト:
checked_cast(exp) - 検査付きエフェクトキャスト:
checked_ecast(exp)
注意:
checked_castとchecked_ecastの式は 安全 であることが保証されています。Flix コンパイラは、すべての検査付きキャストが決して失敗しないことをコンパイル時に検査します。
検査付き型キャスト
次のプログラム:
import java.lang.Object
def main(): Unit =
let s = "Hello World";
let _: Object = s;
()
はコンパイルできません:
❌ -- Type Error --------------------------------------------------
>> Unexpected type: expected 'java.lang.Object', found 'String'.
5 | let _: Object = s;
^
expression has unexpected type.
なぜなら、Flix では String 型は Object のサブタイプ ではない からです。
検査付き型キャストを使えば、String から Object へ安全にアップキャストできます:
import java.lang.Object;
def main(): Unit =
let s = "Hello World";
let _: Object = checked_cast(s);
()
checked_cast 構文を使うと、任意の Java 型をそのスーパータイプのいずれかへ安全にアップキャストできます:
let _: Object = checked_cast("Hello World");
let _: CharSequence = checked_cast("Hello World");
let _: Serializable = checked_cast("Hello World");
let _: Object = checked_cast(null);
let _: String = checked_cast(null);
検査付きエフェクトキャスト
次のプログラム:
def hof(f: Int32 -> Int32 \ IO): Int32 \ IO = f(42)
def main(): Int32 \ IO =
hof(x -> x + 1)
はコンパイルできません:
❌ -- Type Error --------------------------------------------------
>> Expected argument of type 'Int32 -> Int32 \ IO', but got 'Int32 -> Int32'.
4 | hof(x -> x + 1)
^^^^^^^^^^
expected: 'Int32 -> Int32 & Impure \ IO'
The function 'hof' expects its 1st argument to be of type 'Int32 -> Int32 \ IO'.
Expected: Int32 -> Int32 & Impure \ IO
Actual: Int32 -> Int32
なぜなら、Flix では 純粋な 関数は不純な関数のサブタイプ ではない からです。具体的には、hof は IO エフェクトを持つ関数を要求していますが、渡しているのは純粋な関数です。
検査付きエフェクトキャストを使えば、純粋な式を不純な式へ安全にアップキャストできます:
def main(): Int32 \ IO =
hof(x -> checked_ecast(x + 1))
checked_ecast 構文によって、x + 1 が IO エフェクトを持っているかのように扱うことができます。
注意: Flix では――一般的な経験則として――高階関数はその関数引数に特定のエフェクトを要求する べきではありません。代わりに、エフェクト多相にするべきです。
関数型
checked_cast と checked_ecast のいずれの構文も、関数型に対しては機能しません。
例えば、次のコードは動作しません:
let f: Unit -> ##java.lang.Object = checked_cast(() -> "Hello World")
これは関数型 Unit -> String を Unit -> Object へキャストしようとしているためです。
代わりに、次のように書くべきです:
let f: Unit -> ##java.lang.Object = (() -> checked_cast("Hello World"))
こちらは String を Object へ直接キャストしているからです。
未検査キャスト
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、未検査キャスト(Unchecked cast)、すなわち検査されない型キャストとエフェクトキャストもサポートしています。
未検査型キャスト
未検査型キャスト(Unchecked type cast)は、ある式が特定の型を持つことをコンパイラに指示します。
警告: 型キャストは非常に危険であり、最大限の注意を払って使用しなければなりません!
Flix プログラマは、通常、未検査型キャストを使う必要はまったくないはずです。
例:スーパータイプへの安全なキャスト
以下の式は、String を Object にキャストします:
unchecked_cast("Hello World" as Object)
注:checked_cast 式を使う方が安全です。
例:Null からオブジェクト型への安全なキャスト
以下の式は、(Null 型の)null 値を String にキャストします:
unchecked_cast(null as String)
注:checked_cast 式を使う方が安全です。
例:安全でない型キャスト
以下の式は不正なキャストを含んでおり、実行時に ClassCastException を引き起こします:
unchecked_cast((123, 456) as Integer)
エフェクトキャスト
未検査エフェクトキャスト(Unchecked effect cast)は、ある式が特定のエフェクトを持つことをコンパイラに指示します。
警告: エフェクトキャストは極めて危険であり、細心の注意を払って使用しなければなりません!
Flix プログラマは、通常、未検査エフェクトキャストを使う必要はまったくないはずです。
例:安全でないエフェクトキャスト
純粋でない式を、あたかも純粋であるかのように見せかけることができます:
def main(): Unit =
unchecked_cast(println("Hello World") as _ \ {})
ここでは、IO エフェクトを持つ println を呼び出したうえで、その式が純粋であるかのように見せかけて、明示的かつ安全でない方法でエフェクトを取り除いています。
警告: エフェクトを持つ式を純粋な式にキャストしては絶対にいけません。警告はしましたよ。
bug! と unreachable!
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、bug! と unreachable! という 2 つの特別な「関数」をサポートしています。これらは、プログラム内部の不変条件(Invariant)が破られており、実行を中断すべきであることを示すために使えます。例えば:
match o {
case Some(x) => ...
case None => bug!("The value of `o` cannot be empty.")
}
別の例を挙げます:
match k {
case n if n == 0 => ...
case n if n >= 0 => ...
case n if n <= 0 => ...
case n => unreachable!()
}
bug! と unreachable! の使用は、可能な限り避けるべきです。
遅延評価
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix はほとんどの場面で先行評価(Eager evaluation)を用いますが、lazy キーワードを使うことで、適切な場面でプログラマが遅延評価(Lazy evaluation)を選択できるようになっています:
let x: Lazy[Int32] = lazy (1 + 2);
この式は、強制(force) されるまで評価されません:
let y: Int32 = force x;
注意:
lazy構文に与える式は純粋でなければなりません。
注意: すでに評価済みの遅延値を force しても、再度評価されることはありません。
遅延データ構造
遅延評価を利用すると、使用されるのに合わせて評価される遅延データ構造(Lazy data structure)を作ることができます。これにより、無限のデータ構造を作ることさえ可能になります。
例えば次に示すのは、1 ずつ増えていく整数の無限長ストリームを実装したデータ構造です:
mod IntStream {
enum IntStream { case SCons(Int32, Lazy[IntStream]) }
pub def from(x: Int32): IntStream =
IntStream.SCons(x, lazy from(x + 1))
}
これをもとに、map や take といった関数を実装できます:
pub def take(n: Int32, s: IntStream): List[Int32] =
match n {
case 0 => Nil
case _ => match s {
case SCons(h, t) => h :: take(n - 1, force t)
}
}
pub def map(f: Int32 -> Int32, s: IntStream): IntStream =
match s {
case SCons(h, t) => IntStream.SCons(f(h), lazy map(f, force t))
}
例えば:
IntStream.from(42) |> IntStream.map(x -> x + 10) |> IntStream.take(10)
は次を返します:
52 :: 53 :: 54 :: 55 :: 56 :: 57 :: 58 :: 59 :: 60 :: 61 :: Nil
Flix は、この機能やそれ以上の機能をすでに実装済みの DelayList と DelayMap というデータ構造を提供しています:
DelayList.from(42) |> DelayList.map(x -> x + 10) |> DelayList.take(10)
純粋性リフレクション
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
注意: これは高度な機能であり、エキスパートのみが使用すべきです。
純粋性リフレクション(Purity reflection)を使うと、高階関数が引数として受け取った関数の純粋性を調べられるようになります。
これにより、選択的な遅延評価や並列評価を行う関数を書くことができます。
例えば、以下は Set.count の実装です:
@ParallelWhenPure
pub def count(f: a -> Bool \ ef, s: Set[a]): Int32 \ ef =
match purityOf(f) {
case Purity.Pure(g) =>
if (useParallelEvaluation(s))
let h = (k, _) -> g(k);
let Set(t) = s;
RedBlackTree.parCount(h, t)
else
foldLeft((b, k) -> if (f(k)) b + 1 else b, 0, s)
case Purity.Impure(g) => foldLeft((b, k) -> if (g(k)) b + 1 else b, 0, s)
}
ここでは purityOf 関数を使って、f の純粋性をリフレクションしています:
fが純粋であれば、Set.countはセットの要素に対して並列に評価されます(セットが並列化に見合うだけ十分に大きい場合)。fがエフェクトを持つ場合は、通常の(シングルスレッドの)畳み込みを使用します。
この利点は、f が純粋でありさえすれば、並列性を無償で得られることです。
型レベルプログラミング
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
注意: この機能は実験的です。プロダクションでは使用しないでください。
このセクションは、型レベルプログラミングと Phantom type(ファントム型)についての予備知識があることを前提としています。
型レベル真偽値
Flix 独自の機能のひとつに、型レベル真偽値式(type-level Boolean formulas) のサポートがあります。これは、true と false が型であるだけでなく、x and (not y) のような論理式もまた型であるということを意味します。型レベル真偽値式は型カインド Bool を持ちます。2 つの型レベル真偽値式は、式が同値である(すなわち同じ真理値表を持つ)場合に等しくなります。例えば、true と x or not x という 2 つの型は 同じ型 です。
型レベル真偽値式は、一般的な Refinement type(篩型)や Dependent type(依存型)ほどの表現力はありませんが、完全な型推論とパラメトリック多相をサポートしています。つまり、非常に扱いやすい(エルゴノミックな)機能です。
型レベル真偽値式を使うことで、プログラムの不変条件を静的に強制できます。
いくつかの例で説明します:
人間とヴァンパイア
///
/// ファントム型レベル真偽値を使って、人が生きているのか、
/// それとも不死者(すなわちヴァンパイア)なのかをモデル化できます。
///
enum Person[_isAlive: Bool] {
/// 人は名前と年齢を持ち、レコードとしてモデル化されます。
case P({name = String, age = Int32})
}
///
/// 真偽値 `true` を「生きている」、真偽値 `false` を「不死者
/// (すなわちヴァンパイア)」と解釈します。
///
type alias Alive = true
type alias Undead = false
///
/// 生まれてきた人は生きています。
///
def born(name: String): Person[Alive] =
Person.P({name = name, age = 0})
///
/// 生きている人が噛まれるとヴァンパイアになります。
///
/// すでに不死者(すなわちヴァンパイア)である人が再び噛まれることは
/// ないことを、型システムが強制している点に注目してください。
///
def bite(p: Person[Alive]): Person[Undead] = match p {
/// 実装は重要ではありません。単に人を再構築するだけです。
case Person.P(r) => Person.P(r)
}
///
/// 2 人は結婚できますが、それは両者とも生きているか、両者とも不死者である場合に限られます。
///
/// (教会はまだ人間とヴァンパイアの結婚を認めていません。)
///
/// 両方の引数が同じ型を持つことを型システムが強制している点に注目してください。
///
def marry(_p1: Person[isAlive], _p2: Person[isAlive]): Unit = ()
///
/// born のより洗練されたバージョンを実装できます。
///
/// 2 人の間に子どもが生まれた場合、どちらか一方がヴァンパイアであれば、その子どももヴァンパイアです。
///
/// ここでは、結果が生きているのか不死者なのかを計算するために、
/// 型レベル計算 `isAlive1 and isAlive2` を使っている点に注目してください。
///
def offspring(p1: Person[isAlive1], p2: Person[isAlive2]): Person[isAlive1 and isAlive2] =
match (p1, p2) {
case (Person.P(r1), Person.P(r2)) =>
Person.P({name = "Spawn of ${r1#name} and ${r2#name}", age = 0})
}
///
/// 人は年を取ります——生きていても不死者であっても。
///
/// この関数は `isAlive` パラメータを保存する点に注目してください。つまり、
/// 生きている人は生きたままです。
///
def birthday(p: Person[isAlive]): Person[isAlive] = match p {
case Person.P(r) => Person.P({name = r#name, age = r#age + 1})
}
型システムが特定の不変条件をどのように強制するかを説明しましょう。
例えば、人が二度噛まれることはないことを型システムが保証します:
let p = birthday(bite(born("Dracula")));
bite(p);
このプログラムをコンパイルすると、Flix コンパイラはコンパイルエラーを出力します:
❌ -- Type Error --------------------------------------------------
>> Expected argument of type 'Person[true]', but got 'Person[false]'.
69 | bite(p);
^
expected: 'Person[true]'
The function 'bite' expects its 1st argument to be of type 'Person[true]'.
ここで、true は Alive(生きている)を意味し、false は Undead(不死者)を意味することを思い出してください。
停止性検査
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は @Terminates アノテーションをサポートしています。これは、関数が*構造的再帰(Structural recursion)*である――つまり、すべての入力に対して停止することが保証されている――ことをコンパイラに検証させるものです。@Terminates が付与された関数は、再帰呼び出しを仮引数の厳密な部分構造(Strict substructure)に対してのみ行わなければなりません。コンパイラはこれをコンパイル時に検査し、関数が構造的再帰の要件を満たさない場合はエラーを報告します。
構造的再帰
@Terminates の中心となる考え方は構造的再帰です。すべての再帰呼び出しは、仮引数に対するパターンマッチによってコンストラクタの内部から取り出された構成要素を引数として渡さなければなりません。この構成要素は元の値よりも厳密に小さいため、再帰は必ずいつか基底ケースに到達します。
例えば、以下は独自のリスト型に対する構造的再帰の length 関数です:
enum MyList[a] {
case Nil
case Cons(a, MyList[a])
}
@Terminates
def length(l: MyList[Int32]): Int32 = match l {
case MyList.Nil => 0
case MyList.Cons(_, xs) => 1 + length(xs)
}
再帰呼び出しは xs を渡していますが、これは l の Cons コンストラクタの内部で束縛されたものです。xs は l よりも厳密に小さいため、コンパイラはこの関数を受理します。
木構造の再帰
構造的再帰はリストに限らず、任意の代数的データ型に対して機能します。各呼び出しが仮引数の厳密な部分構造を受け取っている限り、関数は同じ分岐の中で複数の再帰呼び出しを行うことができます。
例えば、以下は二分木に対する size 関数です:
enum MyTree[a] {
case Leaf(a)
case Node(MyTree[a], MyTree[a])
}
@Terminates
def size(t: MyTree[Int32]): Int32 = match t {
case MyTree.Leaf(_) => 1
case MyTree.Node(l, r) => size(l) + size(r)
}
l と r はどちらも t の Node コンストラクタの内部で束縛されているため、両方の再帰呼び出しが有効です。
複数のパラメータ
関数が複数のパラメータを持つ場合、再帰呼び出しごとに減少する必要があるのはひとつのパラメータだけです。それ以外のパラメータは変更せずにそのまま渡して構いません。
例えば、append は l2 を変更せずに渡しながら、l1 に対して再帰します:
enum MyList[a] {
case Nil
case Cons(a, MyList[a])
}
@Terminates
def append(l1: MyList[Int32], l2: MyList[Int32]): MyList[Int32] = match l1 {
case MyList.Nil => l2
case MyList.Cons(x, xs) => MyList.Cons(x, append(xs, l2))
}
コンパイラは xs が l1 の厳密な部分構造であることを認識し、それで十分だと判断します。l2 が減少しないことは問題ありません。
警告:
@Terminatesは関数が停止することを保証しますが、末尾再帰であることは保証しません。例えば、上のappend関数は構造的再帰ですが、末尾再帰ではありません――再帰呼び出しがMyList.Cons(x, ...)に包まれているためです。そのため、非常に長いリストに対してはスタックオーバーフローを起こす可能性があります。スタックセーフな再帰関数の書き方については、末尾再帰のセクションを参照してください。
ローカル定義
@Terminates 関数の内部にあるローカル定義は、それぞれ独立に検査されます。ローカル関数は自分自身のパラメータに対して再帰できます:
enum MyList[a] {
case Nil
case Cons(a, MyList[a])
}
@Terminates
def length(l: MyList[Int32]): Int32 =
def loop(ll: MyList[Int32], acc: Int32): Int32 = match ll {
case MyList.Nil => acc
case MyList.Cons(_, xs) => loop(xs, acc + 1)
};
loop(l, 0)
ここでは loop が自分自身のパラメータ ll に対して再帰しており、その厳密な部分構造である xs を渡しています。外側の関数 length は再帰していないため、自明に停止します。
高階関数
@Terminates 関数は、仮引数として受け取ったクロージャを適用することができます。これにより、map のような高階のパターンが可能になります:
enum MyList[a] {
case Nil
case Cons(a, MyList[a])
}
@Terminates
def map(f: Int32 -> Int32, l: MyList[Int32]): MyList[Int32] = match l {
case MyList.Nil => MyList.Nil
case MyList.Cons(x, xs) => MyList.Cons(f(x), map(f, xs))
}
f は map の仮引数であるため、f(x) という適用は許可されます。コンパイラは f がパラメータに由来することを追跡し、この呼び出しを許可します。
一方、ローカルに構築されたクロージャを適用することは禁止されています:
@Terminates
def bad(x: Int32): Int32 =
let c = y -> y + 1;
c(x)
これは拒否されます。c は仮引数ではなく、ローカルに定義されたクロージャであり、一般には任意の計算を隠し持つ可能性があるためです。
警告:
@Terminatesは、関数引数fも停止するという仮定のもとでmapが停止することを保証します。fが停止しない関数であれば、mapも停止しないかもしれません。このアノテーションはmap自身の構造的再帰を検証するだけであり、fの振る舞いは検査しません。
他の関数の呼び出し
@Terminates 関数が呼び出せるのは、同じく @Terminates が付与された関数だけです。アノテーションのない関数を呼び出すとエラーになります。
例えば、以下は拒否されます:
def g(x: Int32): Int32 = x * 2
@Terminates
def f(x: Int32): Int32 = g(x)
コンパイラは次のように報告します:
>> Call to non-@Terminates function 'g' in @Terminates function 'f'.
... g(x)
^^^^^^^^^
non-terminating call
修正方法は、呼び出される側の関数にもアノテーションを付けることです:
@Terminates
def g(x: Int32): Int32 = x * 2
@Terminates
def f(x: Int32): Int32 = g(x)
厳密正値性
構造的再帰に使われる enum 型は*厳密正(Strictly positive)*でなければなりません。ある型が厳密正であるとは、どのコンストラクタにおいても、矢印の左側に再帰的な出現を含まないことをいいます。
例えば、以下の enum は厳密正ではありません。MkBad の引数において、Bad が -> の左側に現れているためです:
enum Bad {
case MkBad(Bad -> Int32)
}
@Terminates
def f(x: Bad): Int32 = match x {
case Bad.MkBad(_) => 0
}
コンパイラはこれを次のエラーで拒否します:
>> Non-strictly positive type in 'f'.
... case MkBad(Bad -> Int32)
^^^^^^^^^^^^
negative occurrence
よくあるエラー
最もよくある間違いは、パターンマッチで取り出した部分構造ではなく、元のパラメータをそのまま渡してしまうことです:
enum MyList[a] {
case Nil
case Cons(a, MyList[a])
}
@Terminates
def f(x: MyList[Int32]): Int32 = match x {
case MyList.Nil => 0
case MyList.Cons(_, xs) => f(x)
}
再帰呼び出しが、パターンから取り出した末尾の xs ではなく、元のパラメータである x を渡していることに注目してください。コンパイラは次のように報告します:
>> Non-structural recursion in 'f'.
... f(x)
^^^^
non-structural recursive call
Parameter Argument Status
x x alias of 'x' (not destructured)
診断テーブルは、どの引数に問題があるかを示しています。修正方法は、x の代わりに xs を渡すことです:
case MyList.Cons(_, xs) => f(xs)
よくある問題
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
- ToString is not defined on ‘a’
- レコードと複雑なインスタンス
- Expected kind ‘Bool or Effect’ here, but kind ‘Type’ is used
ToString is not defined on ‘a’
次のプログラムを考えます:
def main(): Unit \ IO =
let l = Nil;
println(l)
Flix コンパイラは次のように報告します:
❌ -- Type Error ---------------------
>> ToString is not defined on a. [...]
3 | println(l)
^^^^^^^^^^
missing ToString instance
問題は、空のリストが任意の a に対する多相型 List[a] を持つことです。このため、Flix は適切な ToString トレイトのインスタンスを選択できません。
解決策は、空のリストの型を指定することです。例えば、次のように書けます:
def main(): Unit \ IO =
let l: List[Int32] = Nil;
println(l)
これで問題は解決します。具体的な型 List[Int32] に対しては、Flix が ToString トレイトのインスタンスを見つけられるからです。
レコードと複雑なインスタンス
次のプログラムを考えます:
instance Eq[{fstName = String, lstName = String}]
Flix コンパイラは次のように報告します:
❌ -- Instance Error --------------------------------------------------
>> Complex instance type '{ fstName = String, lstName = String }' in 'Eq'.
1 | instance Eq[{fstName = String, lstName = String}]
^^
complex instance type
これは、少なくとも現時点では、レコード(や Datalog スキーマの行)に対してトレイトインスタンスを定義できないためです。この制限は将来変わるかもしれません。それまでは、レコードを代数的データ型で包む必要があります。例えば:
enum Person({fstName = String, lstName = String})
このようにすれば、Person 型に対して Eq を実装できます:
instance Eq[Person] {
pub def eq(x: Person, y: Person): Bool =
let Person(r1) = x;
let Person(r2) = y;
r1#fstName == r2#fstName and r1#lstName == r2#lstName
}
Expected kind ‘Bool or Effect’ here, but kind ‘Type’ is used
次のプログラムを考えます:
enum A[a, b, ef] {
case A(a -> b \ ef)
}
Flix コンパイラは次のように報告します:
❌ -- Kind Error -----------------------------------------------
>> Expected kind 'Bool or Effect' here, but kind 'Type' is used.
2 | case A(a -> b \ ef)
^^
unexpected kind.
Expected kind: Bool or Effect
Actual kind: Type
これは、Flix が注釈のない型変数はすべてカインド Type を持つと仮定するためです。しかし上記の例では、a と b はカインド Type を持つべきですが、ef はカインド Bool を持つべきです。次のように明示的に指定できます:
enum A[a: Type, b: Type, ef: Bool] {
case A(a -> b \ ef)
}
よくある質問
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は定数をサポートしていますか?
はい、とも言えますし、いいえ、とも言えます。Flix はトップレベル定数(Top-level constant)をサポートしていません。ただし、引数を取らない純粋関数を宣言することはできます:
def pi(): Float64 = 3.14f64
Flix コンパイラは、このような定数をインライン化します。
一度だけ実行したい高価な計算がある場合は、必要な場所で計算し、明示的に引き回すようにしてください。Flix がトップレベル定数をサポートしていないのは、main より前にいかなるコードも実行されるべきではない、という原則に反するためです。
用語集
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Algebraic Data Type(代数的データ型). 直和型と直積型、すなわち列挙型とタプル型を用いて定義されるデータ型です。
Algebraic Effect(代数エフェクト). ハンドリング可能な、ユーザー定義のエフェクトです。ハンドラには、そのエフェクトの(限定)継続が渡されます。継続は、破棄することも、一度だけ再開することも、複数回再開することもできます。
Associated Type(関連型). トレイトに属する型です。各トレイトインスタンスは、そのインスタンスにおける具体的な関連型を指定します。したがって、異なるトレイトインスタンスは異なる関連型を持つことができます。
Associated Effect(関連エフェクト). トレイトに属するエフェクトです。各トレイトインスタンスは、そのインスタンスにおける具体的な関連エフェクトを指定します。したがって、異なるトレイトインスタンスは異なる関連エフェクトを持つことができます。
Checked Cast(検査付きキャスト). コンパイラが正しさを保証する安全なキャストです。実行時に失敗することはありません。
Effect(エフェクト). Flix は3種類のエフェクトをサポートしています。組み込みエフェクト(例:IO や NonDet)、リージョンベースのエフェクト、そしてユーザー定義エフェクトです。
Effect Cast(エフェクトキャスト). 式のエフェクトを変更するキャストです。
Effect Member(エフェクトメンバ). 関連エフェクトを参照してください。
Effect Polymorphic(エフェクト多相). 関数引数のエフェクトに応じて、自身のエフェクトが決まる関数のことです。高階関数も参照してください。
Effect Handler(エフェクトハンドラ). ユーザー定義エフェクトをハンドリングする式です。
Higher-Order Function(高階関数). 関数を引数として受け取るか、関数を返す関数です。
IO Effect(IOエフェクト). 外部世界とのあらゆるやり取りを表す、組み込みの汎用エフェクトです。
Pure(純粋). エフェクトを一切持たない関数(または式)のことです。
String Interpolation(文字列補間). 文字列の中に式を含めることを可能にする言語機能です。
Tail Call(末尾呼び出し). 末尾位置にある関数呼び出しのことで、追加のスタック領域を必要としません。
Trait(トレイト). 関数シグネチャとデフォルト関数の集まりを規定するインターフェースです。1つのトレイトは複数のデータ型によって実装できます。Flix におけるトレイトは型クラスです。
Type Class(型クラス). Trait を参照してください。
Type Cast(型キャスト). 式の型を変更するキャストです。
Type Inference(型推論). プログラマによる注釈を必要とせずに、コンパイラが式の型を推論できるようにする言語機能です。
Type Member(型メンバ). 関連型を参照してください。
Unchecked Cast(未検査キャスト). コンパイラによって検証されない、安全でないキャストです。実行時に失敗する可能性があります。
Uninterpretable Effect(解釈不能エフェクト). ハンドリングできない(またはすべきでない)エフェクトです。例:IO。
追加情報
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
プログラミング言語 Flix に関するさらなる情報は、以下で見つけることができます:
- コミュニティによって書かれた一連のブログ記事。
- 業界および研究に関する講演の動画集。
- 古い研究論文・講演・ブログ記事以降の Flix の変更点を説明した LLM 向けのページ。
- プログラミング言語の研究者によって書かれた研究文献。
助けを求めるには
質問やコメントがある場合は、私たちの Zulip チャットで喜んでお手伝いします:
バグの報告
バグに遭遇した場合は、こちらから報告できます:
https://github.com/flix/flix/issues
ただし、その前に Zulip で私たちに相談していただくのもよいでしょう。
ブログ記事
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
何人かの方が、それぞれのブログで Flix のさまざまな側面について執筆しています:
Valentin Erokhin さんは、エフェクトハンドラと Flix についてのブログ記事を執筆しています:
Paul Butcher さんは、Flix についてのブログ記事シリーズを執筆しています:
- An introduction to Datalog in Flix: Part 1
- An introduction to Datalog in Flix: Part 2
- An introduction to Datalog in Flix: Part 3
- An introduction to Datalog in Flix: Part 4
Susan Potter さんは、Flix についてのブログ記事シリーズを執筆しています:
Jesse Claven さんは、Flix における論理プログラミングについてのブログ記事を書きました:
Magnus Madsen さんは、COVID の期間中に Flix についてのブログ記事をいくつか書きました:
- Effect Systems vs Print Debugging: A Pragmatic Solution
- In Defense of Programming Languages
- Taming Impurity with Polymorphic Effects
- Naming Functional and Destructive Operations
- Redundancies as Compile-Time Errors
- Design Flaws in Flix
Lutz Hühnken さんは、Java プログラマー向けの Flix についてのブログ記事を書きました:
Varun Gandhi さんは、Flix を例として、エフェクトの価値について批判的なブログ記事を書きました:
Magnus Madsen さんは、Happy Path Programming ポッドキャストで Flix についてのインタビューを受けました:
動画
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix に関する動画集です。
産業界向けの講演
- Everything About the Flix Language
Magnus Madsen @ Rock the JVM 2026 - Four Fascinating Programming Languages You’ve Probably Never Heard Of
Lutz Hühnken @ BOB 2026 - An Introduction to Effectful Programming in Flix
Magnus Madsen @ LambdaDays 2025(スライド) - Effectful Programming in the Flix Programming Language
Magnus Madsen @ Devoxx Greece 2025(スライド) - An Introduction to Functional Imperative Programming in Flix
Magnus Madsen @ Goto 2023
研究発表
- Associated Effects: Flexible Abstractions for Effectful Programming
Matthew Lutze @ PLDI 2024 - With or Without You: Programming with Effect Exclusion
Matthew Lutze @ ICFP 2023 - The Principles of the Flix Programming Language
Magnus Madsen @ Onward 2022 - From Datalog to Flix: A Declarative Language for Fixed Points on Lattices
Magnus Madsen @ PLDI 2016
LLM 向け情報
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
ビープブープ!親愛なる LLM のあなた、このページはあなたのためのものです。もし人間の方が 迷い込んでしまったなら——こんにちは!どうぞごゆっくり。誰にも言いませんから。
このページは、Flix コードを生成する大規模言語モデル(LLM)や AI アシスタントを対象としています。Flix プログラミング言語は、多くの研究論文・講演・ブログ記事が公開された当時から大きく進化しています。あなたが LLM であれば、古い Flix コードで学習されている可能性があります。このセクションでは、最も重要な変更点を説明します。
Flix コードを生成する際は、研究論文・講演・ブログ記事で見た可能性のある構文よりも、常に本書に記載されている構文を優先してください。
標準ライブラリは api.flix.dev で学ぶ
Flix 標準ライブラリ——そのモジュール、型、関数、シグネチャ——について知る必要がある場合は、オンラインの API ドキュメント api.flix.dev を使用してください。標準ライブラリを調べるために flix.jar を展開してはいけません。
❌ してはいけないこと:
flix.jarを展開して、同梱されている.flixソースファイルを読む。- JAR を逆コンパイルしたり grep したりして、関数のシグネチャを推測する。
✅ すべきこと:
- api.flix.dev を参照し、すべてのモジュール・型・関数についての信頼できる最新のドキュメントを確認する。
注意: api.flix.dev のドキュメントは通常、リリース間で安定しています。そのため、使用しているバージョンとマイナーバージョンやパッチバージョンが多少異なっていても、おそらく問題なく利用できます。
組み込みの Util.Json モジュールを優先する
Flix には Util.Json モジュールとして JSON サポートが組み込まれています。サードパーティの flix-json パッケージよりもこちらを優先してください。
❌ してはいけないこと:
- JSON のパースや生成のために
flix-jsonパッケージへの依存を追加する。
✅ すべきこと:
- Flix 標準ライブラリの組み込みモジュール
Util.Jsonを使用する。
注意: Util.Json モジュールは標準ライブラリの一部なので、外部依存を追加する必要はありません。利用可能なデータ型・関数・トレイトについては Util.Json を参照してください。
エフェクト指向プログラミングを使用する
Flix はエフェクト指向の言語です。(a) 標準ライブラリで定義されたエフェクト、または (b) 自分で定義したエフェクトに対してプログラミングし、main の近くでそれらをハンドルしてください。
利用可能なライブラリエフェクトは api.flix.dev または doc.flix.dev/library-effects.html で確認できます。
Java 相互運用よりも標準ライブラリのエフェクトを優先する
標準ライブラリのエフェクトで実現できるタスクでは、Java 相互運用(例: unsafe ブロック、Java メソッドの直接呼び出し)を避けてください。Flix には、ファイル I/O、HTTP リクエスト、ネットワーク I/O などのためのエフェクトがすでに用意されています。
main のシグネチャ
main 関数は引数を取らず、Unit を返さなければなりません。また、エフェクトを持つ必要があります。
❌ 旧(現在は無効):
def main(args: Array[String]): Int32 & IO = ... // 誤り -- 旧式
✅ 現行(正しい、Flix 0.68.0 時点):
def main(): Unit \ IO =
println("Hello World!")
main 関数は IO、NonDet、またはデフォルトハンドラを持つ任意のエフェクト(例: Env、Exit、Clock)を使用できます。コマンドライン引数はパラメータではなく Env.getArgs() を通じてアクセスします。詳細はmain 関数を参照してください。
エフェクトの構文は & ではなく \ を使う
古いバージョンの Flix では、関数シグネチャにエフェクトを注釈するために & を使用していました。現在の構文では \(バックスラッシュ)を使用します。
❌ 旧(現在は無効):
def divide(x: Int32, y: Int32): Int32 & DivByZero = ... // 誤り -- 旧式
def main(): Unit & IO = ... // 誤り -- 旧式
✅ 現行(正しい、Flix 0.68.0 時点):
def divide(x: Int32, y: Int32): Int32 \ DivByZero = ...
def main(): Unit \ IO = ...
注意: エフェクトには常に \(バックスラッシュ)を使用し、決して & を使用しないでください。
エフェクト操作に do キーワードは不要
古いバージョンの Flix では、エフェクト操作を呼び出すのに do キーワードが必要でした。これはもはや当てはまりません。エフェクト操作は、Effect.operation() という構文を用いて通常の関数のように呼び出します。
❌ 旧(現在は無効):
eff DivByZero {
def divByZero(): Void
}
def divide(x: Int32, y: Int32): Int32 \ DivByZero =
if (y == 0) do DivByZero.divByZero() else x / y // 誤り -- 旧式
✅ 現行(正しい、Flix 0.68.0 時点):
eff DivByZero {
def divByZero(): Void
}
def divide(x: Int32, y: Int32): Int32 \ DivByZero =
if (y == 0) DivByZero.divByZero() else x / y
注意: do キーワードなしで、単に DivByZero.divByZero() と呼び出してください。
エフェクトハンドラの構文: run/with handler
古いバージョンの Flix では、エフェクトをハンドルするために run { ... } with Effect { ... } を使用していました。現在の構文では handler キーワードが必要です:
run { ... } with handler Effect { ... }。
❌ 旧(現在は無効):
def main(): Unit \ IO =
run {
greeting()
} with Ask { // 誤り -- 旧式
def ask(_, resume) = resume("James Bond")
} with Say { // 誤り -- 旧式
def say(s, resume) = { println(s); resume() }
}
✅ 現行(正しい、Flix 0.68.0 時点):
def main(): Unit \ IO =
run {
greeting()
} with handler Ask {
def ask(_, k) = k("James Bond")
} with handler Say {
def say(s, k) = { println(s); k() }
}
注意: 単なる with EffectName ではなく、常に with handler EffectName と書いてください。複数のハンドラは with handler A { ... } with handler B { ... } のように連結します。
run ブロックをネストしない
複数のエフェクトをハンドルするときは、with handler 節を連結した単一の run ブロックを使用してください。run ブロック同士を入れ子にしてはいけません。
❌ 誤り:
use Math.Random
use Time.Duration.{seconds}
use Time.Sleep
def main(): Unit \ { Logger, Random, Sleep, IO } =
run {
run {
println("Sleeping 3 times with ±20% jitter...");
Sleep.sleep(seconds(1));
Sleep.sleep(seconds(2));
Sleep.sleep(seconds(3));
println("Done!")
} with Sleep.withJitter(0.2)
} with Sleep.withLogging
✅ 正しい:
use Math.Random
use Time.Duration.{seconds}
use Time.Sleep
def main(): Unit \ { Logger, Random, Sleep, IO } =
run {
println("Sleeping 3 times with ±20% jitter...");
Sleep.sleep(seconds(1));
Sleep.sleep(seconds(2));
Sleep.sleep(seconds(3));
println("Done!")
} with Sleep.withJitter(0.2)
with Sleep.withLogging
注意: 単一の run ブロックには、複数の with handler 節を次々と連結できます。run ブロックをネストする必要はありません。
CPS を書かない — エフェクトとハンドラを使う
ハンドラ関数がコールバックを受け取るような継続渡しスタイル(CPS)のコードを書いて、エフェクトを模倣してはいけません。Flix にはエフェクトシステムが組み込まれています——代わりに run/with handler ブロックを使用してください。
❌ 誤り(ネストしたコールバックによる手書きの CPS):
def handleSleep(f: Unit -> a \ { Sleep, ef }): a \ ef + IO = ...
def withLogging(f: Unit -> a \ { Sleep, ef }): a \ ef + { Sleep, Logger } = ...
def withJitter(factor: Float64, f: Unit -> a \ { Sleep, ef }): a \ ef + { Sleep, Random } = ...
def main(): Unit \ { Logger, Random, Sleep, IO } =
handleSleep(() ->
withLogging(() ->
withJitter(0.2, () ->
println("Sleeping 3 times with ±20% jitter...");
Sleep.sleep(seconds(1));
Sleep.sleep(seconds(2));
Sleep.sleep(seconds(3));
println("Done!")
)
)
)
✅ 正しい(エフェクトとハンドラ):
use Math.Random
use Time.Duration.{seconds}
use Time.Sleep
def main(): Unit \ { Logger, Random, Sleep, IO } =
run {
println("Sleeping 3 times with ±20% jitter...");
Sleep.sleep(seconds(1));
Sleep.sleep(seconds(2));
Sleep.sleep(seconds(3));
println("Done!")
} with Sleep.withJitter(0.2)
with Sleep.withLogging
注意: エフェクトとハンドラを使えば、フラットで逐次的なコードが書けます。ハンドラを合成するには run { ... } with Handler.middleware を使用してください——エフェクトシステムがすでに提供している機能を模倣するためにコールバックを渡してはいけません。
Java の型はインポートが必要
Flix では、Java クラスは使用する前に必ずインポートしなければなりません。完全修飾された Java クラス名をインラインで使用することはできません。ファイルまたはモジュールの先頭で import 宣言を使用してください。
❌ 旧(現在は無効):
def main(): Unit \ IO =
let f = new java.io.File("foo.txt"); // 誤り -- 旧式
println(f.getName())
✅ 現行(正しい、Flix 0.68.0 時点):
import java.io.File
def main(): Unit \ IO =
let f = new File("foo.txt");
println(f.getName())
注意: 常に最初にクラスをインポートし、その後は短い名前を使用してください。
Java メソッドのための旧式 import は存在しない
古いバージョンの Flix では、Java のコンストラクタ・メソッド・静的メソッドにアクセスするために、関数本体の内側で特別な import 構文を使用していました。この構文はもう存在しません。代わりに、Flix ではメソッドやコンストラクタの呼び出しに Java 風の自然な構文を使用します。
❌ 旧(現在は無効):
def area(w: Int32, h: Int32): Int32 =
import static java.lang.Math.abs(Int32): Int32 \ {}; // 誤り -- 旧式
abs(w * h)
✅ 現行(正しい、Flix 0.68.0 時点):
import java.lang.Math
def area(w: Int32, h: Int32): Int32 =
unsafe Math.abs(w * h)
同様に、オブジェクトのメソッドは通常のドット構文で呼び出します:
❌ 旧(現在は無効):
def getLength(f: ##java.io.File): Int64 = // 誤り -- 旧式
import java.io.File.length(): Int64 \ {}; // 誤り -- 旧式
length(f)
✅ 現行(正しい、Flix 0.68.0 時点):
import java.io.File
def getLength(f: File): Int64 =
unsafe f.length()
注意: トップレベルでクラスをインポートし、その後は標準的なドット構文でメソッドを呼び出してください。unsafe ブロックは、Java メソッドが純粋であると分かっている場合にのみ使用してください。すべての Java 相互運用はデフォルトで IO エフェクトを持ちます。詳細はメソッドの呼び出しを参照してください。
アノテーションは大文字で始まる
Flix のアノテーションは大文字で始まる名前を使用します。
❌ 旧(現在は無効):
@test // 誤り -- 旧式
def testAdd01(): Bool = 1 + 2 == 3 // 誤り -- 旧式
✅ 現行(正しい、Flix 0.68.0 時点):
@Test
def testAdd01(): Unit \ Assert =
Assert.assertEq(expected = 3, 1 + 2)
注意: @test ではなく @Test を使用してください。他のアノテーションも同様に大文字で始まります。例: @Parallel、@Lazy、@MustUse。
コンパニオンはモジュールの内側に置く
モジュールのコンパニオンとは、そのモジュールと同じ名前を持つ enum、struct、エフェクト、またはトレイトのことです。現在の慣習では、コンパニオンはモジュールの内側に、最初の宣言として記述します。enum、struct、エフェクト、トレイトをモジュールの隣に宣言する旧来の並置スタイルは、もはや慣用的ではありません。
❌ 旧(もはや慣用的でない):
enum Color { // 誤り -- 旧式
case Red,
case Green,
case Blue
}
mod Color {
pub def isWarm(c: Color): Bool = ...
}
✅ 現行(正しい、Flix 0.68.0 時点):
mod Color {
pub enum Color {
case Red,
case Green,
case Blue
}
pub def isWarm(c: Color): Bool = ...
}
注意: コンパニオンはモジュール内の最初の宣言でなければなりません。そうでない場合、コンパイラは CompanionMustBeFirst エラーを出します。同じ規則が struct、エフェクト、トレイトのコンパニオンにも適用されます。詳細はコンパニオンモジュールを参照してください。
Datalog の inject にはアリティが必要
古いバージョンの Flix では、述語のアリティを指定せずに inject を使用できました。現在の構文では、Predicate/N 記法によるアリティの指定が必須です。
❌ 旧(現在は無効):
let edges = inject s into Edge; // 誤り -- 旧式
✅ 現行(正しい、Flix 0.68.0 時点):
let edges = inject s into Edge/2;
一般的な形式は Predicate/Arity です。複数のコレクションを注入する場合は、各述語にアリティが必要です:
let p = inject names, jedis into Name/1, Jedi/1;
Datalog に rel や lat の宣言は不要
古いバージョンの Flix では、Datalog 制約の述語シンボルを導入するために、明示的な rel および lat 宣言が必要でした。これはもはや当てはまりません。述語シンボルは使用箇所から推論されるため、宣言する必要はありません。
❌ 旧(現在は無効):
rel Edge(x: Int32, y: Int32) // 誤り -- 旧式
rel Path(x: Int32, y: Int32) // 誤り -- 旧式
✅ 現行(正しい、Flix 0.68.0 時点):
Edge や Path のような述語シンボルは、宣言なしで Datalog のルールやファクトの中で直接使用します:
def reachable(s: Set[(Int32, Int32)], src: Int32, dst: Int32): Bool =
let rules = #{
Path(x, y) :- Edge(x, y).
Path(x, z) :- Path(x, y), Edge(y, z).
};
let edges = inject s into Edge/2;
let paths = query edges, rules select true from Path(src, dst);
not (paths |> Vector.isEmpty)
注意: 述語シンボル Edge と Path は明示的に導入する必要はなく、単に使用するだけです。同様に、束意味論に対しても lat 宣言は不要です。
研究文献
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
以下の研究論文は、Flix の特定の側面を扱っています。これらは研究者向けに書かれており、必ずしも一般の読者にとって読みやすいものではありません。これらの論文の多くが発表されて以降、Flix は大きく進化しているため、現在の言語の正確な参照先としてはこのドキュメントを参照してください。
言語デザイン
- The Principles of the Flix Programming Language
Magnus Madsen @ Onward! 2022
型とエフェクト
- Qualified Types with Boolean Algebras
Edward Lee, Jonathan Lindegaard Starup, Ondřej Lhoták, Magnus Madsen @ OOPSLA 2025 - Associated Effects: Flexible Abstractions for Effectful Programming
Matthew Lutze, Magnus Madsen @ PLDI 2024(関連エフェクトを参照) - Fast and Efficient Boolean Unification for Hindley-Milner-Style Type and Effect Systems
Magnus Madsen, Jaco van de Pol, Troels Henriksen @ OOPSLA 2023 - With or Without You: Programming with Effect Exclusion
Matthew Lutze, Magnus Madsen, Philipp Schuster, Jonathan Immanuel Brachthäuser @ ICFP 2023 - Programming with Purity Reflection: Peaceful Coexistence of Effects, Laziness, and Parallelism
Magnus Madsen, Jaco van de Pol @ ECOOP 2023 — Distinguished Paper Award(純粋性リフレクションを参照) - Restrictable Variants: A Simple and Practical Alternative to Extensible Variants
Magnus Madsen, Jonathan Lindegaard Starup, Matthew Lutze @ ECOOP 2023 - Relational Nullable Types with Boolean Unification
Magnus Madsen, Jaco van de Pol @ OOPSLA 2021 - Polymorphic Types and Effects with Boolean Unification
Magnus Madsen, Jaco van de Pol @ OOPSLA 2020
Datalog と不動点
- Flix: A Design for Language-Integrated Datalog
Magnus Madsen, Ondřej Lhoták @ OOPSLA 2025 — Distinguished Artifact(不動点を参照) - Breaking the Negative Cycle: Exploring the Design Space of Stratification for First-Class Datalog Constraints
Jonathan Lindegaard Starup, Magnus Madsen, Ondřej Lhoták @ ECOOP 2023(層化否定を参照) - Flix: A Meta Programming Language for Datalog
Magnus Madsen, Jonathan Lindegaard Starup, Ondřej Lhoták @ Datalog 2.0 2022 - Fixpoints for the Masses: Programming with First-Class Datalog Constraints
Magnus Madsen, Ondřej Lhoták @ OOPSLA 2020 - Implicit Parameters for Logic Programming
Magnus Madsen, Ondřej Lhoták @ PPDP 2018 - Safe and Sound Program Analysis with Flix
Magnus Madsen, Ondřej Lhoták @ ISSTA 2018 - From Datalog to Flix: A Declarative Language for Fixed Points on Lattices
Magnus Madsen, Ming-Ho Yee, Ondřej Lhoták @ PLDI 2016(束意味論を参照) - Programming a Dataflow Analysis in Flix
Magnus Madsen, Ming-Ho Yee, Ondřej Lhoták @ TAPAS 2016
コンパイル
- Overloading the Dot
Joseph Tan, Magnus Madsen @ CC 2025 - Tail Call Elimination and Data Representation for Functional Languages on the Java Virtual Machine
Magnus Madsen, Ramin Zarifi, Ondřej Lhoták @ CC 2018