仅使用Hindley-Milner类型系统定义列表


10

我正在开发一个小型lambda演算编译器,该编译器具有运行正常的Hindley-Milner类型推断系统,并且现在还支持递归let(不在链接代码中),据我所知应该足以使Turing完成

现在的问题是我不知道如何使它成为支持列表,或者它是否已经支持它们,我只需要找到一种编码它们的方法。我希望能够定义它们,而不必向类型系统中添加新规则。

我想到的一个列表的最简单方法x是将其作为null(或为空列表),或同时包含x和的列表对x。但是要做到这一点,我需要能够定义对和或,我相信它们是乘积和总和类型。

看来我可以这样定义对:

pair = λabf.fab
first = λp.p(λab.a)
second = λp.p(λab.b)

因为pair将具有类型a -> (b -> ((a -> (b -> x)) -> x)),所以在传递一个int和一个后string,它会产生一个带有类型的东西(int -> (string -> x)) -> x,这将是一对intand 的表示string。这里令我困扰的是,如果那个代表一对,那为什么在逻辑上不等同于或暗示这个命题int and string呢?但是,等效于(((int and string) -> x) -> x),好像我只能将产品类型作为函数的参数一样。这个答案似乎解决了这个问题,但我不知道他使用的符号是什么意思。另外,如果这不能真正对产品类型进行编码,那么我对上面的对的定义无法做到的产品类型(考虑到我也可以用相同的方式定义n元组)做任何事情?如果不是,这是否与仅使用蕴涵就不能表达(AFAIK)连词的事实相矛盾?

另外,总和类型呢?我可以仅使用函数类型以某种方式对其进行编码吗?如果是这样,是否足以定义列表?否则,是否有其他方法可以定义列表而不必扩展我的类型系统?如果不是这样,如果我想使其尽可能简单,我需要进行哪些更改?

请记住,我是计算机程序员,但不是计算机科学家或数学家,并且在阅读数学符号方面相当不擅长。

编辑: 我不确定到目前为止实现的技术名称是什么,但是我所拥有的基本上只是上面链接的代码,这是一种约束生成算法,使用应用程序规则,抽象规则和变量先从Hinley-Milner算法开始,然后是获得主体类型的统一算法。例如,该表达式\a.a将产生type a -> a,并且该表达式\a.(a a)将引发一个发生检查错误。最重要的是,这里没有一个确切的let规则,但是一个函数似乎具有与您定义递归全局函数(如此伪代码)相同的作用:

GetTypeOfGlobalFunction(term, globalScope, nameOfFunction)
{
    // Here 'globalScope' contains a list of name-value pair where every value is of class 'ClosedType', 
    // meaning their type will be cloned before unified in the unification algorithm so that they can be used polymorphically 
    tempType = new TypeVariable() // Assign a dummy type to `tempType`, say, type 'x'.
    // The next line creates an scope with everything in 'globalScope' plus the 'nameOfFunction = tempType' name-value pair
    tempScope = new Scope(globalScope, nameOfFunction, tempType) 
    type = TypeOfTerm(term, tempScope) // Calculate the type of the term 
    Unify(tempType, type)
    return type
    // After returning, the code outside will create a 'ClosedType' using the returned type and add it to the global scope.
}

代码基本上像往常一样获得术语的类型,但是在统一之前,它会将用伪类型定义的函数的名称添加到类型范围中,以便可以在其内部递归使用。

编辑2:我刚刚意识到,我还需要递归类型来定义所需的列表,而我没有。


您能否具体说明已实施的具体内容?您是否实现了简单类型的lambda演算(具有递归定义),并为其赋予了参数多态性Hindley-Milner风格?还是实现了二阶多态Lambda演算?
Andrej Bauer 2014年

也许我可以用一种更简单的方式问:如果我使用OCaml或SML并将其限制为纯lambda术语和递归定义,那是您在说的吗?
Andrej Bauer 2014年

@AndrejBauer:我已经编辑了问题。我不确定OCaml和SML,但是我很确定您是否采用Haskell并将其限制为lambda术语,并且顶级递归让(例如let func = \x -> (func x))可以得到我所拥有的。
2014年

1
为了改善您的问题,请查看此meta post
Juho 2014年

Answers:


13

此编码是对的教会编码。类似的技术可以对布尔值,整数,列表和其他数据结构进行编码。

