如何在没有“let rec”的情况下定义y-combinator?

时间:2010-01-04 09:23:50

标签: f# y-combinator

在几乎所有的例子中,ML类型语言中的y-combinator都是这样编写的:

let rec y f x = f (y f) x
let factorial = y (fun f -> function 0 -> 1 | n -> n * f(n - 1))

这可以按预期工作,但使用let rec ...定义y-combinator感觉就像作弊一样。

我想使用标准定义来定义这个组合子而不使用递归:

Y = λf·(λx·f (x x)) (λx·f (x x))

直接翻译如下:

let y = fun f -> (fun x -> f (x x)) (fun x -> f (x x));;

然而,F#抱怨它无法弄清楚类型:

  let y = fun f -> (fun x -> f (x x)) (fun x -> f (x x));;
  --------------------------------^

C:\Users\Juliet\AppData\Local\Temp\stdin(6,33): error FS0001: Type mismatch. Expecting a
    'a    
but given a
    'a -> 'b    
The resulting type would be infinite when unifying ''a' and ''a -> 'b'

如何在不使用let rec ...的情况下在F#中编写y-combinator?

5 个答案:

答案 0 :(得分:51)

正如编译器指出的那样,没有可以分配给x的类型,因此表达式(x x)是良好类型的(这不是严格正确的;您可以显式键入{{ 1}}作为x - 见我的最后一段)。您可以通过声明递归类型来解决此问题,以便可以使用非常相似的表达式:

obj->_

现在Y-combinator可以写成:

type 'a Rec = Rec of ('a Rec -> 'a)

不幸的是,你会发现这不是很有用,因为F#是一种严格的语言, 所以你尝试使用这个组合器定义的任何函数都会导致堆栈溢出。 相反,您需要使用Y-combinator的应用顺序版本(let y f = let f' (Rec x as rx) = f (x rx) f' (Rec f') ):

\f.(\x.f(\y.(x x)y))(\x.f(\y.(x x)y))

另一种选择是使用显式懒惰来定义正常的Y组合子:

let y f =
  let f' (Rec x as rx) = f (fun y -> x rx y)
  f' (Rec f')

这样做的缺点是递归函数定义现在需要一个惰性值的显式强制(使用type 'a Rec = Rec of ('a Rec -> 'a Lazy) let y f = let f' (Rec x as rx) = lazy f (x rx) (f' (Rec f')).Value 属性):

Value

但是,它具有以下优点:您可以使用惰性语言定义非函数递归值:

let factorial = y (fun f -> function | 0 -> 1 | n -> n * (f.Value (n - 1)))

作为最后的替代方案,您可以尝试通过使用拳击和向下转换来更好地逼近无类型的lambda演算。这会给你(再次使用Y-combinator的应用顺序版本):

let ones = y (fun ones -> LazyList.consf 1 (fun () -> ones.Value))

这有明显的缺点,它会导致不需要的装箱和拆箱,但至少这完全是内部的实现,并且永远不会在运行时导致失败。

答案 1 :(得分:10)

我会说这是不可能的,并且问为什么,我会手动并调用简单输入lambda演算的事实normalization property。简而言之,简单类型的lambda演算的所有项都终止(因此Y不能在简单类型的lambda演算中定义)。

F#的类型系统并不完全是简单类型lambda演算的类型系统,但它足够接近。没有let rec的F#非常接近简单类型的lambda演算 - 而且,重申一下,在那种语言中你不能定义一个不终止的术语,并且不包括定义Y.

换句话说,在F#中,“let rec”至少需要是一个语言原语,因为即使你能够从其他原语定义它,你也无法输入这个定义。将它作为原语允许您(除其他外)为该原语提供特殊类型。

编辑:kvb在他的回答中表明,类型定义(简单类型的lambda演算中缺少的一个特征,但存在于let-rec-less F#中)允许得到某种递归。非常聪明。

答案 2 :(得分:4)

ML衍生案中的Case和let语句是Turing Complete的原因,我相信它们基于System F而不是简单的输入,但重点是相同的。

系统F找不到任何定点组合子的类型,如果可以的话,它不会强烈正常化。

强正规化意味着任何表达式都具有一个正常形式,其中正常形式是一个无法进一步缩小的表达式,这与无类型表达式不同,其中每个表达式都具有 at max 一个普通形式,它也可以没有正常形式。

如果类型化的lambda calculi可以以任何方式构造一个固定点运算符,那么表达式很可能没有正常形式。

另一个着名的定理,即停机问题,意味着强烈规范化的语言不是图灵完整的,它说不可能决定(不同于证明)图灵完整语言的程序的哪个子集将停止输入什么。如果一种语言正在强烈规范化,那么如果它停止,则它是可判定的,即总是停止。我们决定这个的算法是程序:true;

为了解决这个问题,ML衍生产品扩展了System-F with case和let(rec)来克服这个问题。因此,函数可以再次在它们的定义中引用它们,使它们实际上不再是lambda演算,不再可能单独依赖匿名函数来处理所有可计算函数。因此,他们可以再次进入无限循环并恢复他们的图灵完整性。

答案 3 :(得分:2)

简短的回答:你不能。

答案很长: 简单类型的lambda演算强烈正常化。这意味着它不是图灵的等价物。其原因基本上归结为Y组合子必须是原始的或递归定义的(如您所见)。它根本无法在系统F(或更简单的类型计算)中表达。没有办法解决这个问题(毕竟已经证明了这一点)。您可以实施的Y组合器可以按照您想要的方式工作。

如果你想要一个真正的教堂式Y组合器,我建议你试试方案。使用上面给出的应用版本,因为其他版本将无法使用,除非您明确添加惰性,或使用惰性Scheme解释器。 (在技术上,Scheme不是完全无类型的,但是它是动态类型的,这对此来说已经足够了。)

为了证明强正常化,请参见此处: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.127.1794

在考虑了一些之后,我很确定添加一个原始的Y组合子,其行为与letrec定义的方式完全相同,使得System F图灵完成。您需要做的就是模拟图灵机然后将磁带实现为整数(以二进制解释)和移位(定位头)。

答案 4 :(得分:0)

简单地定义一个函数,将它自己的类型作为记录,就像在 Swift 中一样(有一个结构体):)

这里,Y(大写)在语义上被定义为一个可以用它自己的类型调用的函数。在 F# 术语中,它被定义为包含名为 call 的函数的记录,因此要调用定义为这种类型的 y,您必须实际调用 y.call :)

all:
  hosts:
    AAA-BAT1:
      Fabric: AAA
      Role: BGW
      Type de materiel: N9K-C9YTR
      emplacement:
        Hauteur: '20'
        Salle: '1'
        Travee: '4'
    BBB-BAT2:
      Fabric: BBB
      Role: SP
      Type de materiel: N9K-C9YTR
      emplacement:
        Hauteur: '20'
        Salle: '1'
        Travee: '4'

阅读起来不是非常优雅,但它不会诉诸递归来定义应该自己提供递归的 y 组合器 ^^