仅接受sum类型的一个变量作为OCaml函数参数

时间:2014-07-09 12:08:38

标签: ocaml type-inference

我有一个源于现有代码的大量类型。让我们说它看起来像这样:

type some_type =
  | Variant1 of int
  | Variant2 of int * string

虽然其他地方都使用了Variant1Variant2,但我有一个仅在Variant2上运行的特定功能:

let print_the_string x =
  match x with
  | Variant2(a,s) -> print_string s; ()
  | _ -> raise (Failure "this will never happen"); ()

由于此辅助函数仅从另一个地方调用,因此很容易显示它将始终使用Variant2的输入进行调用,而不是输入Variant1

让我们说这个电话看起来像这样:

let () =
  print_the_string (Variant2(1, "hello\n"))

如果Variant1Variant2是不同的类型,我会期望OCaml推断出Variant2 -> ()的{​​{1}}类型,因为它们都是相同总和的变体类型,OCaml推断签名print_the_string

当我遇到一个抛出异常并带有“这将永远不会发生”的消息的程序时,我通常认为原来的程序员做错了。

当前的解决方案有效,但这意味着程序中的错误将在运行时被捕获,而不是编译器错误。

理想情况下,我希望能够像这样注释函数:

some_type -> ()

但是,当然,这是不允许的。

问题:在将let print_the_string (x : some_type.Variant2) = 传递给Variant1的任何情况下,有没有办法导致编译错误?

有一个相关问题被问到here,但是nlucarioni和Thomas的回答只是解决了处理不正确呼叫的更简洁方法。我的目标是让程序更明显地失败,而不是更少。


更新:我接受了加利斯的解决方案,因为在使用它之后,它似乎是实现这样的最干净的方式。不幸的是,如果没有非常混乱的包装器,我不相信任何解决方案都适用于我无法修改print_the_string的原始定义的情况。

4 个答案:

答案 0 :(得分:6)

您的帖子中没有足够的信息来决定以下内容是否对您有用。这种方法基于传播一个不变量,并且如果你的代码是不变的,它将很好地发挥作用。基本上,如果你没有some_type -> some_type类型的函数,它们使用Variant2作为头部构造函数将值转换为使用Variant1构造的函数,那么你应该对这种方法没问题。否则很快就会很烦人。

这里我们要编码不变量"使用Variant2"通过使用进入类型 phantom types并将some_type定义为GADT。我们首先声明其唯一目的是扮演标签角色的类型。

type variant2
type variantNot2

现在,我们可以使用这些类型来记录用于生成值some_type的构造函数。这是Ocaml中的GADT语法;它与ADT略有不同,因为我们可以声明构造函数的返回类型是什么,不同的构造函数可以有不同的返回类型。

type _ some_type =
  | Variant1 : int          -> variantNot2 some_type
  | Variant2 : int * string -> variant2    some_type

只要他们的签名记录了他们不是Variant2的事实,人们也可以投入一些额外的构造函数。我今后不会处理它们,但你可以尝试扩展下面给出的定义,以便它们能够很好地与这些额外的构造函数配合使用。您甚至可以添加print_the_second_int,只需Variant3Variant4作为输入,以检查您是否明白这一点。

  | Variant3 : int * int    -> variantNot2 some_type
  | Variant4 : float * int  -> variantNot2 some_type

现在,print_the_string的类型可以非常精确:我们只对使用构造函数some_type构建的Variant2元素感兴趣。换句话说,print_the_string的输入应为variant2 some_type类型。编译器可以静态检查Variant2是该类型值的唯一构造函数。

let print_the_string (x : variant2 some_type) : unit =
  match x with Variant2 (_, s) -> print_string s

确定。但是如果我们有'a some_type类型的值,因为它是由客户交给我们的呢?我们建造它扔硬币;等等。?好吧,那里没有魔力:如果你想使用print_the_string,你需要确保使用Variant2构造函数构建了这个值。您可以尝试将值转换为variant2 some_type(但这可能会失败,因此使用option类型):

let fromVariant2 : type a. a some_type -> (variant2 some_type) option = function
  | Variant2 _ as x -> Some x
  | Variant1 _      -> None

或者(甚至更好!)决定价值在哪个领域:

type ('a, 'b) either = | Left  of 'a | Right of 'b

let em : type a. a some_type -> (variant2 some_type, variantNot2 some_type) either =
   fun x -> match x with
   | Variant1 _ -> Right x
   | Variant2 _ -> Left x

答案 1 :(得分:4)

我的解决方案是拥有print_the_string : int * string -> unit,因为Variant2部分不提供任何信息,您应放弃它。

答案 2 :(得分:3)

类型推断适用于推断类型(显然)不是类型的。但是你可以用多态变体做你提出的建议。虽然,我同意Thomash。

 type v1 = [ `Variant1 of int ]
 type v2 = [ `Variant2 of int * string ]

 let print_the_string (`Variant1 x) = ()

答案 3 :(得分:1)

加莱提供了一个很好但很长的答案,所以我决定添加一个较短的版本。

如果您有变体类型并且想要添加仅适用于变体子集的函数,那么您可以使用GADTS。考虑一下这个例子:

open Core.Std

type _ t =
  | Int: int -> int t
  | Str: string -> string t

let str s = Str s

let uppercase (Str s) = Str (String.uppercase s)

函数uppercase的类型为string t -> string t,只接受t类型的字符串版本,因此您可以在适当的位置解构变量。函数str具有类型string -> string t,因此返回类型本身包含一个信息(见证类型),该函数生成的唯一可能变体是Str。所以当你有一个具有这种类型的值时,你可以很容易地解构它而不使用显式模式匹配,因为它变得无可辩驳,即它不会失败。