Value Restriction,从OCaml到F#

Value Restriction是什么?

Value restriction是用于控制类型推断能否对值声明进行多态泛化的规则(MLton原文:“The value restriction is a rule that governs when type inference is allowed to polymorphically generalize a value declaration.”)。常出现在ML系的语言中,如SMLOCamlF#中,其实value restriction产生的本质原因是为了保证类型系统在结合参数多态与命令式特性(imperative feature,如ref)时候的可靠性(soundness)。一个典型的例子就是:

// 如果没有value restriction
let x = ref None  // 'a option ref
let y: int option ref = x // type checked
let z: string option ref = x // type checked
let () = y := Some 2  // type checked
let  v: string = !z  // 破坏了类型安全

限制了什么?

简单来讲,value restriction限制了类型泛化只能发生在表达式的右边是句法意义上的值。那么什么是句法意义上的值呢,SML的语言规范上明确给出了什么样的表达式是句法意义上的值(准确来说是non-expansive):

  • 常量,如13,"string"
  • 变量,如x,y
  • 函数,如fn x => e
  • 除了ref以外的构造函数在值上的调用,如Foo v
  • 类型上受约束的值,如v: t
  • 每一个元素都是值的tuple, 如(v1, v2, v3)
  • 每一个字段都是值的record, 如{l1 = v1, l2 = v2}
  • 每一个元素都是值的list, 如[v1, v2, v3]

确切的来讲,只要是协变(covariant)的类型并且不和可变的特性相结合,那么它总是可以类型安全的泛化(OCaml manual原文:“As a corollary, covariant variables will never denote mutable locations and can be safely generalized.”)。即:

  1. 是没有副作用的
  2. 表达式的结果是一个不可变对象

在完备性上的问题

从上述规则来看,let x = ref None显然是非法的表达式,然而在引入value restriction的同时,类型系统损失了一定的完备性(completeness),因为以下代码同样违反了value restriction:

let id x = x  // 'a -> 'a
let listId = List.map id  // 违反了value restriction

即使我们只使用不可变特性,上述代码依然无法通过类型检查。因为函数调用不是句法意义上的值(因为编译器无法判断函数调用是否是pure的)。当然上述问题可以通过eta-expansion来避免,即:

let listId = fun x -> List.map id x  // 'a list -> 'a list

lambda表达式是句法意义上的值,因此上述代码是可以通过类型检查的。

如何避免value restriction

为了能够使得我们本身soundness的代码通过类型检查,在value restriction的限制下我们不得不做一些额外的工作。

  1. eta-expansion

    向上一个例子那样,我们可以引入一个自由变量,使得函数调用变成了一个函数声明,从而通过了类型检查。

    let lsitId = fun x -> List.map id x
    

    在这种情况下,每一次listId被调用时,List.map id都会被调用。而不是像原来那样只在声明listId时调用一次,当然在有些情况下这可能会造成一个性能问题。

  2. 引入局部变量,例如以下代码同样无法通过类型检查

    type 'a T = A of string | B of 'a
    let a = A (if true then "yes" else "no")  // failed
    

    但是可以修改为

    let s = if true then "yes" else "no" in 
    let a = A s
    

    使得其符合value restriction的规则。

OCaml和F#中的value restriction

OCaml和F#同样存在着value restriction的完备性的问题,俩者通过不同的方式对其进行了relax。

OCaml的relaxed value restriction

OCaml通过引入一个弱类型变量来放宽value restriction. 所谓弱类型变量是指编译器未知的变量,而一旦这个弱类型变量被编译器推断为一个具体的变量时,该弱类型变量就被具体的变量所替代,并且不在可变。例如:

# let a = ref None;;
val a : '_a option ref = {contents = None}
# let () = a := Some 2;;
# a;;
- : int option ref = {contents = Some 2}

这和我们第一个例子是类似的,同意违反了value restriction。但是OCaml将a的类型推断为'_a option ref,这里的弱类型变量'_a指代的是未知的类型变量,在let () = a := Some 2中,编译器将'_a推断为int并且将a的类型固定为int option ref,通过这样的处理解决了第一个例子所展示的类型不安全的问题。换一种角度来看,所谓的弱类型变量是推迟了推断的具体的变量,即具体变量的占位符。这样确实解决了原有value restriction的完备性的问题,但同样导致了某些程序不在足够的泛化。例如

# let id x = x;;
val id : 'a -> 'a = <fun>
# let listId = List.map id;;
val listId : '_a list -> '_a list = <fun>

和前面一样,这同样是一个违反了value restriction的例子,于是OCaml使用了弱类型变量来处理,这意味着一旦我们在int list类型上调用完listId,例如listId [1; 2; 3],之后listId就被固定为int list -> int list,这意味着我们无法再在string list上调用listId,而这同样不符合我们泛化的初衷,即'a list -> 'a list。当然我觉得OCaml的relaxed value restriction算是处理的非常优雅,有兴趣的可以阅读相关论文[6]。

F#的处理

