It’s not a matter of narrowing types, it’s a matter of the way effects are typed in Koka vs OCaml. This is the only meaningful difference in the two examples, as the rest is just translation.
If you trace down the type variable a
in the Koka example, you’ll see that at all times it’s available everywhere thanks to the effect type… or in approximate OCaml 6? syntax:
effect 'a yield = Yield : 'a -> unit
val traverse : 'a list -['a yield]-> unit
whereas OCaml’s way of typing effects (using GADTs and extensible variants and only typing the declaration) disallows such connection between the type constructor and the data constructor to be made, and disallows a similar connection to be made on the traverse
arrow… in other words the a
is existential and functions can’t say much about the types of the effects they perform:
type 'ret Effect.t += Yield : 'a -> unit Effect.t
(* notice the difference in type variables 'ret and 'a *)
val traverse : 'a list -> unit
(* and notice the lack of a type equality between list element and the effect *)
…
Writing a handler for the Koka version is simple; the type of elements in the list is the same as the type of elements in the effect (we know that because traverse
tells us that), and a specialized handler is even possible and can be type-checked because we have enough type info…
val handler : (unit -['a yield]-> 'b) -> unit -> 'b
(fun () -> traverse [1; 2; 3]) : unit -[int yield]-> unit
A handler for the OCaml version cannot assert such equalities, not with traverse
written as it is.
Indeed, in OCaml you could do the following:
let rec traverse : int list -> unit = function
| [] -> ()
| x :: xs -> perform (Yield "I am not an int"); traverse xs
To handle an existential, you can’t make any assumptions about it except what it gives you to work with; an unconstrained existential is more-or-less useless. It’s not possible to “narrow” it as you’d expect.
…
To cope with such difference, you need to at the very least pass the function which touches 'a
as another field, you may choose to use printf
as the field, but the more specific you are, the less useful the dynamic nature of effect handlers becomes (that you can choose different behaviors for the same effect). I chose an ad-hoc constraint that there also exists a function capable of taking the existential variable and returning a string:
type _ Effect.t += Yield : { value : 'a; show : 'a -> string } -> unit Effect.t
let rec traverse : ('a -> string) -> 'a list -> unit
= ...
let main () =
...
| Yield {value; show} -> Some
(fun (k : ...) -> ... (show value); continue k ())
that way, the handler can use the provided show
field to convert the existential to a type it knows, and make use of it.
This is the minimal change, something more robust would be what @SkySkimmer beat me to writing, because it brings back the ability to talk about a
outside of the effect declaration (at that cost you’re seeing :P)