为什么操作数的顺序会影响范围?

Why does the order of operands affect scope?

我试图理解为什么某个构造函数在一个表达式中被接受而在另一个表达式中不被接受。我原以为它会超出两者的范围。我是 OCaml 的初学者(我主要使用 Haskell),所以我可能会遗漏一些对有经验的人来说非常明显的东西。

type zero = Zero
type 'n succ = Succ
type 'n snat =
  | SZero : zero snat
  | SSucc : 'm snat -> 'm succ snat

module SimpleInduction (Pred : sig type 'n pred end) = struct
  open Pred
  type hyps =
    { base : zero pred
    ; step : 'm. 'm pred -> 'm succ pred}

  let rec induct : type n. hyps -> n snat -> n pred =
          fun h sn -> match sn with
          | SZero -> h.base
          | SSucc p -> h.step (induct h p)
end;;

let module Snot = struct type 'n pred = Top end in
  let module Goop = SimpleInduction(Snot) in
    Goop.induct {base = Top; step = fun _ -> Top} SZero = Top;;
(*
let module Snot = struct type 'n pred = Top end in
  let module Goop = SimpleInduction(Snot) in
    Top = Goop.induct {base = Top; step = fun _ -> Top} SZero;;
*)

出于某种原因,这编译得很好。 Snot 的第二个定义未注释,我得到一个错误:

19 |     Top = Goop.induct {base = Top; step = fun _ -> Top} SZero;;
         ^^^
Error: Unbound constructor Top

是什么将 Top 引入了 Snot 的第一个定义中?使用常规模块而不是 first-classlocal 没有区别。

如果我在左侧使用 Snot.Top,我在右侧就不会收到任何投诉。为什么 ?

简而言之,类型导向的消歧确实不局限于范围。

通过显式类型注释,类型检查器可以 select 类型中的构造函数,而无需将构造函数带入作用域。

例如,

module M = struct type 'a t = A of 'a end
let ok: _ M.t = A ()
let wrong: _ M.t = A (A ())

第一个示例是有效的,因为类型注释足以知道 A () 中的 A 是一个 _ A.t。但是,第二个示例不起作用,因为构造函数尚未进入范围。

此外,类型导向消歧只需要已知构造函数或记录的预期类型。通常,在这个例子中

let expected =
  let f (M.A x) = x in
  f (A ())

我们知道f的参数类型是_ M.t,因此我们知道f (A ())中的A来自_ M.t我们可以像在带有显式注释的情况下一样使用类型导向消歧。

如果您发现这种行为异常,可以在这种情况下使用警告 42 [name-out-of-scope] 发出警告。使用此警告编译您的示例会产生(以及此警告的许多其他实例)

23 |     Goop.induct {base = Top; step = fun _ -> Top} SZero = Top
                                                               ^^^
Warning 40 [name-out-of-scope]: Top was selected from type Snot.pred.
It is not visible in the current scope, and will not 
be selected if the type becomes unknown.

(警告名称是 4.12 中的新名称)

关于你的第二点,在没有明确注释的情况下,表达的顺序可能很重要。事实上,如果没有显式注释,类型导向消歧将只能在预期类型已知时 select 正确的构造函数。在 OCaml 中,类型检查是从左到右进行的。因此在

... = Top

左侧的类型已经推断出来,因此 Top 的预期类型是 _ Snot.pred。 当顺序颠倒时

Top = ...

类型检查器试图找到一个没有任何类型信息的构造函数 Top,并且范围内没有构造函数 Top。因此它失败并出现 Unbound constructor 错误。如果你想避免依赖顺序,你可以

  • 填写构造函数全名:
  Snot.Top = ...
  • 使用显式类型注释
(Top: _ Snot.pred) = ...
  • 打开Snot模块。
  Snot.( Top ) = ...
  (* or *)
  let open Snot in Top = ...

我建议使用其中一种解决方案,因为有更强大的解决方案。

毕竟依赖类型检查的具体实现是脆弱的。 事实上,有一个编译器标志 -principal 和一个警告 (18) [not-principal] 会在存在这种潜在的脆弱推理时发出警告:

23 |     Goop.induct {base = Top; step = fun _ -> Top} SZero = Top
                                                               ^^^
Warning 18 [not-principal]: this type-based constructor disambiguation is not principal.

这里的“not principal”表示基于类型的消歧结果取决于类型检查的顺序。