プログラミング
OCamlとEioを試してみた
Taking OCaml and Eio for a Spin (mattjhall.co.uk)
要約
著者は、最近注目を集めている関数型プログラミング言語OCamlと、そのエフェクトベースの並行処理フレームワークEioを試した経験を共有しています。学習リソースの評価、言語の感触、そしてEioを使ったシンプルなRPCサーバーの実装について詳しく述べています。
全文翻訳
Matt Hall
About
Now
Projects
Blog
RSS
2026-07-15
OCamlとEioを試してみた
このウェブサイトでの最初のブログ記事はHaskellについてでした。私は常に機能プログラミングに興味がありましたが、Haskellの複雑さはそれに見合う価値がないように思えました。OCamlについては長い間漠然と知っていましたが、実際に試したことはありませんでした。最近、マルチコアサポート、エフェクト、そしてJane Streetによるよりシステム指向のOxCamlという形で、ちょっとしたルネッサンスを迎えています。今がそれを学ぶのに良い時期であり、並行処理フレームワークであるEioに興味がありました。それは前述のエフェクトを使用しますが、私にとってより興味深いのは、テストに役立つ可能性のある決定論的であるということです。
書籍とチュートリアル
まず最初に行うべきことは、チュートリアルまたは書籍を選ぶことです。簡単なGoogle検索の後、Real World OCamlを見つけました。Real World Haskell(気に入っていて、非常に実践的でした)と似ていると仮定し、すでに十分なFPスキルを持っていると仮定して、飛び込みました。構文とセマンティクスを急いで見たかったので、より説明的な序文はスキップしました。もし読んでいれば、その本がJane StreetのBase標準ライブラリの代替を使用することを知ったでしょう。歴史的に、実際の標準ライブラリはかなり貧弱で、コンパイラのニーズをカバーするだけだったようです。これはもはや真実ではなく、ゆっくりと成長しています。それでも、Real World OCamlと呼ばれる本が、その言語の最大の実際のユーザーからのライブラリを使用するのは、私にとって不合理ではないようです。とにかく、私は物事をより注意深く読むという教訓を学び、最初からやり直しました。
本の最初の半分はよく書かれており、私は喜んでそれに従いました。しかし、GADTsとファーストクラスモジュールあたりで迷子になり始めました。本には演習が不足しているように思われるため、馴染みのないものが現れたときに、基本的なことを十分に理解して対応することができません。私はもっと簡単なものを探し、ビデオとテキストの両方を提供するCS3110を見つけました。それには演習もあり、大いに役立ちました。私は人々に、まずこのコースから始めて、より高度な機能のためにRWOに進むことをお勧めします。RWOは気に入っていますが、RWHのようなプロジェクトベースの本を探しているなら、がっかりするでしょう。
手に持った感触
全体として、OCamlはコードを書くのが非常に楽しいです。機能プログラミングと命令型プログラミングの良いミックスがあります。例えば、リストをループするには、再帰関数またはList.mapとその仲間を使用します。
let print_recursive list =
let rec loop i list =
match list with
| [] -> ()
| x :: xs -> Printf.printf "[%d] => %i"
loop (i+1) xs
in loop 0 list
let print_fn = List.iteri (fun i x -> Printf.printf "[%d] => %i\n" i x)
let () = print_recursive ["foo"; "bar"; "baz"];
print_fn ["foo"; "bar"; "baz"];
(* [0] => foo [1] => bar [2] => baz
[0] => foo [1] => bar [2] => baz *)
しかし、必要であればミュータブルな変数なども依然として使用できます。
type client = { mutable request_id : int }
let dispatch cli ~msg =
let request_id = cli.request_id in
cli.request_id <- request_id + 1; (* update request_id *)
Printf.printf "Dispatching %d/%s to server\n" request_id msg
let () = dispatch_cli ~msg:"Hello"; dispatch_cli ~msg:"World"
(* => Dispatching 0/Hello to server
=> Dispatching 1/World to server *)
構文はかなり冗長だと感じます。例えば、let .. in、match .. with の構文です。構文はプログラミング言語の中で最も興味のない部分だと思います。Rustがシジルを持っていた時代でさえ満足していましたので、あまり気になりません。名前付き引数(~付き)があるのは非常に良いです。コンパイラの方が気になります。エラーが発生すると、ファイルの残りの部分の処理を諦めるようです。これにより、イテレーションループが非常に遅くなります。コードを書く、エラーが出る、エラーを修正する、再ビルドする。特に1つのモジュールに集中している場合、エラーをまとめて修正する機会はほとんどありません。
Eio
EioはエフェクトベースのIO並行ライブラリです。比較的新しいですが、io_uringサポートのような興味深い機能があります。最終的な目標は、Raft合意アルゴリズムの実装を書き、それをテストすることです。Raftの論文では、サーバー間の通信にリモートプロシージャコール(RPC)を使用すると述べられています。単純なRPCクライアント/サーバーは、素晴らしい最初のプロジェクトになりそうです。非常にシンプルなプロトコルを考えてみましょう。プロシージャコールは、リクエストID(int)、名前(string)、および単一の引数(string)を持ちます。整数は1バイト、文字列は長さを示すバイトとそのデータとしてシリアライズされます。例として、リクエスト10でEchoプロシージャをHelloという引数で呼び出すには、以下を送信します。
00000000: 0a04 4563 686f 0548 656c 6c6f ..Echo.Hello
応答は、リクエストIDと文字列の戻り値のみになります。
始める前に、Eioがそのビルディングブロックに使用するいくつかの専門用語に慣れる必要があります。
Fiber - 実行のスレッド。ただし、システムスレッドではありません!
Switch - ファイバーをグループ化するものです。Goのsync.WaitGroupのようなものです。
Flow - 読み書きできるものです。
Stream - バウンドされたスレッドセーフキューです。Goのチャネルのようなものです。
Promise - これはご存知だと思います。
サーバーから始めましょう。接続を複数持つことができるため、接続を表す型を定義する必要があります。ここではフローを直接使用していません。代わりにBuf_readまたはBuf_write型を使用しています。これは、バッファリングされた読み書きを使用して効率を高めるためです。フローへの操作は、基盤となるリソース(ソケット、ファイルなど)で直接行われます。幸いなことに、これらのバッファリングされたリーダー/ライターには、ここでリクエストIDを送受信するために使用する、型を読み書きするための便利なヘルパー関数があります。read_string/write_stringはUtilsから来ています。
let read_string src =
let len = Eio.Buf_read.uint8 src in
Eio.Buf_read.take len src
let write_string sink s =
Eio.Buf_write.uint8 sink (String.length s);
Eio.Buf_write.string sink s
次に、バッファリングされたフローを取得してループを実行するためのヘルパーが必要です。
let handle_conn flow =
let src = Eio.Buf_read.of_flow ~max_size:1024 flow in
Eio.Buf_write.with_flow flow @@ fun sink ->
conn_loop {src; sink}
そしてテスト実行可能ファイルです。
open Dumb_rpc
open Eio.Std
let port = 1470
let () = Eio_main.run @@ fun env ->
Eio.Switch.run @@ fun sw ->
let net = Eio.Stdenv.net env in
let handle_client flow addr =
traceln "Accepted connection from %a" Eio.Net.Sockaddr.pp addr;
Dumb_rpc.Server.handle_conn ~sw flow
in
let addr = `Tcp (Eio.Net.Ipaddr.V4.loopback, port) in
let socket = Eio.Net.listen net ~sw ~reuse_addr:true ~backlog:5 addr in
Eio.Net.run_server socket handle_client ~on_error:(traceln "Error handling connection: %a" Fmt.exn)
そして、テストの準備ができました。
> printf '\x00\x04Echo\x05Hello' | nc localhost 1470 -q0
Hello
> printf '\x01\x0aCapitalise\x07matthew' | nc localhost 1470 -q0
Matthew
素晴らしい!
クライアント
クライアントについては、もう少し凝ったものにして、並行して安全に使用できるようにしましょう。そのためには、ソケットへのプロシージャコールの書き込みを一度に1つだけ行うようにする必要があります。これは2つのファイバー(1つは読み込み用、もう1つは書き込み用)を使用して行われます。書き込みファイバーは、メソッドが呼び出されたときに追加するストリームから作業を取得します。
type request = { procedure : string; arg : string; }
type t = { mutable request_id : int; writeq : request Eio.Stream.t; (* Read right to left: stream of request *) }