【问题标题】:Unifying OCaml patterns through function composition通过函数组合统一 OCaml 模式
【发布时间】:2016-11-04 11:55:55
【问题描述】:

假设我手动定义了一些这样的代码:

type test  = A of bool | B | C of bool * bool 
type test2 = D | E of bool * bool 
type test3 = F | G of bool | H of bool

let f = function | A(x) -> E(x,true) | B -> D | C(a,b) -> E(b,a)

let g = function  | D -> F | E(a,b) -> if a then G(b) else H(b)

现在我想将组合 g ∘ f 评估为不需要使用中间表示 test2 的函数。我一直在考虑结构统一,但我在问以下问题:

  1. 声明let h x = g (f x)时OCaml会自动进行这样的统一吗?
  2. 如果没有,是否存在可以在 OCaml 中实现的通用算法,以便将 compile 放入所需的 ml 文件中?

提前致谢

【问题讨论】:

  • 我不知道 OCaml,但 GHC 可以通过内联 f 和 g,然后应用 case-of-case 和 case-of-known-constructor 来做到这一点。
  • 好吧……不过,我宁愿只使用一种语言(我的所有其余代码都是用 OCaml 编写的)。顺便说一句,感谢您的洞察力。
  • @melpomene,好吧,任何语言都“可能”做任何事情......你有真正的证据证明 GHC 会这样做吗?
  • @ivg 否。这是对 2.“是否存在通用算法...?”的回应
  • @ivg,所以,他的意思是我可以在 Haskell 中实现这样的算法。

标签: pattern-matching ocaml unification


【解决方案1】:

免责声明

我不是 OCaml 编译器开发人员。如果您想获得他们的意见,那么最好在邮件列表中与他们联系。

第 1 部分

理论上,现代 OCaml(我已尝试使用 4.0{3,4}+flambda)能够消除test2 类型的一些中间数据结构。但是,要实现这一点,您需要传递特殊的优化选项,并将[@@inlined always] 添加到函数f,否则即使使用像-inline 10000-inline-toplevel 10000 这样的疯狂内联选项,它也不会内联。

但是,在一般情况下,对于更大的功能,这可能不起作用。在这里,我想,您展示的示例是一个玩具示例,在现实生活中,您面临着更大的功能和两个以上的组合(即数百个具有数百个组合的构造函数),否则,它根本不值得优化)。

第 2 部分

理论

说到一般算法,如果我们太挑剔的话,那是不可能的,因为模式匹配中->的右边可以有任何OCaml表达式,即图灵完备的程序。所以,我们有一个等价的决策问题。即使我们通过禁止循环和副作用来限制表达式语言,问题仍然是 NP-hard,因此尝试解决它可能不值得。但是,如果我们进一步限制自己,通过禁止除构造函数应用程序之外的任何表达式,那么我们实际上将编码一个有限状态机(FSM)。有很多定义明确的 FSM 优化和状态最小化算法,因此从中删除冗余并不难。

练习

实际上,我可能不会编写将ml 代码转换为ml 代码的函数。根据我的实际任务,我会考虑以下方法。

可枚举的输入集

如果test 类型的值集实际上是有限的(即,如果它适合 OCaml 数组),那么我会尝试编写一个函数来枚举test 类型的所有可能值,并存储合成的结果。您可以使用[@@deriving enumerate] 计算test 类型的所有可能值

# type test  = A of bool | B | C of bool * bool [@@deriving enumerate];;
type test = A of bool | B | C of bool * bool
val all_of_test : test list =
  [A false; A true; B; C (false, false); C (true, false); C (false, true);
   C (true, true)]

然后,您可以为test 类型的每个值分配一个序数,并通过O(1) 转换为test3 类型。此外,根本不会有任何分配,因为我们已经预先计算了所有构造函数。

但是,您仍然需要转换 test -> int 才能使我们的方法工作,并且此操作将需要 log(k) 分支,其中 ktest 类型的构造函数的数量。从大 O 符号的角度来看,k 是恒定的。但如果它真的很大,那么你可以尝试让你的所有构造函数都没有参数,例如,将A of bool 表示为两个构造函数A_trueA_false。在这种情况下,您将进行纯 O(1) 转换,没有开销(只是一个数组取消引用)。

