HN 日本語サマリー

← 一覧へ戻る
プログラミング

OCamlとEioを試してみた

Taking OCaml and Eio for a Spin (mattjhall.co.uk)

46 pointsby mattjhall6 コメント

要約

著者は、最近注目を集めている関数型プログラミング言語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 *) }