Ocaml 保持类型泛型,不带η-膨胀

Ocaml 保持类型泛型,不带η-膨胀,ocaml,polymorphism,hindley-milner,Ocaml,Polymorphism,Hindley Milner,我在做什么:我正在编写一个小型解释器系统,它可以解析一个文件,将其转换为一系列操作,然后将数千个数据集输入到该序列中,从每个数据集中提取最终值。编译的解释器由一系列纯函数组成,这些纯函数包含两个参数:一个数据集和一个执行上下文。每个函数都返回修改后的执行上下文: type('data',context)解释器=('data->'context->'context)列表 编译器本质上是一个标记器,具有最后一个标记到指令映射步骤,该步骤使用定义如下的映射描述: type('data',context

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

type('data',context)解释器=('data->'context->'context)列表

编译器本质上是一个标记器,具有最后一个标记到指令映射步骤,该步骤使用定义如下的映射描述:

type('data',context)map=(string*('data->'context->'context))列表

典型的解释器用法如下所示:

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
方法的类以及相应的
数据类型(可以是一个上下文类的整数,也可以是另一个上下文类的浮点数)一起工作

但是,
pocket\u calc
被定义为一个值而不是一个函数,因此类型系统不会将其类型设置为泛型:第一次使用时,
'data
'context
类型绑定到我首先提供的任何数据和上下文的类型,解释器将永远与任何其他数据和上下文类型不兼容

一个可行的解决方案是扩展解释器的定义,以允许其类型参数为泛型:

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扩展外,是否有其他方法使类型参数化?也许是一些涉及模块签名或继承的聪明技巧?如果这是不可能的,有没有办法缓解我上面提到的三个问题,从而使eta扩展成为一个可接受的解决方案?谢谢大家!

我认为您的问题在于操作中缺少多态性,您希望使用一个封闭的参数化类型(适用于支持以下算术原语的所有数据),而不是使用表示固定数据类型的类型参数。 然而,要确保它确实是这样有点困难,因为您的代码没有足够的自包含性来测试它

假设基本体的给定类型:

type 'a primitives = <
  add : 'a -> 'a;
  mul : 'a -> 'a; 
  sub : 'a -> 'a;
>
您将返回以下数据不可知类型:

 val map : (string * op) list
编辑:关于您对不同操作类型的评论,我不确定您想要哪种级别的灵活性。我不认为您可以在同一个列表中混合对不同原语的操作,并且仍然可以从每个原语的特定性中获益:充其量,您只能将“add/sub/mul上的操作”转换为“add/sub/mul/div上的操作”(因为我们在原语类型中是逆变的),但肯定不会太多

在更实用的层面上,使用这种设计时,每个原语类型都需要不同的“操作”类型。但是,您可以轻松地构建一个由基元类型参数化并返回操作类型的函子

我不知道如何暴露不同原语类型之间的直接子类型关系。问题是,这需要在函子级别建立一个子类型关系,我认为在Caml中没有这种关系。但是,您可以使用一种更简单的显式子类型(而不是强制转换
a:>b
,使用函数
a->b
),构建第二个函子,逆变,即给定从一个基元类型到另一个基元类型的映射,将构建从一个操作类型到另一个操作类型的映射

完全有可能的是,通过对已演化类型的一种不同而巧妙的表示,一种更简单的解决方案是可能的。3.12的第一类模块也可能发挥作用,但它们往往有助于第一类存在类型,而这里我们使用的是通用类型

解释性开销和操作具体化 除了你当地的打字问题,我不确定你的方向是否正确。您试图通过构建“提前”(在使用操作之前)来消除解释性开销,该闭包对应于操作的语言表示形式

根据我的经验,这种方法通常不会消除解释性开销,而是将其转移到另一层。如果您天真地创建了闭包,那么将在闭包层复制控制的解析流:闭包将调用其他闭包,等等,因为您的解析代码在创建闭包时“解释”了输入。您消除了解析的成本,但可能次优的控制流仍然是一样的。此外,闭包往往是直接操作的难点:您必须非常小心比较操作,例如序列化等

我认为,从长远来看,您可能会对一种表示您的操作的中间“具体化”语言感兴趣:一种用于算术运算的简单代数数据类型,您可以从文本表示构建它。您仍然可以尝试“提前”从中构建闭包,尽管我不这么认为
 val map : (string * op) list
 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
 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