x:a; y:bpair x y(a -> b -> t) -> t¬

一个bŤŤ¬¬一个¬bŤŤ一个b¬ŤŤ一个bŤ
ab tpairt

pair是对类型的构造函数,first并且second是析构函数。(这些是面向对象编程中使用的相同的词;在这里,这些词的含义与我在此不再赘述的类型和术语的逻辑解释有关。)直觉上,析构函数使您可以访问在对象中,构造函数通过将应用于对象各部分的函数作为参数为析构函数铺平道路。该原理可以应用于其他类型。

求和

区别联合的教堂编码基本上是一对教堂的编码。如果一对具有必须放在一起的两个部分,并且您可以选择提取一个或另一个,则可以选择以两种方式之一构建并集,并且在使用它时,您需要允许两种方式。因此,有两个构造函数,并且有一个带两个参数的析构函数。

let case w = λf. λg. w f g           case : ((a->t) -> (b->t) -> t) -> (a->t) -> (b->t) -> t
  (* or simply let case w = w *)
let left x = λf. λg. f x             left : a -> ((a->t) -> (b->t) -> t)
let right y = λf. λg. g x            right : b -> ((a->t) -> (b->t) -> t)

让我将类型缩写(a->t) -> (b->t) -> tSUM(a,b)(t)。然后,析构函数和构造函数的类型为:

case : SUM(a,b)(t) -> (a->t) -> (b->t) -> t
left : a -> SUM(a,b)(t)
right : b -> SUM(a,b)(t)

从而

case (left x) f g → f x
case (rightt y) f g → g y

清单

对于列表,请再次应用相同的原理。a可以使用两种方法构建其元素具有类型的列表:它可以是空列表,也可以是元素(头)加列表(尾)。与对相比,在析构函数上有一点扭曲:您不能有两个单独的析构函数headtail因为它们只能在非空列表上使用。您需要一个带有两个参数的析构函数,其中一个是nil情况的0参数函数(即一个值),另一个是cons情况的2参数函数。喜欢的功能is_emptyhead并且tail可以从派生。与求和一样,列表直接是其自己的析构函数。

let nil = λn. λc. n
let cons h t = λn. λc. c h t
let is_empty l = l true (λh. λt. false) 
let head l default = l default (λh. λt. h)
let tail l default = l default (λh. λt. t)

consconsconsŤŤ1个Ťñ

推测时,如果要定义仅包含同类列表的类型,则需要递归类型。为什么?让我们看一下列表的类型。列表被编码为带有两个参数的函数:在空列表中返回的值,以及计算在cons单元中返回的值的函数。让a是元素类型,b是列表的类型,c是由析构函数返回的类型。列表的类型是

a -> (a -> b -> c) -> c

使列表均匀是说如果它是一个cons单元格,则尾部必须具有与整体相同的类型,即它增加了约束

a -> (a -> b -> c) -> c = b

Hindley-Milner类型系统可以使用此类递归类型进行扩展,实际上,实际的编程语言可以做到这一点。实用的编程语言往往不允许这种“赤裸裸”的方程式,并且需要数据构造函数,但这不是基础理论的内在要求。要求数据构造函数可简化类型推断,并且在实践中倾向于避免接受实际上有错误的函数,但碰巧由于某些意想不到的约束而可打字,从而在使用该函数时导致难以理解的类型错误。例如,这就是为什么OCaml仅使用非默认-rectypes编译器选项接受不受保护的递归类型的原因。以下是OCaml语法中的上述定义,以及使用表示法的同类列表的类型定义别名递归类型type_expression as 'a表示类型type_expression与变量统一'a

