保持类型通用而不用η-扩展

时间:2010-10-25 09:40:52

标签: ocaml polymorphism hindley-milner

我正在做什么:我正在编写一个小型解释器系统,可以解析文件,将其转换为一系列操作,然后将数千个数据集提供到该序列中以提取每个人的最终价值。编译的解释器由一系列带有两个参数的纯函数组成:数据集和执行上下文。每个函数都返回修改后的执行上下文:

  

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解释程序能够使用支持addsubmul方法的任何类,和相应的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成为可接受的解决方案?谢谢!

2 个答案:

答案 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.runinterpreter的部分解释可以做一些计算,你也应该将它移出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操作”模型,可以更有效地评估。