Skip to content

Instantly share code, notes, and snippets.

@podhmo
Created June 16, 2025 22:44
Show Gist options
  • Save podhmo/3e3c1a1d0b7903c3da5e24e6a5704dc6 to your computer and use it in GitHub Desktop.
Save podhmo/3e3c1a1d0b7903c3da5e24e6a5704dc6 to your computer and use it in GitHub Desktop.
ocamlのlwtからeioまて(生成ai)

対話履歴

ユーザー: ocamlのlwtに続くチュートリアルを作ってください。なぜ必要だったかどのように使うか?などを明らかにしてください。ocamlの基礎文法は前提として知ってるとします。


AI: OCamlのLwtライブラリについてのチュートリアルへようこそ! このチュートリアルでは、Lwtがなぜ必要なのか、そして基本的な使い方から実践的な例までを解説します。OCamlの基本的な文法(変数束縛、関数定義、パターンマッチなど)は理解していることを前提とします。

目次

  1. Lwtとは何か?なぜ必要なのか?
  2. セットアップと最初のLwtプログラム
  3. Lwtのコアコンセプト: Promiseとlet*構文
  4. エラーハンドリング
  5. Lwtによる並行処理
  6. Lwtフレンドリーなライブラリの活用 (Lwt_unix, Lwt_io)
  7. 実践的な例: 非同期ファイル処理
  8. Lwtを使う上での注意点とベストプラクティス
  9. まとめと次のステップ

1. Lwtとは何か?なぜ必要なのか?

LwtはOCamlのための軽量スレッド (cooperative threads) ライブラリです。主にノンブロッキングI/Oを実現し、効率的な並行処理 (concurrency) を書くために使われます。

なぜLwtが必要なのでしょうか?

従来の多くのプログラミングスタイルでは、I/O処理(ファイル読み書き、ネットワーク通信など)はブロッキングで行われます。これは、I/O操作が完了するまでプログラムの実行がその場で停止(ブロック)することを意味します。

ブロッキングI/Oの問題点:

  • リソースの浪費: サーバーアプリケーションなどで多数のクライアントを同時に処理しようとすると、クライアントごとにOSスレッドやプロセスを割り当てる必要が出てきます。I/O待ちの間、これらのスレッド/プロセスはCPUを使っていないにも関わらず、メモリやコンテキストスイッチのオーバーヘッドを生み出します。
  • 応答性の低下: ある処理がI/Oでブロックされると、他の処理も待たされてしまう可能性があります(特にシングルスレッドの場合)。

Lwtのアプローチ: ノンブロッキングI/Oと協調的マルチタスキング Lwtはこれらの問題を解決するために、以下の仕組みを提供します。

  • ノンブロッキングI/O: LwtのI/O関数は、操作が完了するのを待たずに即座に制御を返します。実際のI/O処理はバックグラウンドで行われ、完了したら指定された処理(コールバック)が実行されます。
  • Promise (プロミス): Lwtは、非同期処理の結果を表す「Promise」(Lwtでは Lwt.t 型)という概念を中核に据えています。Promiseは「将来得られる値」または「将来発生するかもしれないエラー」をカプセル化します。これにより、非同期処理の連鎖をコールバック地獄に陥らずに、より構造化された形で記述できます。
  • 協調的マルチタスキング: LwtのスレッドはOSのネイティブスレッドとは異なり、非常に軽量です。Lwtスレッドは、明示的に実行権を譲る (yield) か、LwtのノンブロッキングI/O関数を呼び出したときに切り替わります。これにより、シングルコアでも多数のタスクを効率的に並行実行できます。

Lwtの利点まとめ:

  • 多数のI/Oバウンドなタスクを少ないリソースで効率的に処理できる。
  • 非同期コードをPromiseを使って比較的読みやすく書ける。
  • 応答性の高いアプリケーションを構築しやすい。

2. セットアップと最初のLwtプログラム

Lwtを使うには、まずライブラリをインストールし、ビルドシステム (dune) を設定する必要があります。

インストール: opam (OCamlのパッケージマネージャ) を使ってインストールします。lwt_ppxlet* などの便利な構文を提供してくれるPPX(プリプロセッサ)です。

opam install lwt lwt_ppx

Duneプロジェクトの設定: dune ファイルでLwtライブラリとPPXを指定します。 例えば、my_lwt_project/bin/main.ml というファイルでLwtを使う場合、my_lwt_project/bin/dune ファイルは以下のようになります。

; my_lwt_project/bin/dune
(executable
 (name main)
 (preprocess (pps lwt_ppx))
 (libraries lwt lwt.unix)) ; Lwt_unixも使う場合

lwt.unixLwt_unixLwt_io モジュールなど、Unixシステムコールに関連するLwt機能を提供します。

最初のLwtプログラム: Lwtの処理は Lwt.t 型の値(Promise)として表現されます。これらのPromiseを実行し、結果を得るためには Lwt_main.run 関数を使います。

(* bin/main.ml *)
let greeting_promise : string Lwt.t =
  Lwt.return "Hello, Lwt!" (* 'a -> 'a Lwt.t : 値をPromiseで包む *)

let main () : unit Lwt.t =
  let promise_message = greeting_promise in
  (* Promiseが解決されたら、その値を使って次の処理を行う *)
  Lwt.bind promise_message (fun message ->
    print_endline message;
    Lwt.return () (* print_endlineはunitを返すので、Lwt.return () で unit Lwt.t を作る *)
  )

(* Lwt_main.run でLwtの計算を開始し、完了するまで待つ *)
let () = Lwt_main.run (main ())

コンパイルと実行 (プロジェクトルート my_lwt_project/ で):

dune build
./_build/default/bin/main.exe

出力:

Hello, Lwt!

3. Lwtのコアコンセプト: Promiseとlet*構文

'a Lwt.t 型 (Promise): Lwtの中心となるのは 'a Lwt.t 型です。これは「将来的に 'a 型の値が得られる(またはエラーで失敗する)かもしれない計算」を表すPromiseです。 Promiseには以下の3つの状態があります:

  1. Pending (保留中): 計算は進行中。
  2. Resolved (解決済み / Fulfilled): 計算が成功し、値が得られた。
  3. Rejected (拒否済み / Failed): 計算が失敗し、例外が発生した。

Lwt.return : 'a -> 'a Lwt.t 既に値が分かっている場合に、それをPromiseで包みます。即座に解決済みのPromiseを作成します。

Lwt.bind : 'a Lwt.t -> ('a -> 'b Lwt.t) -> 'b Lwt.t (演算子 >>=) これがLwtで非同期処理を繋ぐ基本的な方法です。 p >>= f は、「Promise p が解決されたら、その結果の値を関数 f に渡し、f が返す新しいPromise 'b Lwt.t を全体の計算結果とする」という意味です。

(* bin/main.ml *)
open Lwt.Infix (* >>= を使えるようにする *)

let ask_name () : string Lwt.t =
  print_endline "What is your name?";
  (* 標準入力は通常ブロッキングだが、Lwt_io.read_lineはノンブロッキング *)
  Lwt_io.read_line Lwt_io.stdin

let greet (name : string) : unit Lwt.t =
  Lwt_io.printf "Hello, %s!\n" name (* Lwt_io.printf も unit Lwt.t を返す *)

let main () : unit Lwt.t =
  ask_name () >>= fun name -> (* ask_nameが完了したら、その結果(name)をgreetに渡す *)
  greet name

let () = Lwt_main.run (main ())

let* 構文 (ppx_let): >>= を多用すると、コードがネストして読みにくくなることがあります (コールバック地獄に似た形)。lwt_ppx が提供する let* 構文 (OCaml 4.08以降の let-syntax 拡張) を使うと、非同期コードを同期コードのようにより自然に書けます。

let* x = p in eLwt.bind p (fun x -> e) とほぼ等価です。 let+ x = p in eLwt.map (fun x -> e) p とほぼ等価です(Lwt.map f pp >>= fun x -> Lwt.return (f x))。

先程の例を let* で書き換えると:

(* bin/main.ml *)
(* open Lwt.Infix は let* を使う場合は不要なことが多い *)

let ask_name () : string Lwt.t =
  let* () = Lwt_io.printl "What is your name?" in (* Lwt_io.printl は unit Lwt.t を返す *)
  Lwt_io.read_line Lwt_io.stdin

