我有一个源于现有代码的大量类型。让我们说它看起来像这样:
type some_type =
| Variant1 of int
| Variant2 of int * string
虽然其他地方都使用了Variant1
和Variant2
,但我有一个仅在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"))
如果Variant1
和Variant2
是不同的类型,我会期望OCaml推断出Variant2 -> ()
的{{1}}类型,因为它们都是相同总和的变体类型,OCaml推断签名print_the_string
。
当我遇到一个抛出异常并带有“这将永远不会发生”的消息的程序时,我通常认为原来的程序员做错了。
当前的解决方案有效,但这意味着程序中的错误将在运行时被捕获,而不是编译器错误。
理想情况下,我希望能够像这样注释函数:
some_type -> ()
但是,当然,这是不允许的。
问题:在将let print_the_string (x : some_type.Variant2) =
传递给Variant1
的任何情况下,有没有办法导致编译错误?
有一个相关问题被问到here,但是nlucarioni和Thomas的回答只是解决了处理不正确呼叫的更简洁方法。我的目标是让程序更明显地失败,而不是更少。
更新:我接受了加利斯的解决方案,因为在使用它之后,它似乎是实现这样的最干净的方式。不幸的是,如果没有非常混乱的包装器,我不相信任何解决方案都适用于我无法修改print_the_string
的原始定义的情况。
答案 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
,只需Variant3
和Variant4
作为输入,以检查您是否明白这一点。
| 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
。所以当你有一个具有这种类型的值时,你可以很容易地解构它而不使用显式模式匹配,因为它变得无可辩驳,即它不会失败。