我在做什么:我正在编写一个小型解释器系统,它可以解析文件,将其转换为一系列操作,然后将数千个数据集输入该序列以从中提取一些最终值。编译的解释器由一个带有两个参数的纯函数列表组成:一个数据集和一个执行上下文。每个函数都返回修改后的执行上下文:
type ('data, 'context) interpreter = ('data -> 'context -> 'context) list
编译器本质上是一个标记器,具有最终标记到指令的映射步骤,该步骤使用如下定义的映射描述:
type ('data, 'context) map = (string * ('data -> 'context -> 'context)) list
典型的解释器用法如下所示:
let pocket_calc =
let map = [ "add", (fun d c -> c # add d) ;
"sub", (fun d c -> c # sub d) ;
"mul", (fun d c -> c # mul d) ]
in
Interpreter.parse map "path/to/file.txt"
let new_context = Interpreter.run pocket_calc data old_context
问题:我希望我的pocket_calc
解释器可以使用任何支持add
、方法sub
和mul
相应data
类型的类(一个上下文类可以是整数,另一个可以是浮点数)。
然而,pocket_calc
被定义为一个值而不是一个函数,因此类型系统不会使其类型成为泛型:第一次使用它时,'data
and'context
类型被绑定到我首先提供的任何数据和上下文的类型,解释器变成永远与任何其他数据和上下文类型不兼容。
一个可行的解决方案是对解释器的定义进行 eta-expand 以允许其类型参数是通用的:
let pocket_calc data context =
let map = [ "add", (fun d c -> c # add d) ;
"sub", (fun d c -> c # sub d) ;
"mul", (fun d c -> c # mul d) ]
in
let interpreter = Interpreter.parse map "path/to/file.txt" in
Interpreter.run interpreter data context
但是,由于以下几个原因,此解决方案是不可接受的:
每次调用它都会重新编译解释器,这会显着降低性能。即使是映射步骤(使用映射列表将令牌列表转换为解释器)也会导致明显的减速。
我的设计依赖于在初始化时加载的所有解释器,因为只要加载文件中的标记与映射列表中的行不匹配,编译器就会发出警告,我希望在软件启动时看到所有这些警告(而不是单独解释器最终运行)。
我有时想在几个解释器中重用给定的映射列表,无论是单独使用还是通过附加指令(例如,
"div"
)。
问题:除了eta-expansion,还有什么方法可以使类型参数化?也许是一些涉及模块签名或继承的巧妙技巧?如果这是不可能的,有没有办法缓解我上面提到的三个问题,以使 eta-expansion 成为可接受的解决方案?谢谢!