let greet (name : string) : unit Lwt.t =
  Lwt_io.printf "Hello, %s!\n" name

let main () : unit Lwt.t =
  let* name = ask_name () in
  greet name

let () = Lwt_main.run (main ())

こちらのほうがスッキリして読みやすいですね。今後、このチュートリアルでは主に let* 構文を使用します。


4. エラーハンドリング

非同期処理ではエラーハンドリングが重要です。LwtのPromiseは成功だけでなく失敗(例外)も伝えることができます。

Lwt.fail : exn -> 'a Lwt.t 指定された例外で失敗するPromiseを作成します。

Lwt.catch : (unit -> 'a Lwt.t) -> (exn -> 'a Lwt.t) -> 'a Lwt.t 最初の関数(Promiseを返す)を実行し、もしそれが例外で失敗したら、2番目の関数(例外ハンドラ)を実行します。

try%lwt ... with ... 構文 (ppx_let): lwt_ppx は、標準の try ... with ... と似た try%lwt ... with ... 構文を提供します。

(* bin/main.ml *)
let may_fail (succeed : bool) : string Lwt.t =
  if succeed then
    Lwt.return "Operation succeeded!"
  else
    Lwt.fail (Failure "Operation failed intentionally.")

let run_operation (succeed : bool) : unit Lwt.t =
  try%lwt
    let* message = may_fail succeed in
    Lwt_io.printf "Result: %s\n" message
  with
  | Failure msg -> Lwt_io.printf "Caught error: %s\n" msg
  | exn -> Lwt_io.printf "Caught an unexpected error: %s\n" (Printexc.to_string exn)

let main () : unit Lwt.t =
  let* () = run_operation true in
  run_operation false

let () = Lwt_main.run (main ())

出力:

Result: Operation succeeded!
Caught error: Operation failed intentionally.

5. Lwtによる並行処理

Lwtの真価は並行処理にあります。複数の非同期タスクを同時に開始し、それらの完了を待つことができます。

Lwt_unix.sleep : float -> unit Lwt.t 指定された秒数だけノンブロッキングで待機します。この間、他のLwtスレッドが実行可能です。

逐次実行と並行実行の比較:

(* bin/main.ml *)
let task (name : string) (delay : float) : unit Lwt.t =
  let* () = Lwt_unix.sleep delay in
  Lwt_io.printf "Task %s finished after %.1f seconds.\n" name delay

let sequential_run () : unit Lwt.t =
  Lwt_io.printl "--- Sequential Run ---" >>= fun () ->
  let* () = task "A" 1.0 in
  let* () = task "B" 1.0 in
  Lwt.return () (* Total time: approx 2.0 seconds *)

let concurrent_run () : unit Lwt.t =
  Lwt_io.printl "--- Concurrent Run ---" >>= fun () ->
  let promise_a = task "C" 1.0 in (* タスクCを開始 *)
  let promise_b = task "D" 1.0 in (* タスクDを開始 *)
  (* promise_a と promise_b は既に並行して実行開始されている *)
  let* () = promise_a in (* タスクCの完了を待つ *)
  let* () = promise_b in (* タスクDの完了を待つ *)
  Lwt.return () (* Total time: approx 1.0 second *)

let main () : unit Lwt.t =
  let* () = sequential_run () in
  concurrent_run ()

let () = Lwt_main.run (main ())

出力 (実行タイミングによりCとDの出力順は変わることがあります):

--- Sequential Run ---
Task A finished after 1.0 seconds.
Task B finished after 1.0 seconds.
--- Concurrent Run ---
Task C finished after 1.0 seconds.
Task D finished after 1.0 seconds.

(または D が C より先に表示される) concurrent_run の方が全体の実行時間が短いことに注目してください。

複数のPromiseを扱う関数:

  • Lwt.join : unit Lwt.t list -> unit Lwt.t: unit Lwt.t のリストを受け取り、全てのPromiseが完了するまで待つ新しいPromiseを返します。結果の値は捨てられます。
  • Lwt.all : 'a Lwt.t list -> 'a list Lwt.t: 'a Lwt.t のリストを受け取り、全てのPromiseが完了したら、それらの結果をリストとして返す新しいPromiseを返します。一つでも失敗すると全体が失敗します。
  • Lwt.both : 'a Lwt.t -> 'b Lwt.t -> ('a * 'b) Lwt.t: 2つのPromiseを受け取り、両方が完了したら、それらの結果のペアを返す新しいPromiseを返します。
  • Lwt.pick : 'a Lwt.t list -> 'a Lwt.t: Promiseのリストを受け取り、最初に解決(成功または失敗)したPromiseの結果を返します。他のPromiseはキャンセルされることがあります(Lwtのバージョンや状況による)。
(* bin/main.ml *)
let fetch_data (id : int) (delay : float) : string Lwt.t =
  let* () = Lwt_unix.sleep delay in
  Lwt.return (Printf.sprintf "Data %d (waited %.1fs)" id delay)

let main () : unit Lwt.t =
  let* () = Lwt_io.printl "--- Lwt.join example ---" in
  let tasks_for_join = [
    (Lwt_unix.sleep 0.5 >>= fun () -> Lwt_io.printl "Join task 1 done");
    (Lwt_unix.sleep 0.2 >>= fun () -> Lwt_io.printl "Join task 2 done");
  ] in
  let* () = Lwt.join tasks_for_join in

  let* () = Lwt_io.printl "\n--- Lwt.all example ---" in
  let tasks_for_all = [
    fetch_data 1 0.3;
    fetch_data 2 0.1;
  ] in
  let* results = Lwt.all tasks_for_all in
  List.iter (fun s -> Printf.printf "Result: %s\n" s) results;
  Lwt.return_unit >>= fun () ->

  let* () = Lwt_io.printl "\n--- Lwt.pick example ---" in
  let tasks_for_pick = [
    fetch_data 3 0.5; (* This will likely be picked or the one below *)
    fetch_data 4 0.1; (* This will likely be picked *)
    (Lwt_unix.sleep 0.2 >>= fun () -> Lwt.fail (Failure "Picker failed"))
  ] in
  try%lwt
    let* picked_result = Lwt.pick tasks_for_pick in
    Lwt_io.printf "Picked: %s\n" picked_result
  with
  | Failure msg -> Lwt_io.printf "Picked failure: %s\n" msg
  | exn -> Lwt_io.printf "Picked unexpected error: %s\n" (Printexc.to_string exn)


let () = Lwt_main.run (main ())

6. Lwtフレンドリーなライブラリの活用 (Lwt_unix, Lwt_io)

Lwt自体はコアな並行処理機能を提供しますが、具体的なI/O操作はLwtに対応したライブラリを使います。

Lwt_unix: Unixシステムコールの多く(ファイル操作、ネットワーク、プロセス管理など)をLwtのノンブロッキングAPIとして提供します。

  • Lwt_unix.openfile, Lwt_unix.read, Lwt_unix.write, Lwt_unix.close
  • Lwt_unix.socket, Lwt_unix.bind, Lwt_unix.connect, Lwt_unix.accept, Lwt_unix.recv, Lwt_unix.send
  • Lwt_unix.sleep (既に使いましたね)
  • Lwt_unix.system (コマンドの非同期実行)

Lwt_io: Lwt_unix のファイルディスクリプタをベースに、より高レベルなOCamlの in_channel/out_channel に似たインターフェースを提供します。行単位の読み書きなどが容易になります。

  • Lwt_io.open_file, Lwt_io.close
  • Lwt_io.read_line, Lwt_io.read_line_opt, Lwt_io.read_char, Lwt_io.read
  • Lwt_io.write_line, Lwt_io.write_char, Lwt_io.write, Lwt_io.flush
  • Lwt_io.printl, Lwt_io.printf, Lwt_io.eprintf
  • Lwt_io.stdin, Lwt_io.stdout, Lwt_io.stderr (Lwt版の標準入出力チャネル)

例: Lwt_io でファイルから読み込み

(* bin/main.ml *)
(* (実行前に test.txt というファイルを作成しておいてください) *)
(* e.g., echo "Hello Lwt\nThis is a test" > test.txt *)