# let nil = fun n c -> n;;
val nil : 'a -> 'b -> 'a = <fun>
# let cons h t = fun n c -> c h t;;
val cons : 'a -> 'b -> 'c -> ('a -> 'b -> 'd) -> 'd = <fun>
# let is_empty l = l true (fun h t -> false);;
val is_empty : (bool -> ('a -> 'b -> bool) -> 'c) -> 'c = <fun>
# let head l default = l default (fun h t -> h);;
val head : ('a -> ('b -> 'c -> 'b) -> 'd) -> 'a -> 'd = <fun>
# let tail l default = l default (fun h t -> t);;
val tail : ('a -> ('b -> 'c -> 'c) -> 'd) -> 'a -> 'd = <fun>
# type ('a, 'b, 'c) ulist = 'c -> ('a -> 'b -> 'c) -> 'c;;
type ('a, 'b, 'c) ulist = 'c -> ('a -> 'b -> 'c) -> 'c
# is_empty (cons 1 nil);;
- : bool = false
# head (cons 1 nil) 0;;
- : int = 1
# head (tail (cons 1 (cons 2.0 nil)) nil) 0.;;
- : float = 2.

(* -rectypes is required for what follows *)
# type ('a, 'b, 'c) rlist = 'c -> ('a -> 'b -> 'c) -> 'c as 'b;;
type ('a, 'b, 'c) rlist = 'b constraint 'b = 'c -> ('a -> 'b -> 'c) -> 'c
# let rcons = (cons : 'a -> ('a, 'b, 'c) rlist -> ('a, 'b, 'c) rlist);;
val rcons :
  'a ->
  ('a, 'c -> ('a -> 'b -> 'c) -> 'c as 'b, 'c) rlist -> ('a, 'b, 'c) rlist =
  <fun>
# head (rcons 1 (rcons 2 nil)) 0;;
- : int = 1
# tail (rcons 1 (rcons 2 nil)) nil;;
- : 'a -> (int -> 'a -> 'a) -> 'a as 'a = <fun>
# rcons 1 (rcons 2.0 nil);;
Error: This expression has type
         (float, 'b -> (float -> 'a -> 'b) -> 'b as 'a, 'b) rlist = 'a
       but an expression was expected of type
         (int, 'b -> (int -> 'c -> 'b) -> 'b as 'c, 'b) rlist = 'c

褶皱

从更一般的角度来看,代表数据结构的功能是什么?

  • ññ
  • XÿXÿ
  • 一世ñ一世X一世X
  • [X1个Xñ]

一般而言,数据结构表示为其折叠功能。这是数据结构的一般概念:折叠函数是遍历数据结构的高阶函数。从技术上讲,折叠是通用的:所有“通用”数据结构遍历都可以用折叠表示。可以将数据结构表示为其折叠功能表明了这一点:关于数据结构,您需要了解的所有内容是如何遍历它,剩下的就是实现细节。


您提到了整数,对,和的“ 教会编码”,但是对于列表,您使用了斯科特编码。我认为对于那些不熟悉归纳类型编码的人可能会有些困惑。
斯特凡希门尼斯

因此,基本上,我的对类型实际上不是产品类型,因为具有这种类型的函数可以返回t并忽略应该采用a和的参数b(这正是(a and b) or t在说)。看来我在加和方面也有同样的麻烦。而且,没有递归类型,我将没有同类列表。因此,用几句话来说,您是说我应该添加求和,乘积和递归类型规则以获取同类列表?
2014年

您是说case (right y) f g → g y总结”部分的末尾吗?
胡安

@StéphaneGimenez我没有意识到。我不习惯在类型化的世界中处理这些编码。您可以为Church编码和Scott编码提供参考吗?
吉尔(Gilles)'所以

@JuanLuisSoldi您可能已经听说过“没有任何问题可以通过额外的间接级别解决”。教会编码通过添加一定级别的函数调用将数据结构编码为函数:数据结构成为二阶函数,您可以将其应用于该函数以对部件起作用。如果要使用同构列表类型,则必须处理尾巴的类型与整个列表的类型相同的事实。我认为这必须涉及一种类型递归。
吉尔斯(Gilles)'所以

2

您可以将总和类型表示为带有标签和值的产品类型。在这种情况下,我们可以作弊并使用一个标签来表示是否为空,而使用第二个标签来表示头/尾对。

我们以通常的方式定义布尔值:

true = λi.λe.i
false = λi.λe.e
if = λcond.λthen.λelse.(cond then else)

那么列表就是一对,第一个元素为布尔值,第二个元素为头/尾对。一些基本的列表功能:

isNull = λl.(first l)
null = pair false false     --The second element doesn't matter in this case
cons = λh.λt.(pair true (pair h t ))
head = λl.(fst (snd l))   --This is a partial function
tail = λl.(snd (snd l))   --This is a partial function  

map = λf.λl.(if (isNull l)
                 null 
                 (cons (f (head l)) (map f (tail l) ) ) 

但这不会给我同类清单,对吗?
2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.