虽说F#参照了OCaml, 但还是存在着相当多的不同之处,在value restriction的处理上俩者也存在着区别。在F#中,上述违反了value restriction的例子依然是非法的。F#语言规范中同样明确给出了可以泛化的情况(generalizable):

  • 函数表达式
  • 实现接口的对象表达式
  • 委托表达式
  • 右边同样是可泛化的let表达式
  • 右边同样是可泛化的let rec表达式
  • 所有元素都是可泛化的tuple表达式
  • 所有字段都是可泛化且不包含可变字段的record表达式
  • 所有参数都是可泛化的union case表达式(即union类型表达式)
  • 所有参数都是可泛化的exception表达式
  • 空的array表达式
  • 常量表达式
  • 带有GeneralizableValue标签的类型函数的调用

因此在F#中listId同样是非法的。但是F#允许你引入一个显示的泛型参数来解决这个问题,即:

> let listId<'T> : 'T list -> 'T list = List.map id;;
val listId<'T> : ('T list -> 'T list)

这样的处理虽然不够优雅,但似乎是完美解决了这个问题,因为这里不会出现OCaml那样泛化不够的问题。但我们在看ref的问题:

> let v<'T> : 'T option ref = ref None;;
val v<'T> : 'T option ref
> v := Some 2;;
val it : unit = ()
> let x: int option = !v;;
val x : int option = None  // Oops

我们看到,这里x的值居然是None,而不是预期的Some 2。实际上这里的v并不是一个ref对象,而是一个泛型类,其接收一个泛型参数,产生一个具体的类,当我们对v赋值时,真正调用的是(v<int>) := Some 2,而此时会生成一个新的ref对象。即使我们使用let x: int option = !v<int>得到的依然是None,因为此时又生成了一个新的ref对象,这个行为是由IL所决定的(有兴趣可以参考[4])。因此我们不得不声明类型变量:

> let v1 : int option ref = v<int>;;
val v1 : int option ref = { contents = None }
> let () = v1 := Some 2;;
> let x = !v1;;
val x : int option = Some 2

而这就又回到了OCaml的relaxed value restriction,并且比F#更加优雅:

# let v1 = ref None;;
val v1 : '_a option ref = {contents = None}
# let () = v1 := Some 2;;
# let x = !v1;;
val x : int option = Some 2

可见俩者在一定程度上是等价的。对于lsitId而言F#更有优势,因为泛型方法能够自动推断参数类型。而对于

ref对象而言,OCaml的处理更优雅,因为F#中,v变成了一个泛型类,而不是普通的值,而这是比较令人困惑的。在F#中,为了避免这样的问题,可以使用[<RequiresExplicitTypeArguments>],即:

[<RequiresExplicitTypeArguments>]
let v<'T> : 'T option ref = ref None

在这样的情况下,你将无法使用v := Some 2,而必须使用v<int> := Some 2,这样就能清晰的表示v是一个泛型类而不再是一个普通的值。另外,值得一提的是F#还提供了[<GeneralizableValue>](即上述可泛化对象的最后一条),来告诉编译器这是一个可泛化的值:

> [<GeneralizableValue>]
- let v<'T> : 'T option ref = ref None;;
val v<'T> : 'T option ref
> let a = v;;
val a : 'a option ref

如果没有[<GeneralizableValue>]let a = v将违反value restriction.

结语

如果你看到了这里,我想你对value restriction应该有了一个清晰的认识,并且对OCaml和F#如何放宽value restriction有了充分的了解。而如果你使用F#编程,那么我的建议是除非你清楚的知道自己在做什么(即添加额外的泛型参数),否则就按照MSDN的建议,我这边稍微扩展了一下:

  1. 添加一个显示的参数,使得其变为具体的类型

    let counter = ref None
    // Adding a type annotation fixes the problem:
    let counter : int option ref = ref None
    
  2. 使用eta-expansion将函数组合与部分调用展成一个lambda表达式或常规的函数

    let maxhash = max << hash
    // The following is acceptable because the argument 
    // for maxhash is explicit:
    let maxhash obj = (max << hash) obj
    // or
    let maxhash = fun obj -> (max << hash) obj
    
  3. 引入局部变量来重写表达式

    type 'a T = A of string | B of 'a
    let a = A (if true then "yes" else "no")
    // introducing a local variable fixs the problem
    let s = if true then "yes" else "no" in 
    let a = A s
    
  4. 通过添加一个额外的,无用的参数将表达式变成一个thunk

    let emptyList10 = Array.create 10 []
    // Adding an extra (unused) parameter makes it a function,
    // which is generalizable.
    let emptyList10 () = Array.create 10 []
    

最后,如果你有任何问题或者关于该文章的任何建议,欢迎邮件我。

参考文献

[1] ValueRestriction

[2] Polymorphism and its limitations

[3] Relaxed value restriction

[4] Finer Points of F# Value Restriction

[5] Wright, A. K. (1995). Simple imperative polymorphism. Lisp and symbolic computation, 8(4), 343-355.

[6] Garrigue, J. (2004, April). Relaxing the value restriction. In International Symposium on Functional and Logic Programming (pp. 196-213). Springer, Berlin, Heidelberg.

posted @ 2021-03-30 14:58  Christophe1997  阅读(292)  评论(0编辑  收藏  举报