let read_file_contents (filename : string) : unit Lwt.t =
  try%lwt
    (* ファイルを読み込みモードで開く *)
    let* ic = Lwt_io.open_file ~mode:Lwt_io.Input filename in
    let rec loop () =
      try%lwt
        let* line = Lwt_io.read_line ic in
        let* () = Lwt_io.printf "Read line: %s\n" line in
        loop ()
      with End_of_file ->
        Lwt.return () (* ファイル終端に達したら終了 *)
    in
    let* () = loop () in
    Lwt_io.close ic
  with
  | Unix.Unix_error (err, _, _) ->
    Lwt_io.eprintf "Error reading file %s: %s\n" filename (Unix.error_message err)
  | exn ->
    Lwt_io.eprintf "An unexpected error occurred: %s\n" (Printexc.to_string exn)

let main () : unit Lwt.t =
  read_file_contents "test.txt"

let () = Lwt_main.run (main ())

7. 実践的な例: 非同期ファイル処理

複数のURLからコンテンツを(ダミーで)ダウンロードし、それぞれの文字数を表示するプログラムを考えてみましょう。

(* bin/main.ml *)
(* dune ファイルに cohttp-lwt-unix を追加してください:
   (libraries lwt lwt.unix cohttp-lwt-unix)
   そして opam install cohttp-lwt-unix を実行
*)
open Cohttp_lwt_unix (* HTTPクライアント機能のため *)

let fetch_url_content (url_str : string) : (string * string) Lwt.t =
  let uri = Uri.of_string url_str in
  Lwt_io.printf "Fetching %s...\n" url_str >>= fun () ->
  try%lwt
    Client.get uri >>= fun (resp, body) ->
    let status = Cohttp.Response.status resp in
    if Cohttp.Code.is_success (Cohttp.Code.code_of_status status) then
      Cohttp_lwt.Body.to_string body >>= fun body_str ->
      Lwt.return (url_str, body_str)
    else
      let err_msg = Printf.sprintf "Failed to fetch %s: %s" url_str (Cohttp.Code.string_of_status status) in
      Lwt.return (url_str, err_msg)
  with exn ->
    let err_msg = Printf.sprintf "Exception while fetching %s: %s" url_str (Printexc.to_string exn) in
    Lwt.return (url_str, err_msg)


let main () : unit Lwt.t =
  let urls = [
    "https://ocaml.org";
    "http://example.com";
    "https://nonexistent.dummy.domain/hopefully"; (* This should fail *)
  ] in

  (* 各URLに対して fetch_url_content を呼び出し、Promiseのリストを作成 *)
  let promises = List.map fetch_url_content urls in

  (* 全てのPromiseが完了するのを待つ *)
  let* results = Lwt.all promises in

  List.iter (fun (url, content_or_error) ->
    if String.starts_with ~prefix:"Failed" content_or_error || String.starts_with ~prefix:"Exception" content_or_error then
      Lwt_io.printf "Error for %s: %s\n" url content_or_error
    else
      Lwt_io.printf "Content length from %s: %d\n" url (String.length content_or_error)
    |> Lwt.ignore_result (* Lwt_io.printf は Lwt.t を返すので、結果を無視する *)
  ) results;
  Lwt.return_unit

let () = Lwt_main.run (main ())

この例では、Cohttp_lwt_unix ライブラリを使って実際のHTTP GETリクエストを非同期に行っています。各リクエストは並行して発行され、Lwt.all ですべての完了を待ちます。


8. Lwtを使う上での注意点とベストプラクティス

  • LwtスレッドはOSスレッドではない: Lwtは協調的マルチタスキングです。CPUを長時間占有する計算(重い計算ループなど)をLwtスレッド内で直接実行すると、他のLwtスレッドの実行がブロックされてしまいます。そのようなCPUバウンドな処理は、Lwt_preemptive.detach を使って別のOSスレッドで実行するか、別のプロセスに分離することを検討してください。
  • ブロッキング呼び出しを避ける: Lwtのコード内で、通常のブロッキングI/O関数(例: Unix.sleep, 標準の input_line)を使わないでください。これらはLwtのスケジューラ全体をブロックします。常にLwt版の関数(Lwt_unix.sleep, Lwt_io.read_lineなど)を使用してください。
  • エラーハンドリングの徹底: 全てのPromiseは失敗する可能性があります。try%lwt ... with ...Lwt.catch を使って適切にエラーを処理しましょう。未処理の例外は Lwt_main.run で捕捉されるか、プログラムをクラッシュさせる可能性があります。
  • Lwt_main.run は通常トップレベルで一度だけ: Lwt_main.run はLwtのイベントループを開始し、指定されたPromiseが完了するまでブロックします。Lwtのコードの中から再度 Lwt_main.run を呼び出すべきではありません。
  • Promiseリークに注意: 作成したPromiseを let* で束縛しなかったり、Lwt.joinLwt.all などで待たなかったり、Lwt.ignore_result で明示的に無視しなかったりすると、そのPromiseは実行されないか、バックグラウンドで実行されてもその結果や発生したエラーが捕捉されません。これはバグの原因となります。 もしPromiseを起動して結果を気にしない("fire and forget")場合は、Lwt.async (fun () -> your_promise) を使います。これはPromiseを実行し、もしエラーが発生した場合はLwtのデフォルトエラーハンドラ(通常は標準エラー出力に表示)に送ります。

9. まとめと次のステップ

