I was flummoxed in using ppx’s non-trivally for a while as well, until I had the same revelation, that ppx-emitted code was just looking to refer to definitions with names that followed implicit conventions, as available in the lexical scope where the ppx is used.
Where this is is very handy is that you can trivially customize behaviour in different contexts. e.g. say you have a Foo
that is used widely in your program, and you use yojson converters for basic IPC:
module Foo = struct
type t = ...... [@@deriving yojson]
end
Fine, very convenient. But when you want to present Foo
s to e.g. users of an external API, the naive implementation won’t be so great:
module API = struct
(** The type of response provided for [Foo] GET requests *)
type foo_get =
{ id : Uuidm.t
; name : string
; foo : Foo.t
}
[@@deriving yojson]
end
With this, API consumers will be exposed to yojson’s rendering of whatever Foo.t
is, which is surely not something they’ll be expecting, and which will probably leak all kinds of internal changes over time, thus churning that external API’s compatibility. And, we don’t want to change Foo.to_yojson
, as that will impact our IPC usage.
The solution is to just make an API-local Foo
, and implement whatever encoding of it makes sense in that context:
module API = struct
module Foo = struct
include Foo
let to_yojson foo = ....
end
(** The type of response provided for [Foo] GET requests *)
type foo_get =
{ id : Uuidm.t
; name : string
; foo : Foo.t
}
[@@deriving yojson]
end
Now my API-local Foo
module with its own purpose-built to_yojson
is what is lexically in scope for the code in the derived foo_get_to_yojson
function, and my internal IPC usage of the default Foo.to_yojson
is undisturbed.
(BTW, yojson does support using an annotation for customizing the serializer used for just one field, e.g.
module API = struct
let api_foo_to_yojson foo = .......
(** The type of response provided for [Foo] GET requests *)
type foo_get =
{ id : Uuidm.t
; name : string
; foo : Foo.t [@to_yojson api_foo_to_yojson]
}
[@@deriving yojson]
end
This is great if you really only need said customization in a single location, but quickly becomes tedious otherwise, so I find myself defining local copies of modules much more frequently.)