I am writing a PPX rewriter to ease the definition of Lenses. Let me recall for the casual reader what lenses are.
A lens associated with a field of a record is a pair of functions allowing to extract the record and update it. Here is an example:
module Lens =
struct
type ('a, 'b) t = {
get : 'a -> 'b;
set : 'b -> 'a -> 'a
}
end
type car = {
vendor: string;
make: string;
mileage: int;
}
let vendor_lens = {
Lens.get = (fun x -> x.vendor);
Lens.set = (fun v x -> { x with vendor = v })
}
The vendor_lens
allows us to get the value of the field vendor
in our car
and to update it – which means returning a fresh copy of the car
differing from the original only by the value of the vendor
car. This might at first sound very banal but it is not: since lenses are essentially functions, they can be composed and the Lenses module is filled with useful functions. The ability to compose accessors is crucial in complex code bases, as it eases the decoupling by abstracting the path from a computation context to a deeply nested record. I also recently refactored my Getopts and configuration file parser to adopt a functional interface, which makes lenses even more relevant – at least for me.
The definition of vendor_lens
above is nothing more than boilerplate code and there is really no reason one could not take advantage of PPX-rewriters to let us simply write
type car = {
vendor: string;
make: string;
mileage: int;
} [@@with_lenses]
and see automagically definition of the lenses we need to work with our car.¹
I decided to tackle the problem and could produce:
a predicate is_record : Parsetree.structure_item -> bool
recognising type record definitions.
a function label_declarations : Parsetree.structure_item -> string list
maybe returning the list of record declarations for a record definition – yes, we could smash 1 and 2 together using an option.
a function lens_expr : string -> Parsetree.structure_item
generating the lens definition for a given field declaration. Unfortunately I discovered ppx_metaquot by Alain Frisch after I wrote this function.
It seems to me I have here the essential parts of the PPX-rewriter I want to write. Still, how can I combine them together?
¹ While searching for a PPX-rewriter for lenses, I stumbled on not less than five blogs or READMEs involving the very same car
structure. Recycling this example here is a vile attempt to look like a full-time member of the selective club of lens-equipped car drivers.
The final goal of your PPX project is to build a mapper of type Ast_mapper.mapper
.
mapper
is a large record type and carries mapper functions for the Parsetree
data types, for example,
type mapper = {
...
structure : mapper -> structure -> structure;
signature : mapper -> signature -> signature;
...
}
There is a default mapper Ast_mapper.default_mapper
and this is the starting point of your mapper: you can inherit it and override some of record members for your use. For your lens project, you have to implement structure
and signature
:
let extend super =
let structure self str = ... in
let signature self str = ... in
{ super with structure; signature }
let mapper = extend default_mapper
Your function structure
should scan the structure items and add an appropriate value definition for each record type declaration. signature
should do the same thing but add signatures of lens functions:
let structure self str = List.concat (List.map (fun sitem -> match sitem.pstr_desc with
| Pstr_type tds when tds_with_lenses sitem ->
sitem :: sitems_for_your_lens_functions
| _ -> [sitem]) str)
in
let signature self str = List.concat (List.map (fun sgitem -> match sgiitem.psig_desc with
| Psig_type tds when tds_with_lenses sitem ->
sgitem :: sgitems_for_your_lens_functions
| _ -> [sgitem]) str)
in
super
and self
are as same as those of OO: super
is the original mapper your are extending and self
is the result of extension. (Actually for the very first version of Ast_mapper
used a class instead of the record type. If you prefer OO style, you can use Ast_mapper_class
of ppx_tools package, which provides the same in OO interface.) In anyway.. I guess in your case, there is no need to use self
or super
arguments.
Once you finish your own mapper then give it Ast_mapper.apply
to run the mapper against the input:
let () =
let infile = .. in
let outfile = .. in
Ast_mapper.apply ~source:infile ~target:outfile mapper
More or less, all the PPX rewriter implementations are like the above. Checking several small PPX implementations surely helps your understanding.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With