如何编写 PPX 重写器生成记录镜头?

Mic*_*ald 5 ocaml metaprogramming

我正在编写 PPX 重写器以简化Lenses的定义。让我为普通读者回忆一下什么是镜头

关于镜头

与记录字段相关联的镜头是一对函数,允许提取记录并更新它。下面是一个例子:

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 })
}
Run Code Online (Sandbox Code Playgroud)

vendor_lens让我们获得该领域的价值vendor在我们car和更新它-这意味着返回的全新副本car只能由值从原来的不同的vendor车。乍一看这听起来很平庸,但事实并非如此:由于镜头本质上是功能,因此它们可以组合,并且镜头模块充满了有用的功能。组合访问器的能力在复杂的代码库中至关重要,因为它通过抽象从计算上下文到深度嵌套记录的路径来简化解耦。我最近还重构了我的Getopts配置文件解析器以采用功能性接口,这使镜头更加相关——至少对我而言。

生成镜头

vendor_lens上面的定义只不过是样板代码,真的没有理由不能利用 PPX 重写器让我们简单地编写

type car = {
  vendor: string;
  make: string;
  mileage: int;
} [@@with_lenses]
Run Code Online (Sandbox Code Playgroud)

并自动查看我们需要在汽车上使用的镜头的定义。¹

我决定解决这个问题并且可以产生:

  1. is_record : Parsetree.structure_item -> bool识别类型记录定义的谓词。

  2. 一个函数label_declarations : Parsetree.structure_item -> string list可能会返回记录定义的记录声明列表——是的,我们可以使用 option 将 1 和 2 粉碎在一起

  3. lens_expr : string -> Parsetree.structure_item为给定的字段声明生成镜头定义的函数。不幸的是,我在编写此函数后发现了 Alain Frisch 的ppx_metaquot

在我看来,这里有我想编写的 PPX 重写器的基本部分。不过,我怎样才能将它们组合在一起?


¹ 在寻找镜头的 PPX 重写器时,我偶然发现了不少于五个涉及相同car结构的博客或自述文件。在这里回收这个例子是一种卑鄙的尝试,它看起来像一个配备镜头的汽车司机选择性俱乐部的全职成员。

cam*_*ter 4

PPX 项目的最终目标是构建类型为 的映射器Ast_mapper.mapper

mapper是一个大记录类型,并带有Parsetree数据类型的映射器函数,例如,

type mapper = { 
  ...
  structure : mapper -> structure -> structure;
  signature : mapper -> signature -> signature;
  ...
}
Run Code Online (Sandbox Code Playgroud)

有一个默认映射器Ast_mapper.default_mapper,这是映射器的起点:您可以继承它并覆盖一些记录成员以供您使用。对于您的镜头项目,您必须实施structure并且signature

let extend super =
  let structure self str = ... in
  let signature self str = ... in
  { super with structure; signature }

let mapper = extend default_mapper
Run Code Online (Sandbox Code Playgroud)

您的函数structure应该扫描结构项并为每个记录类型声明添加适当的值定义。signature应该做同样的事情,但添加镜头功能的签名:

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
Run Code Online (Sandbox Code Playgroud)

superselfOO 的相同:super是你正在扩展的原始映射器,self是扩展的结果。(实际上,第一个版本Ast_mapper使用了类而不是记录类型。如果您更喜欢 OO 风格,您可以使用Ast_mapper_classppx_tools 包,它在 OO 接口中提供相同的功能。)无论如何..我想在您的情况下,有不需要使用selfsuper争论。

完成自己的映射器后,让它Ast_mapper.apply根据输入运行映射器:

let () =
  let infile = .. in
  let outfile = .. in
  Ast_mapper.apply ~source:infile ~target:outfile mapper
Run Code Online (Sandbox Code Playgroud)

或多或少,所有 PPX 重写器的实现都与上面类似。检查几个小型 PPX 实现肯定有助于您的理解。

  • 我们在 ppx 上举办了纽约 OCaml 聚会。虽然不多,但审查的代码在[此处](https://github.com/agarwal/ppx_basic_demo)。 (2认同)