具有未解释函数的可枚举输入

如果存在具有intstring 类型值的构造函数,则几乎不可能枚举它们。但是,如果转换 h 不查看此值,而只是重新排列它们,即将它们视为 Uninterpreted functions,那么您应该尝试使用 GADT 将这些参数与输入语言分离。假设您有以下数据构造函数:

| C of string

结果h (C s) 对于所有s 都是相同的。然后根本不需要将s 传递给h。但是,您仍然希望将有效负载 s 传递给其他函数,例如 exec。可以使用 GADT,首先我们将有效负载与构造函数解耦:

| C : string query

然后我们将能够将查询作为两个不同的参数传递给函数:

val exec : 'a query -> 'a -> unit

一般情况

如果上述两种情况不适用于您,那么您需要编写自己的编译器 :) 看起来,您正在实现某种语言的解释器,它使用 OCaml 作为宿主语言,而您'希望 OCaml 会为你做优化。好吧,OCaml 确实是编写解释器的一个不错的选择,但它不能优化你的 DSL,因为它不知道你的 DSL 的语义。语义规则的巧妙应用是所有优化的目的。因此,这意味着,如果您对解释器的性能不满意,那么您需要编写自己的优化编译器。首先,您需要设计执行查询的抽象机器,然后编写一个针对该机器的优化器。

【讨论】:

  • 确实,这只是一个玩具示例。我正在实现一种查询语言,因此其中有很多不同的案例和重写。因此,我认为在我的情况下,我无法枚举所有可能的情况(请参阅,我也可以将字符串作为构造函数的一部分,例如属性或值)。所以我认为在这种情况下我不能枚举所有可能的元素(ps。所以,在你的解决方案中,我有 两个 声明相同的数据类型?)
  • 第二个只是来自 REPL 的回复(我添加了# 以使其更明显)。因此,如果您的实际问题包括实际上不可枚举的类型,但h 转换不会解释这些类型,那么您可能应该将它们与查询语言分离。您可以为此使用 GADT。但是,可能更容易的是,不要尝试用宿主语言对查询进行编码,而是直接解决它。 只是,写一个AST,然后做常规查询语言优化。
  • 对不起,我不清楚。所以,我要说的是,我可以有一个 AST 作为我的语言的中间表示,但是这种“函数组合部分”只涉及将我的 AST 中间重写为其他语言(例如,SQL 在关系代数中被重写) ,并且不涉及查询优化(目前)。所以我一直在处理“GADT”,但唯一的问题是它可能是C("hi","there"),在这种情况下,我无法枚举所有可能的字符串。
  • 当我提议使用 GADT 时,我的意思是您应该使用 C : (string * string) query 查询构造函数,而不是 C of string * string,并将您的查询作为两个值传递,查询类型(种类)和查询有效负载,例如'a query -> 'a。使用这种编码,每个查询都将是一个立即值,不需要分配。如果您将此方法与 CPS 结合使用,那么您将完全摆脱任何分配。这将使您的代码已经非常高效。
  • 现在,我明白了你的问题。可能,您应该考虑使用抽象来隐藏两种不同的表示,这样您就不需要进行具体的转换。如果这对您不起作用,那么请确保您确实可以快速进行具体的转换,如果您将摆脱数据分配,即通过使用空 GADT 构造函数和 CPS 在函数之间传递数据。一旦完成,转换将只是一个纯粹的算术,所以你可以在一秒钟内完成数十亿次。
【解决方案2】:

对于旧版本的 OCaml,这是不确定的。

但是,如果您激活flambda optimization,您可以肯定您将从这种优化中受益。

请注意,在构建编译器时必须激活 flambda(如果您使用 opam,则有一个专用开关)。您的编译时间可能会更长一些,但这是完全值得的。

如果您想确保它被内联(并因此被简化),您可以在函数声明中使用 [@@inline always] 属性或在函数调用中使用 [@@inlined always]

【讨论】:

  • 非常感谢。我被 7 年前写的 OCaml 的参考手册和一些旧的东西困住了。谢谢!
  • 哇,看看最新的语言特性,你可以从中得到很多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-26
  • 1970-01-01
相关资源
最近更新 更多