如何编写PPX 重写器生成记录镜头?
How to write a PPX rewriter generating lenses for records?
我正在编写一个 PPX 重写器来简化 Lenses 的定义。让我随便回忆一下reader什么是镜头。
关于镜头
与记录的字段关联的lens 是一对允许提取记录并更新记录的函数。这是一个例子:
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 })
}
vendor_lens
允许我们在 car
中获取字段 vendor
的值并更新它——这意味着返回 car
的新副本与原版的区别仅在于 vendor
汽车的价值。这乍听起来可能很 平庸 但事实并非如此:由于镜头本质上是功能,因此可以组合它们并且 Lenses 模块充满了有用的功能。组合访问器的能力在复杂的代码库中至关重要,因为它通过抽象从计算上下文到深层嵌套记录的路径来简化解耦。我最近还重构了我的 Getopts 和 配置文件解析器 以采用功能接口,这使得镜头更加相关 - 至少对我而言。
生成镜头
上面vendor_lens
的定义无非是boilerplate code,实在没理由不利用PPX-rewriters让我们简单写成
type car = {
vendor: string;
make: string;
mileage: int;
} [@@with_lenses]
并自动查看我们需要在汽车上使用的镜头的定义。¹
我决定解决这个问题并可能产生:
谓词 is_record : Parsetree.structure_item -> bool
识别类型记录定义。
一个函数 label_declarations : Parsetree.structure_item -> string list
可能会返回记录定义的记录声明列表 – 是的,我们可以使用一个选项 [=63= 将 1 和 2 粉碎在一起].
函数 lens_expr : string -> Parsetree.structure_item
为给定的字段声明生成镜头定义。不幸的是,我在编写此函数后发现了 Alain Frisch ppx_metaquot。
在我看来,我想写的 PPX-rewriter 的主要部分都在这里了。不过,如何将它们组合在一起?
¹ 在搜索镜头的 PPX-rewriter 时,我偶然发现不少于五个涉及相同 car
结构的博客或自述文件。在这里重复这个例子是一种卑鄙的尝试,看起来像 lens-equipped 汽车司机精英俱乐部的 full-time 成员。
您的 PPX 项目的最终目标是构建类型为 Ast_mapper.mapper
的映射器。
mapper
是一个大的记录类型,并带有 Parsetree
数据类型的映射函数,例如,
type mapper = {
...
structure : mapper -> structure -> structure;
signature : mapper -> signature -> signature;
...
}
有一个默认映射器 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
您的函数 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
super
和self
和OO一样:super
是你扩展的原始映射器,self
是扩展的结果。 (实际上 Ast_mapper
的第一个版本使用了 class 而不是记录类型。如果你喜欢 OO 风格,你可以使用 ppx_tools 包的 Ast_mapper_class
,它提供在 OO 接口中相同。)无论如何..我想在你的情况下,没有必要使用 self
或 super
参数。
完成自己的映射器后,将其 Ast_mapper.apply
交给 运行 映射器以输入:
let () =
let infile = .. in
let outfile = .. in
Ast_mapper.apply ~source:infile ~target:outfile mapper
差不多,所有的PPX rewriter的实现都和上面一样。检查几个小的 PPX 实现肯定有助于您的理解。
我正在编写一个 PPX 重写器来简化 Lenses 的定义。让我随便回忆一下reader什么是镜头。
关于镜头
与记录的字段关联的lens 是一对允许提取记录并更新记录的函数。这是一个例子:
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 })
}
vendor_lens
允许我们在 car
中获取字段 vendor
的值并更新它——这意味着返回 car
的新副本与原版的区别仅在于 vendor
汽车的价值。这乍听起来可能很 平庸 但事实并非如此:由于镜头本质上是功能,因此可以组合它们并且 Lenses 模块充满了有用的功能。组合访问器的能力在复杂的代码库中至关重要,因为它通过抽象从计算上下文到深层嵌套记录的路径来简化解耦。我最近还重构了我的 Getopts 和 配置文件解析器 以采用功能接口,这使得镜头更加相关 - 至少对我而言。
生成镜头
上面vendor_lens
的定义无非是boilerplate code,实在没理由不利用PPX-rewriters让我们简单写成
type car = {
vendor: string;
make: string;
mileage: int;
} [@@with_lenses]
并自动查看我们需要在汽车上使用的镜头的定义。¹
我决定解决这个问题并可能产生:
谓词
is_record : Parsetree.structure_item -> bool
识别类型记录定义。一个函数
label_declarations : Parsetree.structure_item -> string list
可能会返回记录定义的记录声明列表 – 是的,我们可以使用一个选项 [=63= 将 1 和 2 粉碎在一起].函数
lens_expr : string -> Parsetree.structure_item
为给定的字段声明生成镜头定义。不幸的是,我在编写此函数后发现了 Alain Frisch ppx_metaquot。
在我看来,我想写的 PPX-rewriter 的主要部分都在这里了。不过,如何将它们组合在一起?
¹ 在搜索镜头的 PPX-rewriter 时,我偶然发现不少于五个涉及相同 car
结构的博客或自述文件。在这里重复这个例子是一种卑鄙的尝试,看起来像 lens-equipped 汽车司机精英俱乐部的 full-time 成员。
您的 PPX 项目的最终目标是构建类型为 Ast_mapper.mapper
的映射器。
mapper
是一个大的记录类型,并带有 Parsetree
数据类型的映射函数,例如,
type mapper = {
...
structure : mapper -> structure -> structure;
signature : mapper -> signature -> signature;
...
}
有一个默认映射器 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
您的函数 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
super
和self
和OO一样:super
是你扩展的原始映射器,self
是扩展的结果。 (实际上 Ast_mapper
的第一个版本使用了 class 而不是记录类型。如果你喜欢 OO 风格,你可以使用 ppx_tools 包的 Ast_mapper_class
,它提供在 OO 接口中相同。)无论如何..我想在你的情况下,没有必要使用 self
或 super
参数。
完成自己的映射器后,将其 Ast_mapper.apply
交给 运行 映射器以输入:
let () =
let infile = .. in
let outfile = .. in
Ast_mapper.apply ~source:infile ~target:outfile mapper
差不多,所有的PPX rewriter的实现都和上面一样。检查几个小的 PPX 实现肯定有助于您的理解。