我正在做什么:我正在编写一个小型解释器系统,可以解析文件,将其转换为一系列操作,然后将数千个数据集提供到该序列中以提取每个人的最终价值。编译的解释器由一系列带有两个参数的纯函数组成:数据集和执行上下文。每个函数都返回修改后的执行上下文:
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
和'context
类型绑定到我首先提供的任何数据和上下文的类型,并且解释器永远不会与任何其他数据和上下文类型不兼容。
一个可行的解决方案是eta扩展解释器的定义,以允许其类型参数是通用的:
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成为可接受的解决方案?谢谢!
答案 0 :(得分:4)
一个可行的解决方案是eta扩展 允许的解释器的定义 它的类型参数是通用的:
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
然而,这种解决方案是不可接受的 有几个原因:
- 每次调用时它都会重新编译解释器 显着降低性能。 甚至是映射步骤(转动令牌 使用地图列入解释器 list)导致明显的减速。
它每次重新编译解释器,因为你做错了。正确的形式更像是这样(从技术上讲,如果Interpreter.run
到interpreter
的部分解释可以做一些计算,你也应该将它移出fun
。
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
let interpreter = Interpreter.parse map "path/to/file.txt" in
fun data context -> Interpreter.run interpreter data context
答案 1 :(得分:3)
我认为你的问题在于操作中缺少多态性,你希望它具有封闭的参数类型(适用于支持以下算术原语的所有数据),而不是具有表示固定数据类型的类型参数。 但是,确保它正是这样有点困难,因为你的代码不够独立,无法测试它。
假设基元的给定类型:
type 'a primitives = <
add : 'a -> 'a;
mul : 'a -> 'a;
sub : 'a -> 'a;
>
您可以使用结构和对象提供的一阶多态性:
type op = { op : 'a . 'a -> 'a primitives -> 'a }
let map = [ "add", { op = fun d c -> c # add d } ;
"sub", { op = fun d c -> c # sub d } ;
"mul", { op = fun d c -> c # mul d } ];;
您将获得以下与数据无关的类型:
val map : (string * op) list
编辑:关于您对不同操作类型的评论,我不确定您想要的灵活程度。我不认为你可以在同一个列表中混合不同基元的操作,并且仍然可以从每个基元的特性中受益:最多只能将“基于add / sub / mul的操作”转换为“基于add /的操作” sub / mul / div“(因为我们在基元类型中是逆变的),但肯定不多。
在更实用的层面上,使用该设计确实需要为每种基元类型使用不同的“操作”类型。但是,您可以轻松地构建一个由基元类型参数化并返回操作类型的函子。
我不知道如何公开不同基元类型之间的直接子类型关系。问题是这需要在仿函数级别的子类型关系,我认为我们没有在Caml中。但是,您可以使用更简单的显式子类型(而不是转换a :> b
,使用函数a -> b
),构建第二个仿函数,逆变,给定从原始类型到另一个的映射,将构建从一种操作类型到另一种操作类型的映射。
完全可能的是,通过对演化类型的不同和巧妙表示,可以实现更简单的解决方案。 3.12的一流模块也可能起作用,但它们往往对一流的存在类型有所帮助,而在这里我们使用通用类型。
除了您当地的打字问题,我不确定您是否正确行事。您正试图通过“提前”(在使用操作之前)构建一个闭包,该闭包对应于您的操作的语言表示。
根据我的经验,这种方法通常不会消除解释开销,而是将其移动到另一层。如果你天真地创建了闭包,你将在闭包层重现控制的解析流程:闭包将调用其他闭包等,因为你的解析代码在创建闭包时“解释”了输入。您消除了解析的成本,但可能是次优的控制流仍然是相同的。另外,闭包往往是直接操作的痛苦:你必须非常小心比较操作,例如序列化等。
我认为您可能会对代表您的操作的中间“reified”语言感兴趣:一种用于算术运算的简单代数数据类型,您可以根据文本表示来构建。你仍然可以尝试从它“提前”构建闭包,虽然我不确定表演是否比直接解释它更好,如果内存中的表示是体面的。此外,插入中间分析仪/变换器以优化您的操作将更加容易,例如从“关联二元操作”模型转变为“n-ary操作”模型,可以更有效地评估。