Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write a PPX rewriter generating lenses for records?

I am writing a PPX rewriter to ease the definition of Lenses. Let me recall for the casual reader what lenses are.

About lenses

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.

Generating lenses

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:

  1. a predicate is_record : Parsetree.structure_item -> bool recognising type record definitions.

  2. 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.

  3. 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.

like image 286
Michaël Le Barbier Avatar asked Sep 28 '22 00:09

Michaël Le Barbier


1 Answers

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.

like image 101
camlspotter Avatar answered Nov 08 '22 05:11

camlspotter