Skip to content

Instantly share code, notes, and snippets.

@shonfeder
Last active August 16, 2025 18:16
Show Gist options
  • Save shonfeder/a87d7d92626be06d17d2e795c6481a1e to your computer and use it in GitHub Desktop.
Save shonfeder/a87d7d92626be06d17d2e795c6481a1e to your computer and use it in GitHub Desktop.
"Dependency injection" using OCaml 5's effect system: example for https://news.ycombinator.com/item?id=44896063
module EffEx = struct
(** Register our new effects *)
type _ Effect.t +=
| Print_endline : string -> unit Effect.t
| Read_line : string Effect.t
(** helpers to perform the effects *)
let println s = Effect.perform (Print_endline s)
let readln () = Effect.perform Read_line
(** A computation is just a function from the unit type to some derived
result*)
type 'result computation = unit -> 'result
(** An effect handler that handles our effects with normal I/0 operations *)
let io_handler (progn : 'a computation): 'a computation =
fun () ->
match progn () with
| result ->
(* No effects to handle *)
result
| effect Print_endline s, k ->
(* Actually print the string *)
Printf.printf "%s\n" s;
(* Continue the computation *)
Effect.Deep.continue k ()
| effect Read_line, k ->
(* Actually read a line *)
let s = read_line () in
(* Continue the computation with the string read *)
Effect.Deep.continue k s
(** An effect handler that just handles prints, by instead gathering them into a list *)
let mocked_print_handler (progn : 'a computation) : (string list * 'a) computation =
fun () ->
match progn () with
| result ->
(* No outputs left to gather once we have computed the result *)
([], result)
| effect Print_endline s, k ->
(* Keep running the computation to gather all the outputs *)
let outputs, result = Effect.Deep.continue k () in
(* Add the intended output to our collection *)
s :: outputs, result
(** An effect handler that just handles reads, by instead taking them from a supplied list *)
let mocked_read_handler (inputs : string list) (progn : 'a computation): 'a computation =
fun () ->
let input_q = ref inputs in
match progn () with
| result -> result
| effect Read_line, k ->
match !input_q with
| [] -> invalid_arg "Not enough inputs"
| s :: inputs' ->
input_q := inputs';
Effect.Deep.continue k s
(** Compose a mock for both reads and prints *)
let mocked_handler inputs (progn : unit -> 'a) : (string list * 'a) computation =
mocked_print_handler
@@ mocked_read_handler ["first input"; "second input"]
@@ progn
end
(** A normal, direct-style program phat performs our new effects *)
let program () =
let open EffEx in
println "foo";
println (readln ());
let s = readln () in
println "read something";
println "done";
s
let () =
Printf.printf ">> Run the program with normal io operation and interactive input:\n";
EffEx.io_handler program ()
|> Printf.printf "result read: %s\n\n"
let () =
Printf.printf ">> Run the program with mocked operations:\n";
let outputs, result = EffEx.mocked_handler ["first input"; "second input"] program () in
assert (outputs = ["foo"; "first input" ;"read something"; "done"]);
assert (result = "second input");
Printf.printf "Tests passed\n\n"
let () =
Printf.printf ">> Run the program with mocked input but normal output:\n";
let program' =
EffEx.io_handler
@@ EffEx.mocked_read_handler ["first input"; "second input"]
@@ program
in
Printf.printf "result read: %s\n\n" (program' ())
@shonfeder
Copy link
Author

Here an example of running the program:

$ ocaml effex.ml 
>> Run the program with normal io operation and interactive input:
foo
Interactive input 1
Interactive input 1
Interactive input 2
read something
done
result read: Interactive input 2

>> Run the program with mocked operations:
Tests passed

>> Run the program with mocked input but normal output:
foo
first input
read something
done
result read: second input

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment