U 组合子
我在这里聚会迟到了,但我很感兴趣并花了一些时间研究如何用 Lisp 家族语言,特别是 Racket,并认为其他人可能会感兴趣。
我怀疑那里有很多关于这方面的信息,但是现在很难搜索任何看起来像“*-combinator”的东西(即使现在我正在创办一组名为“部件集成”的公司和等等)。
正如您所说,您可以使用 Y 组合器来执行此操作,但我不想这样做,因为我发现 Y 是我一次可以理解几个小时的东西,然后我必须全部工作又出来了。但事实证明,您可以使用更简单的东西:U 组合器。这似乎比 Y 更难搜索,但这里有一段关于它的引述:
在编程语言理论中,U 组合子 U 是将其参数应用于其参数的数学函数;即 U(f) = f(f),或者等效地,U = λ f 。 f(f)。
自应用程序允许在 λ 演算中模拟递归,这意味着 U 组合器可以实现通用计算。 (U 组合器实际上比更著名的定点 Y 组合器更原始。)
表达式 U(U),读作 U of U,是最小的非终止程序,[...]。
(来自here的文字,不幸的是,除了这句话之外,这不是一个关于U组合子的网站。)
先决条件
以下所有代码示例都在 Racket 中。宏当然是特定于 Racket 的。要使宏工作,您需要syntax-parse via:
(require (for-syntax syntax/parse))
但是请注意,我对syntax-parse 的使用是极端天真的:我实际上只是一个解冻的 CL 穴居人,假装了解 Racket 的宏系统。
另外注意我并没有无情地将所有东西都变成λ:这段代码中有lets,使用了多个值包括let-values、(define (f ...) ...)等等。
两个版本的 U
第一个版本的 U 很明显:
(define (U f)
(f f))
但这会遇到应用顺序语言的一些问题,默认情况下是 Racket。为了避免这种情况,我们可以假设(f f) 将成为一个函数,并将该表单包装在另一个函数中以延迟其评估直到需要它:这是您必须在应用程序中为 Y 做的标准技巧-订购语言也是如此。我只会在必要时使用 applicative-order U,所以我会给它一个不同的名字:
(define (U/ao f)
(λ args (apply (f f) args)))
另请注意,我允许多个参数,而不是做纯λ演算的事情。
使用U构造递归函数
为此,我们做了一个与 Y 类似的技巧:编写一个函数,如果给定一个处理递归情况的函数作为参数,它将返回一个递归函数。显然我会使用斐波那契函数作为规范递归函数。
所以,考虑一下这个问题:
(define fibber
(λ (f)
(λ (n)
(if (<= n 2)
1
(+ ((U f) (- n 1))
((U f) (- n 2)))))))
这是一个函数,如果给定另一个函数 U 计算较小的斐波那契数,它将返回一个函数,该函数将计算 n 的斐波那契数。
也就是说,U这个函数就是斐波那契函数!
我们可以测试一下:
> (define fibonacci (U fibber))
> (fibonacci 10)
55
那太好了。
将 U 包装在宏中
因此,要隐藏所有这些,首先要做的是删除递归中对U 的显式调用。我们可以将它们完全从内部函数中提升出来:
(define fibber/broken
(λ (f)
(let ([fib (U f)])
(λ (n)
(if (<= n 2)
1
(+ (fib (- n 1))
(fib (- n 2))))))))
不要尝试计算 U :它会无限递归,因为 (U fibber/broken) -> (fibber/broken fibber/broken) 而这涉及计算 (U fibber/broken),我们注定要失败。
我们可以使用U/ao:
(define fibber
(λ (f)
(let ([fib (U/ao f)])
(λ (n)
(if (<= n 2)
1
(+ (fib (- n 1))
(fib (- n 2))))))))
这一切都很好((U fibber) 10) 是55(并终止!)。
这就是编写宏所需的全部内容:
(define-syntax (with-recursive-binding stx)
(syntax-parse stx
[(_ (name:id value:expr) form ...+)
#'(let ([name (U (λ (f)
(let ([name (U/ao f)])
value)))])
form ...)]))
这很好用:
(with-recursive-binding (fib (λ (n)
(if (<= n 2)
1
(+ (fib (- n 1))
(fib (- n 2))))))
(fib 10))
关于绑定的警告
这里很明显的一点是,这个宏构造了 两个 绑定:外部绑定和同名的内部绑定。而且这些在eq?的意义上并不绑定到同一个函数:
(with-recursive-binding (ts (λ (it)
(eq? ts it)))
(ts ts))
是#f。这仅在绑定可以改变的语言中很重要:换句话说,一种带有赋值的语言。外部绑定和内部绑定,除非它们已经被改变,否则它们是相同的函数作为函数:它们为它们的参数的所有值计算相同的值。事实上,很难看出eq? 在没有赋值的语言中会起到什么作用。
此警告也适用于下面。
用于多种功能的两个版本的 U
U, U*, 对许多函数的明显推广是 U*(f1, ..., fn) 是元组 (f1(f1, ..., fn), f2(f1, ... , fn), ...)。在 Racket 中表达这一点的一种好方法是使用多个值:
(define (U* . fs)
(apply values (map (λ (f)
(apply f fs))
fs)))
我们也需要应用顺序:
(define (U*/ao . fs)
(apply values (map (λ (f)
(λ args (apply (apply f fs) args)))
fs)))
请注意,U* 是 U 的真正概括:(U f) 和 (U* f) 是相同的。
使用U*构造互递归函数
我将使用一对微不足道的函数:
- 如果对象是 cons 并且其 car 和 cdr 是数字对象,则该对象是 数字树;
- 一个对象是一个数字对象,如果它是一个数字,或者它是一个数字树。
所以我们可以定义 'maker' 函数(使用 '-er' 约定:生成 x 的函数是 xer,或者,如果 x 里面有连字符,一个 x-er) 会产生合适的功能:
(define numeric-tree-er
(λ (nter noer)
(λ (o)
(let-values ([(nt? no?) (U* nter noer)])
(and (cons? o)
(no? (car o))
(no? (cdr o)))))))
(define numeric-object-er
(λ (nter noer)
(λ (o)
(let-values ([(nt? no?) (U* nter noer)])
(cond
[(number? o) #t]
[(cons? o) (nt? o)]
[else #f])))))
请注意,对于这两种情况,我稍微提高了对 U* 的调用,只是为了使对 U* 的适当值的调用不那么不透明。
这很有效:
(define-values (numeric-tree? numeric-object?)
(U* numeric-tree-er numeric-object-er))
现在:
> (numeric-tree? 1)
#f
> (numeric-object? 1)
#t
> (numeric-tree? '(1 . 2))
#t
> (numeric-tree? '(1 2 . (3 4)))
#f
将 U* 包装在宏中
当我们对U* 进行内部调用时,会出现与之前相同的问题,结果相同:我们需要使用U*/ao。此外,宏变得更加毛茸茸,我对自己如此轻松地做对感到有点惊讶。这在概念上并不难:对我来说,模式匹配的工作并不明显。
(define-syntax (with-recursive-bindings stx)
(syntax-parse stx
[(_ ((name:id value:expr) ...) form ...+)
#:fail-when (check-duplicate-identifier (syntax->list #'(name ...)))
"duplicate variable name"
(with-syntax ([(argname ...) (generate-temporaries #'(name ...))])
#'(let-values
([(name ...) (U* (λ (argname ...)
(let-values ([(name ...)
(U*/ao argname ...)])
value)) ...)])
form ...))]))
现在,在一阵火花中,我们可以写:
(with-recursive-bindings ((numeric-tree?
(λ (o)
(and (cons? o)
(numeric-object? (car o))
(numeric-object? (cdr o)))))
(numeric-object?
(λ (o)
(cond [(number? o) #t]
[(cons? o) (numeric-tree? o)]
[else #f]))))
(numeric-tree? '(1 2 3 (4 (5 . 6) . 7) . 8)))
并获得#t。
正如我所说,我确信有众所周知的更好的方法可以做到这一点,但我认为这很有趣,不会输。