使用多态递归模块创建访问者模式
Creating a Visitor Pattern with Polymorphic Recursive Modules
(免责声明:我相当确定这在任何方面都不是惯用语。如果 OCaml 中有一些替代的树遍历惯用语,我洗耳恭听:))
我正在用 OCaml 编写一个玩具编译器,我希望我的大型语法树类型有一个访问者。我使用 类 编写了一个,但我认为尝试使用 modules/functors 实现一个会很有趣。我的类型层次结构很大,所以让我说明一下我正在尝试做什么。
考虑以下类型定义(现场制作):
type expr =
SNum of int
| SVarRef of string
| SAdd of expr * expr
| SDo of stmt list
and stmt =
SIf of expr * expr * expr
| SAssign of string * expr
我简单说明一下用法。比方说,我想收集程序内部的所有 SVarRef
。如果我有一个映射访问者(访问树的每个节点并且默认情况下什么都不做),我可以执行以下操作(在完美的世界中):
module VarCollector : (sig
include AST_VISITOR
val var_refs : (string list) ref
end) = struct
include MapVisitor
let var_refs = ref []
let s_var_ref (s : string) =
var_refs := s::!var_refs
SVarRef(s)
end
(* Where my_prog is a stmt type *)
let refs = begin
let _ = VarCollector.visit_stmt my_prog in
VarCollector.!var_refs
end
我应该注意到,为每个特定变体提供函数的优点是我的实际代码库有一个类型,它既有大量的变体,也没有碎片化的意义。特定于变体的函数允许避免对一个类型的其他变体重复迭代实现。
看起来很简单,但要注意的是:有不同类型的访问者。 MapVisitor
return是原始语法树,所以它的类型是
sig
(** Dispatches to variant implementations *)
val visit_stmt : stmt -> stmt
val visit_expr : expr -> expr
(** Variant implementations *)
val s_num : int -> expr
val s_var_ref : string -> expr
val s_add : (expr * expr) -> expr
val s_do : stmt list -> expr
val s_if : (expr * expr * expr) -> stmt
val s_assign : (string * expr) -> stmt
end
同时,人们可能会想象一个折叠访问者,其中每个函数的 return 类型都是一些 t
。试图尽可能地抽象它,这是我的尝试:
module type AST_DISPATCHER = sig
type expr_ret
type stmt_ret
val visit_expr : expr -> expr_ret
val visit_stmt : stmt -> stmt_ret
end
(** Concrete type designation goes in AST_VISITOR_IMPL *)
module type AST_VISITOR_IMPL = sig
type expr_ret
type stmt_ret
val s_num : int -> expr_ret
val s_var_ref : string -> expr_ret
val s_add : (expr * expr) -> expr_ret
val s_do : stmt list -> expr_ret
val s_if : (expr * expr * expr) -> stmt_ret
val s_assign : (string * expr) -> stmt_ret
end
module type AST_VISITOR = sig
include AST_VISITOR_IMPL
include AST_DISPATCHER with type expr_ret := expr_ret
and type stmt_ret := stmt_ret
end
(** Dispatcher Implementation *)
module AstDispatcherF(IM : AST_VISITOR_IMPL) : AST_DISPATCHER = struct
type expr_ret = IM.expr_ret
type stmt_ret = IM.stmt_ret
let visit_expr = function
| SNum(i) -> IM.s_num i
| SVarRef(s) -> IM.s_var_ref s
| SAdd(l,r) -> IM.s_add (l,r)
| SDo(sl) -> IM.s_do sl
let visit_stmt = function
| SIf(c,t,f) -> IM.s_if (c,t,f)
| SAssign(s,e) -> IM.s_assign (s,e)
end
module rec MapVisitor : AST_VISITOR = struct
type expr_ret = expr
type stmt_ret = stmt
module D : (AST_DISPATCHER with type expr_ret := expr_ret
and type stmt_ret := stmt_ret)
= AstDispatcherF(MapVisitor)
let visit_expr = D.visit_expr
let visit_stmt = D.visit_stmt
let s_num i = SNum i
let s_var_ref s = SVarRef s
let s_add (l,r) = SAdd(D.visit_expr l, D.visit_expr r)
let s_do sl = SDo(List.map D.visit_stmt sl)
let s_if (c,t,f) = SIf(D.visit_expr c, D.visit_expr t, D.visit_expr f)
let s_assign (s,e) = SAssign(s, D.visit_expr e)
end
运行 然而,这给了我以下错误信息:
Error: Signature Mismatch:
Values do not match:
val visit_expr : expr -> expr_ret
is not included in
val visit_expr : expr -> expr_ret
我知道这意味着我没有正确表达类型之间的关系,但我无法弄清楚这种情况下的修复方法。
免责声明:模块只是带有类型定义的值记录。由于您的模块中没有类型,所以根本不需要使用它们,只需使用普通的旧记录类型,您将获得一种惯用的 AST 遍历模式。很快,你会发现你需要一个开放的递归,并且会切换到基于 class 的方法。无论如何,这是将 classes 添加到 OCaml 的主要原因。您还可以将 classes 包装到状态 monad 中,以使用任意用户数据折叠 AST。
关于你的错误信息,那很简单,你用签名隐藏了你的类型,这是一个常见的错误。最简单的解决方案是完全省略函子的 return 类型的注释,或者使用 with type expr = expr
注释传播类型相等性。
如果您需要更多惯用方法的示例,那么对于记录,您可以选择 ppx mappers, here is an example of different visitors implemented with classes, including those 包装到状态 monad 中的记录。
(免责声明:我相当确定这在任何方面都不是惯用语。如果 OCaml 中有一些替代的树遍历惯用语,我洗耳恭听:))
我正在用 OCaml 编写一个玩具编译器,我希望我的大型语法树类型有一个访问者。我使用 类 编写了一个,但我认为尝试使用 modules/functors 实现一个会很有趣。我的类型层次结构很大,所以让我说明一下我正在尝试做什么。
考虑以下类型定义(现场制作):
type expr =
SNum of int
| SVarRef of string
| SAdd of expr * expr
| SDo of stmt list
and stmt =
SIf of expr * expr * expr
| SAssign of string * expr
我简单说明一下用法。比方说,我想收集程序内部的所有 SVarRef
。如果我有一个映射访问者(访问树的每个节点并且默认情况下什么都不做),我可以执行以下操作(在完美的世界中):
module VarCollector : (sig
include AST_VISITOR
val var_refs : (string list) ref
end) = struct
include MapVisitor
let var_refs = ref []
let s_var_ref (s : string) =
var_refs := s::!var_refs
SVarRef(s)
end
(* Where my_prog is a stmt type *)
let refs = begin
let _ = VarCollector.visit_stmt my_prog in
VarCollector.!var_refs
end
我应该注意到,为每个特定变体提供函数的优点是我的实际代码库有一个类型,它既有大量的变体,也没有碎片化的意义。特定于变体的函数允许避免对一个类型的其他变体重复迭代实现。
看起来很简单,但要注意的是:有不同类型的访问者。 MapVisitor
return是原始语法树,所以它的类型是
sig
(** Dispatches to variant implementations *)
val visit_stmt : stmt -> stmt
val visit_expr : expr -> expr
(** Variant implementations *)
val s_num : int -> expr
val s_var_ref : string -> expr
val s_add : (expr * expr) -> expr
val s_do : stmt list -> expr
val s_if : (expr * expr * expr) -> stmt
val s_assign : (string * expr) -> stmt
end
同时,人们可能会想象一个折叠访问者,其中每个函数的 return 类型都是一些 t
。试图尽可能地抽象它,这是我的尝试:
module type AST_DISPATCHER = sig
type expr_ret
type stmt_ret
val visit_expr : expr -> expr_ret
val visit_stmt : stmt -> stmt_ret
end
(** Concrete type designation goes in AST_VISITOR_IMPL *)
module type AST_VISITOR_IMPL = sig
type expr_ret
type stmt_ret
val s_num : int -> expr_ret
val s_var_ref : string -> expr_ret
val s_add : (expr * expr) -> expr_ret
val s_do : stmt list -> expr_ret
val s_if : (expr * expr * expr) -> stmt_ret
val s_assign : (string * expr) -> stmt_ret
end
module type AST_VISITOR = sig
include AST_VISITOR_IMPL
include AST_DISPATCHER with type expr_ret := expr_ret
and type stmt_ret := stmt_ret
end
(** Dispatcher Implementation *)
module AstDispatcherF(IM : AST_VISITOR_IMPL) : AST_DISPATCHER = struct
type expr_ret = IM.expr_ret
type stmt_ret = IM.stmt_ret
let visit_expr = function
| SNum(i) -> IM.s_num i
| SVarRef(s) -> IM.s_var_ref s
| SAdd(l,r) -> IM.s_add (l,r)
| SDo(sl) -> IM.s_do sl
let visit_stmt = function
| SIf(c,t,f) -> IM.s_if (c,t,f)
| SAssign(s,e) -> IM.s_assign (s,e)
end
module rec MapVisitor : AST_VISITOR = struct
type expr_ret = expr
type stmt_ret = stmt
module D : (AST_DISPATCHER with type expr_ret := expr_ret
and type stmt_ret := stmt_ret)
= AstDispatcherF(MapVisitor)
let visit_expr = D.visit_expr
let visit_stmt = D.visit_stmt
let s_num i = SNum i
let s_var_ref s = SVarRef s
let s_add (l,r) = SAdd(D.visit_expr l, D.visit_expr r)
let s_do sl = SDo(List.map D.visit_stmt sl)
let s_if (c,t,f) = SIf(D.visit_expr c, D.visit_expr t, D.visit_expr f)
let s_assign (s,e) = SAssign(s, D.visit_expr e)
end
运行 然而,这给了我以下错误信息:
Error: Signature Mismatch:
Values do not match:
val visit_expr : expr -> expr_ret
is not included in
val visit_expr : expr -> expr_ret
我知道这意味着我没有正确表达类型之间的关系,但我无法弄清楚这种情况下的修复方法。
免责声明:模块只是带有类型定义的值记录。由于您的模块中没有类型,所以根本不需要使用它们,只需使用普通的旧记录类型,您将获得一种惯用的 AST 遍历模式。很快,你会发现你需要一个开放的递归,并且会切换到基于 class 的方法。无论如何,这是将 classes 添加到 OCaml 的主要原因。您还可以将 classes 包装到状态 monad 中,以使用任意用户数据折叠 AST。
关于你的错误信息,那很简单,你用签名隐藏了你的类型,这是一个常见的错误。最简单的解决方案是完全省略函子的 return 类型的注释,或者使用 with type expr = expr
注释传播类型相等性。
如果您需要更多惯用方法的示例,那么对于记录,您可以选择 ppx mappers, here is an example of different visitors implemented with classes, including those 包装到状态 monad 中的记录。