Hmm, yeah this is weird.
tl;dr: It’s because of the generated ppx code that the type of juxtaposed
seems to magically change underneath you.
(This is a bit rambling, but it will get there eventually…)
Check it out, the type signature for Let_syntax.sub
says this:
val sub :
?here:Core.Source_code_position.t ->
'a Computation.t ->
f:('a Value.t -> 'b Computation.t) ->
'b Computation.t
which means I would expect to treat juxtaposed
as a string Value.t
in this line:
let%sub (juxtaposed : string Value.t) = juxtapose_digits ~delimiter:" + " a b in
but as I’m sure you noticed, if you put that type in there, you get a compile error something like this:
This pattern matches values of type string Value.t
but a pattern was expected which matches values of type string
Huh? Check these two things out right here:
let _juxtapose_and_sum (a : int Value.t) (b : int Value.t) :
string Computation.t =
let%sub (juxtaposed : string) = juxtapose_digits ~delimiter:" + " a b in
(* This line gives an error...
This expression (juxtaposed) has type string Value.t
but an expression was expected of type string *)
let _s = juxtaposed ^ " yoooo!" in
failwith "..."
That’s weird, because earlier, when we try to annotate juxtaposed
as a string Value.t
it doesn’t compile saying that pattern matches a string
, now in the next line, when we try to use juxtaposed
as a string, compiler is saying that juxtaposed
is in fact NOT a string
at all, rather a string Value.t
.
Okay, something clearly weird here. Just a sanity check, use the sub
function directly and see it works as we expect.
let _juxtapose_and_sum' (a : int Value.t) (b : int Value.t) :
string Computation.t =
Let_syntax.sub (juxtapose_digits ~delimiter:" + " a b)
~f:(fun (juxtaposed : string Value.t) ->
let (y : string Value.t) = (juxtaposed : string Value.t) in
return y )
Yeah compiles fine and all the types look like they should. Okay so what is going on with this weirdness?
First of all, let’s simply the function in question to this:
let foo (a : int Value.t) (b : int Value.t) : string Computation.t =
let%sub (juxtaposed : string) = juxtapose_digits ~delimiter:" + " a b in
return (juxtaposed : string Value.t)
It has the values annotated so you can see them. Even though the compiler says this is correct, as you can see it doesn’t make any sense with what we know about sub
. So let’s check the generated ppx code and see what’s going on. (Assume i have this in a file called yo.ml
.)
$ ocamlc -stop-after parsing -dsource _build/default/yo.pp.ml 2> yo_gen.ml
And you’ll get a function that looks something like this:
let foo (a : int Value.t) (b : int Value.t) : string Computation.t =
Let_syntax.sub
~here:
{ Ppx_here_lib.pos_fname= "yo.ml"
; pos_lnum= 24
; pos_cnum= 660
; pos_bol= 658 } (juxtapose_digits ~delimiter:" + " a b)
~f:(fun __pattern_syntax__008_ ->
Let_syntax.sub
~here:
{ Ppx_here_lib.pos_fname= "yo.ml"
; pos_lnum= 24
; pos_cnum= 660
; pos_bol= 658 }
(Let_syntax.return
(Let_syntax.map __pattern_syntax__008_ ~f:(function
| (__pattern_syntax__009_ : string) -> __pattern_syntax__009_ )
[@merlin.hide] ) )
~f:(fun juxtaposed -> return (juxtaposed : string Value.t)) )
The odd thing there is how it’s doing the rewriting… see at the bottom where it is doing this:
(Let_syntax.return
(Let_syntax.map __pattern_syntax__008_ ~f:(function
| (__pattern_syntax__009_ : string) -> __pattern_syntax__009_ )
[@merlin.hide] ) )
Here are the signatures of map and return in the Let_syntax
module for reference:
val map : 'a Value.t -> f:('a -> 'b) -> 'b Value.t
val return : 'a Value.t -> 'a Computation.t
Let’s unpack it. __pattern_syntax__008_
is bound to the result of (juxtapose_digits ~delimiter:" + " a b)
by the sub
function in ~f
. It’s a string Value.t
because that’s what juxtapose_digits
returns.
Now __pattern_syntax__008_
is passed to map
:
(Let_syntax.map __pattern_syntax__008_ ~f:(function
| (__pattern_syntax__009_ : string) -> __pattern_syntax__009_ )
[@merlin.hide] )
That f
is basically an identity function, and the return value of the expr as a whole is also string Value.t
(because that’s what map
does). BUT check out that (__pattern_syntax__009_ : string)
thing. Well that corresponds to our thing here (because that’s how the let syntax is working)
let%sub (juxtaposed : string) =
^^^^^^^^^^^^^^^^^^^^^
And yeah, in that context, __pattern_syntax__009_
(aka juxtaposed
) is a string there in that pattern, because it is in the map
function and that is what it expects to be there. (which is why you had to annotate is as a string…thats what it is there).
Okay, so that map expression will give you a string Value.t
, and that is then passed to return
which takes it and returns a string Computation.t
. That computation is finally used as an argument to sub
, and in THAT call’s function, juxtaposed
is a string Value.t
, which that is what return
returns, and what sub expects. So here, juxtaposed
is a string Value.t
:
~f:(fun juxtaposed -> return (juxtaposed : string Value.t))
So yeah to answer your question…the type annotations make no sense unless you are looking at the ppx generated code. Now as to why the ppx generates code that happens to have those confusing (seemingly magically changing) types…I don’t have an an answer to that.