このチュートリアルでは、Lwtの基本的な概念と使い方を学びました。

  • LwtはノンブロッキングI/Oと協調的マルチタスキングにより、効率的な並行処理を実現します。
  • Promise ('a Lwt.t) と let* 構文により、非同期コードを構造化して記述できます。
  • Lwt_unixLwt_io などのLwtエコシステムのライブラリを活用することで、さまざまな非同期タスクに対応できます。

次のステップ:

  • Lwtの公式ドキュメント (https://ocsigen.org/lwt/manual/) を読んで、より詳細な機能やAPIについて学んでみましょう。
  • Cohttp-lwt-unix を使って、より複雑なHTTPクライアントやサーバーを作成してみましょう。
  • データベースアクセス (Caqti-lwt) やWebSocket通信など、他のLwt対応ライブラリを試してみましょう。
  • MirageOS (https://mirage.io/) のような、Lwtを基盤としたユニカーネルプロジェクトに触れてみるのも面白いでしょう。

Lwtを使いこなせば、OCamlで高性能かつスケーラブルなネットワークアプリケーションやツールを開発できるようになります。Happy Lwt programming!

(返答終了理由: STOP)


ユーザー: テストコードはどのようなものになりますか?


AI: Lwtを使ったコードのテストは、非同期性やPromiseの扱いを考慮に入れる必要があります。ここでは、人気のあるテストフレームワーク Alcotest とそのLwt拡張 Alcotest_lwt を使ったテストコードの例を紹介します。

1. セットアップ

まず、テストに必要なライブラリをインストールします。

opam install alcotest alcotest-lwt

次に、dune ファイルを更新して、テスト実行可能ファイルとライブラリ依存関係を設定します。 プロジェクトのルートに test ディレクトリを作成し、その中にテストコードを置くことを想定します。

dune ファイル (プロジェクトルート):

(library
 (name my_lwt_project) ; あなたのライブラリ/プロジェクト名
 (preprocess (pps lwt_ppx))
 (libraries lwt lwt.unix)) ; プロジェクトが必要とするライブラリ

(test
 (name test_runner) ; テスト実行可能ファイルの名前
 (modules tests)    ; テストコードが含まれるモジュール (例: tests.ml)
 (preprocess (pps lwt_ppx))
 (libraries my_lwt_project lwt lwt.unix alcotest alcotest-lwt))

ここでは、テスト対象のコードが my_lwt_project というライブラリにあり、テストコードが tests.ml というファイルに書かれていると仮定します。

テスト対象のサンプルモジュール (lib/my_logic.ml): まず、テスト対象となる簡単なLwt関数をいくつか定義しましょう。

(* lib/my_logic.ml *)
let identity_async (x : 'a) : 'a Lwt.t =
  Lwt_unix.sleep 0.01 >>= fun () -> (* わずかな非同期処理をシミュレート *)
  Lwt.return x

let add_async (a : int) (b : int) : int Lwt.t =
  Lwt_unix.sleep 0.01 >>= fun () ->
  Lwt.return (a + b)

let might_fail_async (succeed : bool) : string Lwt.t =
  Lwt_unix.sleep 0.01 >>= fun () ->
  if succeed then
    Lwt.return "Success"
  else
    Lwt.fail (Failure "Operation failed as intended")

let process_list_concurrently (items : int list) : int list Lwt.t =
  let double_async n =
    Lwt_unix.sleep 0.01 >>= fun () ->
    Lwt.return (n * 2)
  in
  Lwt.all (List.map double_async items)

let read_string_lines_count (content : string) : int Lwt.t =
  let ic = Lwt_io.of_bytes ~mode:Lwt_io.Input (Lwt_bytes.of_string content) in
  let rec count_lines acc =
    match%lwt Lwt_io.read_line_opt ic with
    | Some _ -> count_lines (acc + 1)
    | None -> Lwt.return acc
  in
  let* count = count_lines 0 in
  let* () = Lwt_io.close ic in
  Lwt.return count

このモジュールが my_lwt_project ライブラリの一部としてコンパイルされるように、lib/dune ファイルも適切に設定されているとします。

; lib/dune
(library
 (name my_logic)
 (public_name my_lwt_project.my_logic) ; プロジェクト名.モジュール名 の形で公開
 (preprocess (pps lwt_ppx))
 (libraries lwt lwt.unix))

そして、プロジェクトルートの dune ファイルでこのライブラリを参照するようにします。

; dune (プロジェクトルート)
(library
 (name my_lwt_project)
 (modules my_logic) ; lib/my_logic.ml を含める
 (preprocess (pps lwt_ppx))
 (libraries lwt lwt.unix))

(test
 (name test_runner)
 (modules tests)
 (preprocess (pps lwt_ppx))
 (libraries my_lwt_project lwt lwt.unix alcotest alcotest-lwt))

2. テストコードの記述 (test/tests.ml)

Alcotest_lwt を使うと、テストケース関数は unit -> unit Lwt.t という型になります。Alcotest_lwt.test_case でテストケースを定義し、Alcotest_lwt.run でテストスイートを実行します。

(* test/tests.ml *)
open My_lwt_project.My_logic (* テスト対象のモジュールを開く *)

(* Alcotestの基本的な型定義 (文字列、整数など) *)
let string_testable = Alcotest.string
let int_testable = Alcotest.int
let int_list_testable = Alcotest.(list int)

(* 1. 単純な成功するPromiseのテスト *)
let test_identity_async () =
  let expected = "hello" in
  let* actual = identity_async "hello" in
  Alcotest.(check string_testable) "should return the same string" expected actual;
  Lwt.return_unit (* テストケースは unit Lwt.t を返す *)

let test_add_async () =
  let* result = add_async 3 5 in
  Alcotest.(check int_testable) "should return sum of two numbers" 8 result;
  Lwt.return_unit

(* 2. 失敗するPromise (例外) のテスト *)
let test_might_fail_async_success () =
  let* result = might_fail_async true in
  Alcotest.(check string_testable) "should succeed" "Success" result;
  Lwt.return_unit

let test_might_fail_async_failure () =
  (* Alcotest_lwt.check_raises は (unit -> 'a Lwt.t) 型の関数を期待する *)
  Alcotest_lwt.check_raises
    "should fail with Failure exception"
    (Failure "Operation failed as intended")
    (fun () -> might_fail_async false)
  (* check_raises 自体が unit Lwt.t を返す *)

(* 3. 複数のPromiseを扱うテスト (Lwt.all) *)
let test_process_list_concurrently () =
  let input = [1; 2; 3] in
  let expected = [2; 4; 6] in
  let* actual = process_list_concurrently input in
  Alcotest.(check int_list_testable) "should double each element" expected actual;
  Lwt.return_unit

let test_process_empty_list_concurrently () =
  let input = [] in
  let expected = [] in
  let* actual = process_list_concurrently input in
  Alcotest.(check int_list_testable) "should handle empty list" expected actual;
  Lwt.return_unit

(* 4. Lwt_io を使った関数のテスト *)
let test_read_string_lines_count () =
  let content = "line1\nline2\nline3" in
  let expected = 3 in
  let* actual = read_string_lines_count content in
  Alcotest.(check int_testable) "should count lines correctly" expected actual;
  Lwt.return_unit

let test_read_empty_string_lines_count () =
  let content = "" in
  let expected = 0 in (* 空文字列は0行 (EOFがすぐ来るので read_line_opt は None を返す) *)
  let* actual = read_string_lines_count content in
  Alcotest.(check int_testable) "should count 0 for empty string" expected actual;
  Lwt.return_unit

let test_read_string_no_newline_lines_count () =
  let content = "single line no newline" in
  let expected = 1 in
  let* actual = read_string_lines_count content in
  Alcotest.(check int_testable) "should count 1 for single line no newline" expected actual;
  Lwt.return_unit


(* テストスイートの定義 *)
let suite =
  [
    ("Identity Async", `Quick, test_identity_async);
    ("Add Async", `Quick, test_add_async);
    ("Might Fail Async (Success)", `Quick, test_might_fail_async_success);
    ("Might Fail Async (Failure)", `Quick, test_might_fail_async_failure);
    ("Process List Concurrently", `Quick, test_process_list_concurrently);
    ("Process Empty List Concurrently", `Quick, test_process_empty_list_concurrently);
    ("Read String Lines Count (basic)", `Quick, test_read_string_lines_count);
    ("Read String Lines Count (empty)", `Quick, test_read_empty_string_lines_count);
    ("Read String Lines Count (no newline)", `Quick, test_read_string_no_newline_lines_count);
  ]

(* テストの実行 *)
let () =
  Lwt_main.run (Alcotest_lwt.run "My Lwt Project Tests" [("My_logic", suite)])
  (* Alcotest_lwt.run は (string * test_case list) list -> unit Lwt.t を返すので Lwt_main.run で実行 *)

解説:

  • Alcotest_lwt.test_case: Alcotest.test_case と似ていますが、テスト関数が unit -> unit Lwt.t を返す点が異なります。Alcotest_lwtがPromiseの解決を待ちます。 実際には、上記の suite のように直接タプル (string * Alcotest.speed_level * (unit -> unit Lwt.t)) を使います。Alcotest_lwtが内部でこれらを適切に処理します。
  • Alcotest.(check ...): LwtのPromiseの結果 (let* actual = ... in) を得てから、通常のアサーションを行います。
  • Alcotest_lwt.check_raises: Promiseが特定の例外で失敗することをテストします。テスト対象のPromiseを返す関数を unit -> 'a Lwt.t の形で渡します。
  • Lwt_main.run (Alcotest_lwt.run ...): テストスイート全体を Alcotest_lwt.run で設定し、それが返す unit Lwt.tLwt_main.run で実行します。
  • モジュール参照: open My_lwt_project.My_logic のようにして、テスト対象の関数にアクセスします。 dune ファイルで (public_name my_lwt_project.my_logic) としているため、この名前でアクセスできます。もし public_name を指定していなければ、My_logic のように直接モジュール名でアクセスできる場合もあります (dune の設定による)。

3. テストの実行

プロジェクトのルートディレクトリで以下のコマンドを実行します。

dune runtest

または

dune test

これにより、test_runner.exe がビルドされ、実行されます。成功すれば、各テストケースがパスした旨が表示されます。失敗した場合は、どのテストがどの理由で失敗したかの詳細が出力されます。

4. モックについて

上記の read_string_lines_count の例では、Lwt_io.of_bytes を使って文字列から直接Lwtの入力チャネルを作成しています。これは、ファイルI/Oを伴う関数をテストする際の一つの方法で、実際のファイルシステムへの依存を避けてインメモリでテストできます。

より複雑な外部依存(ネットワーク、データベースなど)を持つLwtコードをテストする場合、以下のようなアプローチが考えられます:

  1. インターフェースの抽象化: 外部サービスへのアクセス部分をモジュールシグネチャ(インターフェース)として定義し、実際の処理ではそのインターフェースに依存するようにします。テスト時には、そのインターフェースのモック実装を提供します。
    (* service.mli *)
    module type HttpClient = sig
      val get : string -> string Lwt.t
    end
    
    (* my_code_using_http.ml *)
    module Make (Client : Service.HttpClient) = struct
      let fetch_and_process url =
        let* data = Client.get url in
        (* process data *)
        Lwt.return (String.length data)
    end
    
    (* test_my_code.ml *)
    module MockHttpClient = struct
      let get url =
        if url = "http://example.com/data" then Lwt.return "mocked data"
        else Lwt.fail (Failure "Unknown URL")
    end
    
    module TestedCode = My_code_using_http.Make(MockHttpClient)
    
    let test_fetch () =
      let* len = TestedCode.fetch_and_process "http://example.com/data" in
      Alcotest.(check int) "length of mocked data" 11 len;
      Lwt.return_unit
  2. 設定による切り替え: テスト環境ではローカルのスタブサーバーやインメモリデータベースを使用するように設定を切り替える。
  3. 部分的なモック: ライブラリによっては、特定の関数を一時的に別の実装に差し替える機能を提供している場合があります(例: Lwt_unix の一部関数をテスト用にラップするなど)。これは高度なテクニックであり、注意が必要です。

Lwtのテストは、非同期の流れを意識し、Promiseが期待通りに解決されるか(値または例外)を確認することが中心となります。Alcotest_lwtはそのための強力なサポートを提供してくれます。

(返答終了理由: STOP)


ユーザー: eioへ橋渡ししてください


AI: Lwtのチュートリアル、お疲れ様でした!LwtはOCamlの非同期プログラミングにおいて長らく中心的な役割を果たしてきました。 さて、次に注目すべきは Eio です。Eioは、Lwtとは異なるアプローチでOCamlの並行処理とI/Oを扱うための比較的新しいライブラリです。Lwtの経験を活かしつつ、Eioがどのような課題を解決しようとしているのか、そしてどのように使うのかを見ていきましょう。

Eioへの橋渡し: なぜ新しいライブラリが必要なのか?

LwtはPromiseベースの非同期処理を提供し、多くのユースケースで非常に効果的です。しかし、いくつかの課題や、よりモダンなアプローチへの希求がありました。

  1. コールバック地獄とlet*の限界: Lwtのlet* (bind) はコールバック地獄を大幅に改善しましたが、それでも非同期処理の連鎖は、同期的なコードとは見た目やメンタルモデルが異なります。深いネストや複雑な制御フローは依然として読みにくくなることがあります。

  2. 協調的マルチタスキングの課題: Lwtのスレッドは協調的です。これは軽量である一方、CPUバウンドなタスクが長時間実行権を譲らないと、他のタスクが飢餓状態になる可能性があります。Lwt_preemptive でOSスレッドに逃がすことはできますが、ひと手間かかります。

  3. リソース管理: Lwtでは、ファイルディスクリプタなどのリソースは手動で閉じるか、Lwt_io.with_open_fileのような専用の構文を使う必要がありました。解放忘れはリソースリークにつながります。

  4. エラー伝播とキャンセル: 複数の並行タスクが絡み合うと、エラーがどこで発生し、どのように伝播・処理されるのか、また、不要になったタスクをどのように安全にキャンセルするのかが複雑になりがちです。

Eioのアプローチ: 構造化並行性 (Structured Concurrency) と直接的スタイル (Direct Style)

Eioはこれらの課題に対応するため、以下の原則に基づいています。

  • 構造化並行性: 並行タスクの親子関係を明確にし、親タスクが終了する際には全ての子タスクも確実に終了する(またはキャンセルされる)ことを保証します。これにより、「孤児タスク」やリソースリークを防ぎやすくなります。Eioでは Switch.run がこの役割を担います。

  • 直接的スタイル: OCaml 5.0で導入されたエフェクトハンドラ (Effect Handlers) という強力な機能を活用し、非同期I/Oのコードをあたかも同期的なコードのように書けるようにします。let* のような特別な構文は不要で、通常の関数呼び出しのように見えます。しかし、内部的にはI/O操作でブロックする代わりに中断し、完了したら再開します。

  • 明確なエラーハンドリング: エラーは通常の例外として扱われ、構造化並行性の枠組みの中で適切に伝播します。

  • マルチコア対応: Eioは当初からマルチコア (OCaml 5.0以降のドメイン) を意識して設計されており、CPUバウンドなタスクとI/Oバウンドなタスクをよりシームレスに扱えることを目指しています。

LwtからEioへ: 主な概念の対比

特徴 Lwt Eio 備考
非同期の表現 Promise ('a Lwt.t) (表面上は)通常の関数呼び出し Eioは内部でエフェクトを使って中断・再開
処理の連鎖 Lwt.bind (>>=), let* 通常の逐次実行 (let x = f () in let y = g x in ...) EioではI/O関数が「中断可能な関数」になる
値のラップ Lwt.return 不要(通常の関数が値を返す)
並行処理の開始 Lwt.async, let p = task () (暗黙的) Fiber.fork Eioでは明示的なフォークが必要
並行処理の待機 Lwt.join, Lwt.all, Lwt.pick Fiber.all, Fiber.any, Switch.run (スコープ終了で待機)
リソース管理 手動クローズ, Lwt_io.with_open_file Switch.run によるスコープベース、自動クローズ EioのSwitchは非常に強力
エラー処理 try%lwt ... with ..., Lwt.catch 標準の try ... with ... エラーは例外として伝播
メインループ Lwt_main.run Eio_main.run 引数として環境(env)を受け取る
I/Oプリミティブ Lwt_unix, Lwt_io Eio.Flow, Eio.Path, Eio.Net など (環境から提供) Eioは抽象化されたI/O (Flow) を中心に据える
CPUバウンド Lwt_preemptive.detach Eio.Domain_manager.run (環境経由) Eioはマルチドメイン対応がより組み込まれている

Eioのセットアップと最初のプログラム

インストール:

opam install eio eio_main # eio_main はプラットフォーム依存のバックエンド(unix, luvなど)を提供
# 一般的には eio_main を使うと eio_posix や eio_luv もよしなに選択される

Duneプロジェクトの設定: dune ファイルに eioeio_main を追加します。

(executable
 (name main)
 (libraries eio eio_main)) ; Lwtの依存はここでは削除またはコメントアウト

最初のEioプログラム (標準入力から読み込んで標準出力へ):

(* main.ml *)
(* Lwt関連の open は不要 *)

(* Eioのメイン関数は env を引数に取る *)
let main env =
  Eio.traceln "Enter some text (Ctrl-D to end):";
  let stdin = Eio.Stdenv.stdin env in
  let stdout = Eio.Stdenv.stdout env in
  try
    Eio.Flow.copy stdin stdout
  with End_of_file ->
    Eio.traceln "\nGoodbye!"

(* Eio_main.run でEioプログラムを開始 *)
let () = Eio_main.run main

コンパイルと実行:

dune build
./_build/default/bin/main.exe

Lwtの let* がなく、Eio.Flow.copy がまるで同期的に見えることに注目してください。しかし、内部ではノンブロッキングI/Oが行われています。

Eioのコアコンセプト

  1. 環境 (env): Eio_main.run は、プログラムに「環境」(env) を提供します。この環境を通じて、ファイルシステム (Eio.Stdenv.fs env)、ネットワーク (Eio.Stdenv.net env)、時計 (Eio.Stdenv.clock env)、標準入出力などのリソースにアクセスします。これにより、テスト時にモック環境を注入しやすくなるなどの利点があります。

  2. フロー (Eio.Flow.source, Eio.Flow.sink): Eioは、読み取り可能なソース (source) と書き込み可能なシンク (sink) という統一された抽象化でI/Oを扱います。Eio.Flow.copy source sink は非常に基本的な操作です。

  3. ファイバー (Fiber): Lwtのスレッドに似た、軽量な並行実行の単位です。Fiber.fork ~sw (fun () -> ...) で新しいファイバーを起動します。

  4. スイッチ (Switch): 構造化並行性の要です。Switch.run (fun sw -> ...) でスイッチを作成し、そのスコープ内で起動されたファイバーや開かれたリソースは、スイッチのスコープが終了する際に自動的にクリーンアップ(ファイバーはキャンセル、リソースはクローズ)されます。

    open Eio.Std (* traceln, Fiber, Switch などを直接使えるように *)
    
    let child_task name () =
      traceln "Child task %s: starting" name;
      Eio.Time.sleep (Eio.Stdenv.clock env) 1.0; (* envがスコープ内にある前提 *)
      traceln "Child task %s: finished" name
    
    let main env =
      Eio.Switch.run (fun sw ->
        traceln "Main: Forking child A";
        Fiber.fork ~sw (child_task "A"); (* env は child_task にキャプチャされるか、引数で渡す *)
    
        traceln "Main: Forking child B";
        Fiber.fork ~sw (child_task "B");
    
        traceln "Main: Waiting for children to complete (implicitly by switch ending)"
      );
      traceln "Main: All children finished."
    
    let () = Eio_main.run main

    この例では、main 関数が Eio_main.run から env を受け取ります。child_taskenv を必要とする場合、クロージャとしてキャプチャさせるか、明示的に引数で渡す必要があります。上記だと child_task 内で env が未定義なので、修正が必要です。

    正しい child_taskmain:

    open Eio.Std
    
    let child_task ~clock name () = (* clock を引数で受け取る *)
      traceln "Child task %s: starting" name;
      Eio.Time.sleep clock 1.0;
      traceln "Child task %s: finished" name
    
    let main env =
      let clock = Eio.Stdenv.clock env in (* 環境からclockを取得 *)
      Eio.Switch.run (fun sw ->
        traceln "Main: Forking child A";
        Fiber.fork ~sw (child_task ~clock "A"); (* clockを渡す *)
    
        traceln "Main: Forking child B";
        Fiber.fork ~sw (child_task ~clock "B");
    
        traceln "Main: Waiting for children to complete (implicitly by switch ending)"
      );
      traceln "Main: All children finished."
    
    let () = Eio_main.run main
  5. エラーハンドリング: 標準の try ... with ... を使います。ファイバー内で発生した例外は、Fiber.fork した親に伝播します(適切に処理されない場合)。Switch は、管理下のファイバーのいずれかが例外で終了すると、他のファイバーをキャンセルし、例外を Switch.run の呼び出し元に伝播させます。

    open Eio.Std
    
    let failing_task () =
      traceln "Failing task: starting";
      Eio.Time.sleep (Eio.Stdenv.clock env) 0.5; (* envがスコープに必要 *)
      raise (Failure "Task failed deliberately")
    
    let main env =
      let clock = Eio.Stdenv.clock env in
      try
        Switch.run (fun sw ->
          Fiber.fork ~sw (child_task ~clock "Good"); (* 上記の child_task を使う *)
          Fiber.fork ~sw (fun () -> (* failing_task が env を必要とする場合 *)
            traceln "Failing task: starting";
            Eio.Time.sleep clock 0.5;
            raise (Failure "Task failed deliberately")
          );
          (* Fiber.fork ~sw (failing_task ())  こう書くと env がスコープにない *)
          traceln "Main: tasks forked";
        )
      with Failure msg ->
        traceln "Main: Caught failure - %s" msg
      ;
      traceln "Main: exiting."
    
    
    let () = Eio_main.run main

    ここでも env のスコープに注意が必要です。failing_taskenv を直接参照する場合、Fiber.fork ~sw (failing_task) のように呼び出すのではなく、クロージャ fun () -> failing_task () で囲むか、failing_task が必要なコンポーネント(例: clock)を引数で取るようにします。

    より良い failing_taskmain:

    open Eio.Std
    
    let failing_task ~clock () =
      traceln "Failing task: starting";
      Eio.Time.sleep clock 0.5;
      raise (Failure "Task failed deliberately")
    
    (* child_task は前の例と同じ *)
    let child_task ~clock name () =
      traceln "Child task %s: starting" name;
      Eio.Time.sleep clock 1.0;
      traceln "Child task %s: finished" name
    
    let main env =
      let clock = Eio.Stdenv.clock env in
      try
        Switch.run (fun sw ->
          Fiber.fork ~sw (child_task ~clock "Good");
          Fiber.fork ~sw (failing_task ~clock); (* clock を渡す *)
          traceln "Main: tasks forked, switch will wait for them or an error";
        )
      with Failure msg ->
        traceln "Main: Caught failure from switch - %s" msg
      ;
      traceln "Main: exiting."
    
    let () = Eio_main.run main

    出力例:

    Main: tasks forked, switch will wait for them or an error
    Child task Good: starting
    Failing task: starting
    Main: Caught failure from switch - Task failed deliberately
    Main: exiting.
    

    "Child task Good" が完了する前に "Failing task" がエラーを出し、Switchがそれを補足して child_task もキャンセル(この例ではsleep中なので即座に終了)され、例外が maintry...with に伝播したことがわかります。

Lwt資産との連携: Lwt_eio

既存のLwtベースのコードやライブラリをすぐにEioで書き換えるのは大変です。そこで Lwt_eio というライブラリがあり、LwtのPromiseとEioのファイバーを相互に変換する機能を提供します。

opam install lwt_eio

dune ファイルに lwt_eio を追加します。

(* EioからLwtの関数を呼び出す *)
let run_lwt_promise_in_eio () : string Lwt.t =
  Lwt_unix.sleep 0.1 >>= fun () ->
  Lwt.return "Hello from Lwt promise"

let main env =
  Eio.Switch.run (fun sw ->
    let lwt_domain = Eio.Stdenv.domain_mgr env in (* Lwtを実行するためのドメインを取得 *)
    let result = Lwt_eio.Promise.await_lwt (run_lwt_promise_in_eio ()) in
    (* もし Lwt_eio.run_lwt を使うなら *)
    (* let result = Lwt_eio.run_lwt ~label:"my-lwt-task" ~domains:lwt_domain ~sw run_lwt_promise_in_eio in *)
    Eio.traceln "Eio received from Lwt: %s" result
  )

let () = Eio_main.run main

Lwt_eio.Promise.await_lwt (またはより高レベルな Lwt_eio.run_lwt) を使うと、LwtのPromiseが完了するまで現在のEioファイバーを中断し、結果をEio側で受け取れます。 逆方向 (Lwt_eio.run_eio) も可能です。

Eioの利点と考慮点

利点:

  • コードの見た目が同期的: let* がなくなり、読みやすさが向上する可能性があります。
  • 堅牢なリソース管理: Switch により、リソースリークや孤児タスクのリスクが大幅に低減されます。
  • 明確なエラー伝播: 標準例外と構造化並行性により、エラーの流れが追いやすくなります。
  • テスト容易性: 環境(env) を介した依存性注入が容易です。
  • マルチコア親和性: 設計初期からドメイン(マルチコア)を意識しています。

考慮点:

  • エコシステム: Lwtに比べてライブラリの数はまだ少ないですが、急速に成長しています。主要なものは揃いつつあります (e.g., eio-cohttp, eio-pgx)。
  • 学習コスト: エフェクトハンドラという新しい概念や、Switch を中心とした構造化並行性の考え方に慣れが必要です。
  • OCaml 5.0以降が必要: エフェクトハンドラを利用するため、古いOCamlバージョンでは使えません。

まとめと次のステップ

Eioは、OCamlにおける並行I/Oプログラミングの新しいパラダイムを提案しています。Lwtで培った非同期処理の考え方を基礎にしつつ、構造化並行性と直接的スタイルによって、より堅牢で読みやすいコードを目指すことができます。

次のステップ:

  1. Eioの公式ドキュメントやチュートリアルを読む:
  2. 簡単なEioプログラムを書いてみる:
    • ファイル操作、ネットワーククライアント/サーバーなど。
    • SwitchFiber.fork を使った並行処理を試す。
  3. 既存のLwtコードの一部をLwt_eioを使ってEioから呼び出してみる: 段階的な移行を体験する。
  4. Eioベースのライブラリを使ってみる: HTTPなら cohttp-eio (または h2-eio)、PostgreSQLなら eio-pgx など。

LwtとEioは、それぞれ異なる時代背景と設計思想を持っています。どちらが良いかはプロジェクトの要件やチームの習熟度にもよりますが、Eioが提供する新しいアプローチは非常に魅力的であり、今後のOCamlエコシステムで重要な位置を占めていくことでしょう。 Eioの世界へようこそ!

(返答終了理由: STOP)


ユーザー: テストコードはどのように代わりますか?


AI: Eioを使ったコードのテストも、Lwtの場合と同様に Alcotest を使うことができますが、Eioの特性に合わせたヘルパーライブラリ Alcotest_eio が存在します。これを使うことで、Eioの環境 (env) やスイッチ (Switch) をテストコード内でうまく扱えるようになります。

1. セットアップ

必要なライブラリをインストールします。

opam install alcotest eio eio_main alcotest_eio

dune ファイルを更新します。テスト対象のEioコードが my_eio_project というライブラリにあると仮定します。

dune ファイル (プロジェクトルート):

(library
 (name my_eio_project) ; あなたのEioプロジェクト名
 (modules my_eio_logic) ; 例: lib/my_eio_logic.ml
 (libraries eio))

(test
 (name test_runner)
 (modules tests)
 (libraries my_eio_project eio eio_main alcotest alcotest_eio))

テスト対象のサンプルモジュール (lib/my_eio_logic.ml):

(* lib/my_eio_logic.ml *)
open Eio.Std

let identity_direct (x : 'a) ~clock =
  Time.sleep clock 0.01; (* わずかな非同期処理をシミュレート *)
  x

let add_direct (a : int) (b : int) ~clock =
  Time.sleep clock 0.01;
  a + b

let might_fail_direct (succeed : bool) ~clock =
  Time.sleep clock 0.01;
  if succeed then
    "Success"
  else
    raise (Failure "Operation failed as intended")

let process_list_concurrently (items : int list) ~sw ~clock =
  let double_item n =
    Time.sleep clock 0.01;
    n * 2
  in
  Fiber.List.map ~sw (fun item -> double_item item) items
  (* Fiber.List.map は Eio 0.12 時点ではまだないかもしれません。
     その場合は、Fiber.fork と結果を集めるロジックが必要です。
     ここでは、もし存在すると仮定した Fiber.List.map を使います。
     なければ、以下のように手動で実装します:

     Switch.run (fun sw_inner -> (* もし外部のswを使いたくない場合 *)
       let results = ref [] in
       let wg = Eio.Condition.create () in
       let remaining = ref (List.length items) in
       List.iter (fun item ->
         Fiber.fork ~sw:sw_inner (fun () ->
           let res = double_item item in
           Mutex.use_rw ~protect:true (Eio.Condition.get_mutex wg) (fun () ->
             results := res :: !results;
             decr remaining;
             if !remaining = 0 then Eio.Condition.broadcast wg
           )
         )
       ) items;
       Mutex.use_rw ~protect:true (Eio.Condition.get_mutex wg) (fun () ->
         while !remaining > 0 do
           Eio.Condition.await_no_mutex wg
         done
       );
       List.rev !results (* 順序を保つため rev *)
     )
     この手動実装は複雑なので、実際にはFiber.allと似たようなことをするか、
     Eioのユーティリティに頼りたいところです。
     Eioのバージョンアップでユーティリティが増えることを期待しましょう。
     ここでは Fiber.List.map があると仮定して進めます。
  *)


let read_string_lines_count (content : string) =
  let flow = Eio.Flow.string_source content in
  let buf_reader = Eio.Buf_read.of_flow flow ~max_size:1024 in
  let rec count_lines acc =
    match Eio.Buf_read.line buf_reader with
    | line ->
        traceln "Read: %s" line;
        count_lines (acc + 1)
    | exception End_of_file -> acc
  in
  count_lines 0

Fiber.List.map は Eio のバージョンによっては提供されていないかもしれません。その場合は、Fiber.fork をループで使い、結果を Eio.Promiseref とミューテックス/コンディション変数で集めるなど、より手動での実装が必要になります。ここでは、そのようなユーティリティが存在すると仮定して進めます。もしなければ、テスト対象の関数は逐次処理にするか、テストコード側で並行処理をシミュレートする形になります。

lib/dune ファイル:

; lib/dune
(library
 (name my_eio_logic)
 (public_name my_eio_project.my_eio_logic)
 (libraries eio))

2. テストコードの記述 (test/tests.ml)

Alcotest_eio.run を使うと、テストスイートの各テストケース関数は Eio_main. yoki -> unit または Eio_main. yoki -> Switch.t -> unit のような型になります (yoki は環境 env を含みます)。Alcotest_eio がEio環境とトップレベルのスイッチを提供してくれます。

(* test/tests.ml *)
open Eio.Std
open My_eio_project.My_eio_logic (* テスト対象のモジュール *)

(* Alcotestの基本的な型定義 *)
let string_testable = Alcotest.string
let int_testable = Alcotest.int
let int_list_testable = Alcotest.(list int)

(* Alcotest_eio はテスト関数に env と sw を渡してくれる *)
(* 通常は (env, sw) のタプル yoki または個別の引数として受け取る *)

(* 1. 単純な直接的スタイルの関数のテスト *)
(* Alcotest_eio.check は env と sw を引数に取る関数をラップする *)
let test_identity_direct env =
  let clock = Eio.Stdenv.clock env in
  let expected = "hello eio" in
  let actual = identity_direct "hello eio" ~clock in
  Alcotest.(check string_testable) "should return the same string" expected actual

let test_add_direct env =
  let clock = Eio.Stdenv.clock env in
  let result = add_direct 3 5 ~clock in
  Alcotest.(check int_testable) "should return sum of two numbers" 8 result

(* 2. 例外を発生させる関数のテスト *)
let test_might_fail_direct_success env =
  let clock = Eio.Stdenv.clock env in
  let result = might_fail_direct true ~clock in
  Alcotest.(check string_testable) "should succeed" "Success" result

let test_might_fail_direct_failure env =
  let clock = Eio.Stdenv.clock env in
  Alcotest.check_raises
    "should fail with Failure exception"
    (Failure "Operation failed as intended")
    (fun () -> might_fail_direct false ~clock) (* 通常のAlcotest.check_raisesでOK *)

(* 3. 複数のファイバーを扱うテスト (Fiber.List.map のようなもの) *)
(* このテストケースでは sw も使う *)
let test_process_list_concurrently env sw =
  let clock = Eio.Stdenv.clock env in
  let input = [1; 2; 3] in
  let expected = [2; 4; 6] in
  (* process_list_concurrently が sw を必要とする場合 *)
  let actual = process_list_concurrently input ~sw ~clock in
  Alcotest.(check int_list_testable) "should double each element concurrently" expected actual;
  (* もし process_list_concurrently が内部でSwitch.runするなら、sw引数は不要 *)
  () (* テスト関数は unit を返す *)


let test_process_empty_list_concurrently env sw =
  let clock = Eio.Stdenv.clock env in
  let input = [] in
  let expected = [] in
  let actual = process_list_concurrently input ~sw ~clock in
  Alcotest.(check int_list_testable) "should handle empty list concurrently" expected actual;
  ()

(* 4. Eio.Flow を使った関数のテスト *)
let test_read_string_lines_count _env = (* envは使わない場合もある *)
  let content = "line1\nline2\nline3" in
  let expected = 3 in
  let actual = read_string_lines_count content in
  Alcotest.(check int_testable) "should count lines correctly" expected actual

let test_read_empty_string_lines_count _env =
  let content = "" in
  let expected = 0 in
  let actual = read_string_lines_count content in
  Alcotest.(check int_testable) "should count 0 for empty string" expected actual

let test_read_string_no_newline_lines_count _env =
  let content = "single line no newline" in
  let expected = 1 in
  let actual = read_string_lines_count content in
  Alcotest.(check int_testable) "should count 1 for single line no newline" expected actual

(* テストスイートの定義 *)
(* Alcotest_eio. కేసు は (string * speed_level * (Eio_main.yoki -> unit)) を期待する *)
(* yoki は (env * Switch.t) のようなタプル *)
(* 各テスト関数が env や sw を必要に応じて受け取るようにする *)
let suite =
  [
    Alcotest_eio. కేసు "Identity Direct" `Quick (fun env _sw -> test_identity_direct env);
    Alcotest_eio. కేసు "Add Direct" `Quick (fun env _sw -> test_add_direct env);
    Alcotest_eio. కేసు "Might Fail Direct (Success)" `Quick (fun env _sw -> test_might_fail_direct_success env);
    Alcotest_eio. కేసు "Might Fail Direct (Failure)" `Quick (fun env _sw -> test_might_fail_direct_failure env);
    Alcotest_eio. కేసు "Process List Concurrently" `Quick test_process_list_concurrently; (* env と sw を両方使う *)
    Alcotest_eio. కేసు "Process Empty List Concurrently" `Quick test_process_empty_list_concurrently;
    Alcotest_eio. కేసు "Read String Lines Count (basic)" `Quick (fun env _sw -> test_read_string_lines_count env);
    Alcotest_eio. కేసు "Read String Lines Count (empty)" `Quick (fun env _sw -> test_read_empty_string_lines_count env);
    Alcotest_eio. కేసు "Read String Lines Count (no newline)" `Quick (fun env _sw -> test_read_string_no_newline_lines_count env);
  ]

(* テストの実行 *)
let () =
  (* Alcotest_eio.run は内部で Eio_main.run を呼び出し、各テストに環境とスイッチを提供する *)
  Alcotest_eio.run "My Eio Project Tests" [("My_eio_logic", suite)]

重要な変更点と解説:

  • Alcotest_eio.run: Lwtのテストで Lwt_main.run (Alcotest_lwt.run ...) としたのと同様に、Eioのテストでは Alcotest_eio.run を使います。これは内部で Eio_main.run を呼び出し、各テストケースにEio環境 (env) とトップレベルのスイッチ (sw) を提供します。
  • テストケース関数のシグネチャ: Alcotest_eio. కేసు (Alcotest_eio.test_case のエイリアスかもしれません。ドキュメント確認要) で定義されるテスト関数は、Eio_main.yoki -> unit という形になります。yoki は実際には (env * Switch.t) のようなタプルか、あるいは env -> Switch.t -> unit のように個別の引数として提供されることがあります (Alcotest_eio のバージョンや設計によります)。 上記の例では、fun env sw -> ...fun env _sw -> ... のように、必要な引数だけを受け取るようにラップしています。
  • 環境 (env) とスイッチ (sw) の利用: テスト対象のEio関数が env (例えば clockfs のため) や sw (ファイバーをフォークするため) を必要とする場合、テストケース関数に渡されるこれらの引数を利用します。
  • Alcotest.check_raises: Eioでは例外は通常の例外なので、Alcotest.check_raises がそのまま使えます。Lwtの Alcotest_lwt.check_raises のような特別なものは不要です。
  • モックと環境: Eioの設計は、テスト時にモック環境を注入することを容易にします。Eio_main.run (および Alcotest_eio.run) はデフォルトのOS環境(例: eio_posix)を使いますが、テストでは特定の機能をモックしたカスタム環境を作成し、それをテスト対象の関数に渡すことが可能です。 例えば、ファイルシステムアクセスのテストで、実際のディスクに書き込む代わりにインメモリのファイルシステムモックを使うことができます。
    (* モック環境の例(非常に簡略化) *)
    let mock_fs = object
      inherit Eio.Fs.dir Eio.Path.empty (* ダミーの実装 *)
      method open_in ~sw path =
        if Eio.Path.native_concat path = "test.txt" then
          Eio.Flow.string_source "mocked file content"
        else
          raise (Err.NOENT Eio.Path.(Fs (Eio.Path.fs_dir path, Eio.Path.basename_exn path)))
      (* 他のメソッドも必要に応じて実装 *)
    end
    
    let test_with_mock_fs env =
      (* カスタム環境の一部を差し替える *)
      let mock_env = object
        inherit Eio.Stdenv.base env (* 他は既存のenvを引き継ぐ *)
        method fs = mock_fs
      end in
      (* これをテスト対象関数に渡す: my_function_using_fs mock_env *)
      ()
    
    (* Alcotest_eio が提供するデフォルトの env ではなく、
       自分で作成した mock_env を使ってテストを実行する仕組みが必要になるかもしれません。
       Alcotest_eio がそのような機能を提供しているか、
       あるいはテスト対象関数が env を引数で取るようにして、
       テストコード側で mock_env を渡す形になります。 *)
    Alcotest_eio がどのようにカスタム環境の注入をサポートしているかは、そのドキュメントで確認するのが最善です。最も単純なのは、テスト対象関数が env を引数として取り、テストコード側でモック環境を構築して渡す方法です。

3. テストの実行

Lwtの場合と同様です。

dune runtest

または

dune test

Lwtテストとの比較まとめ

  • メインランナー: Lwt_main.run (Alcotest_lwt.run ...) vs Alcotest_eio.run ...
  • テストケースの型: unit -> unit Lwt.t vs Eio_main.yoki -> unit (または env -> sw -> unit)
  • 非同期処理の結果: let* res = promise in ... vs let res = direct_style_call () in ... (Eioでは結果は直接返る)
  • 例外テスト: Alcotest_lwt.check_raises vs Alcotest.check_raises
  • 環境の扱い: Lwtでは暗黙的/グローバルなことが多い vs Eioでは明示的な env の引き回し(テストでのモックが容易)
  • 並行処理のスコープ: Lwtでは手動管理が主 vs Eioでは Switch による構造化された管理

Eioのテストは、その直接的スタイルと構造化並行性の恩恵を受け、Lwtのテストよりもセットアップや記述がシンプルになる傾向があります。特に、リソース管理やファイバーのライフサイクルが Switch によって自動的に扱われるため、テストのクリーンアップに関する定型コードが減ることが期待できます。

(返答終了理由: STOP)


ユーザー: 今回の全体のトピックを簡単に箇条書きしてください


AI: はい、承知いたしました。今回のチュートリアル全体のトピックを簡単に箇条書きでまとめます。

OCaml Lwt チュートリアル

  • Lwtとは何か、なぜ必要か?
    • ノンブロッキングI/Oと協調的マルチタスキングの実現。
    • ブロッキングI/Oの問題点(リソース浪費、応答性低下)の解決。
  • セットアップと最初のLwtプログラム
    • opam でのインストール (lwt, lwt_ppx)。
    • dune ファイルでの設定。
    • Lwt.returnLwt_main.run
  • Lwtのコアコンセプト: Promiseとlet*構文
    • 'a Lwt.t (Promise) の概念。
    • Lwt.bind (>>=) とその糖衣構文 let*
  • エラーハンドリング
    • Lwt.failLwt.catch
    • try%lwt ... with ... 構文。
  • Lwtによる並行処理
    • 逐次実行と並行実行の比較 (Lwt_unix.sleep)。
    • Lwt.join, Lwt.all, Lwt.both, Lwt.pick
  • Lwtフレンドリーなライブラリの活用
    • Lwt_unix (UnixシステムコールのLwt版)。
    • Lwt_io (高レベルなI/Oチャネル操作)。
  • 実践的な例: 非同期ファイル処理 (実際はHTTP GET)
    • cohttp-lwt-unix を使った複数URLからのコンテンツ取得。
  • Lwtを使う上での注意点とベストプラクティス
    • LwtスレッドとOSスレッドの違い。
    • ブロッキング呼び出しの回避。
    • Promiseリークへの注意、Lwt.async
  • Lwtのテストコード
    • AlcotestAlcotest_lwt の利用。
    • テストケースの型 (unit -> unit Lwt.t)。
    • Alcotest_lwt.check_raises
    • Lwt_main.run (Alcotest_lwt.run ...)
    • モックの考え方。

Eioへの橋渡し

  • Eioとは何か、なぜLwtからの移行が考えられるか?
    • Lwtの課題(コールバック地獄の残り香、協調的マルチタスキングの限界など)。
    • Eioのアプローチ(構造化並行性、直接的スタイル、エフェクトハンドラ)。
  • LwtからEioへの主な概念の対比
    • Promise vs 直接的スタイル、let* vs 通常の逐次実行、Switch など。
  • Eioのセットアップと最初のプログラム
    • インストールとdune設定。
    • Eio_main.run と環境 env
  • Eioのコアコンセプト
    • 環境 (env)、フロー (Flow)、ファイバー (Fiber)、スイッチ (Switch)。
    • エラーハンドリング(標準の try ... with ...)。
  • Lwt資産との連携: Lwt_eio
    • LwtのPromiseとEioのファイバーの相互変換。
  • Eioの利点と考慮点
  • Eioのテストコード
    • AlcotestAlcotest_eio の利用。
    • テストケースの型 (Eio_main.yoki -> unit)。
    • Alcotest_eio.run
    • 環境 envSwitch のテストでの扱い。
    • モックと環境の注入。

(返答終了理由: STOP)

メタデータ

{
  "runSettings": {
    "temperature": 1,
    "model": "models/gemini-2.5-pro-preview-05-06",
    "topP": 0.95,
    "topK": 64,
    "maxOutputTokens": 65536,
    "safetySettings": [
      {
        "category": "HARM_CATEGORY_HARASSMENT",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_HATE_SPEECH",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
        "threshold": "OFF"
      }
    ],
    "responseMimeType": "text/plain",
    "enableCodeExecution": false,
    "enableSearchAsATool": false,
    "enableBrowseAsATool": false,
    "enableAutoFunctionResponse": false
  },
  "systemInstruction": {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment