当前位置: 首页 > news >正文

Y-Combinator推导的Golang描述

缘起

在做计算的本质指称语义的时候,遇到了需要在Python匿名递归调用。Python的lambda表达式本身不支持,需要借助 Y-Combinator技术实现。于是研究了下Y-Combinator。中文世界了很多Blog介绍和推导Y-Combinator的文章。然而大部分的文章都省略了推导的关键步骤和推导的依据。仿佛读者都默认已经懂得Y-Combinator了。最后我在Youtube上找到了Ruby大神Jim WeirichRuby Conf 12 上的topicY Not- Adventures in Functional Programming。大神就是大神,step by step 介绍了lambda calculusY-Combinator。本文即对Jim Weirich推导过程的一个梳理。

Jim Weirich 使用了 Ruby 进行演示。Y-Combinator 推导使用动态语言很方便。然而本文使用Golang语言进行描述。主要原因是作为静态语言,函数的入参和出参都需要声明类型。这样对于不熟悉匿名函数和Y-Combinator的人而言,可以直观的了解嵌套的匿名函数都是什么。不然直观的看 ruby,python或者js的Y-Combinator的推导过程,很容易被括号搞晕。

Y-Combinator

Y-Combinator又称之为Y组合算子。由Haskell B. Curry发现的。其定义为

Y = λf.(λx.(f (x x)) λx.(f (x x))) 

作用就是可以成为匿名函数的容器,让匿名函数进行递归调用。计算理论有图灵机Tuling machine, 递归函数[Recursive function] ,λ演算lambda calculus。lambda 演算里没有命名函数,因此想要在lambda演算里使用递归,就需要借助Y-Combinator

现在的编程语言多数都能命名,并且底层都是基于图灵机模型实现。与Y-Combinator和lambda calculus 没有关系。换言之,Y-Combinator有如屠龙术,对于日常的编程工作几乎无用。那么为什么还要介绍Y-Combinator

为了思维锻炼。

匿名函数(Anonymous function)

lambda calculus 是基于lambda表达式进行演示。其语法规约如下

语法 Syntax命名含义
x变量 Variable表示值
(λx. M)抽象 Abstraction定义抽象(函数, M 是 lambda 表达式
(M N)应用 Application函数调用 M N 都是lambda 表达式

lambda calculus中有三条重要的规则,alpha 变换 , beta 规约 和eta 变换 这些规则的应用在后面的Function Refactor使用中具体介绍。

在Golang中,lambda表达式可以用匿名函数表示。Golang 中匿名函数很简单,使用func声明。与正常函数不一样的就是没有名字。可以声明即调用,也可以声明后赋值给一个变量。

package mainfunc main() {func(a int) int {return a + 1}(2)add1 := func(a int) int {return a + 1}add1(2)
}

高阶函数(High Order)

普通函数的参数是变量或者表达式。高阶函数的定义是至少满足下面一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数
package maintype Fn func(int)intfunc main() {addN := func(n int) Fn{return func(a int) int {return a + n}}makeAdd := func(f func(n int) Fn, n int) Fn {return f(n)}add1 := makeAdd(addN, 1)add1(2) // 3}

makeAdd是一个高阶函数,它的入参有函数,同时也返回一个函数。

运算技巧 Fucntion Reactor

匿名函数有下面几个技巧,推导Y-Combinator即需要用到。

Tennent Correspondence Principle

Tennent Correspondence Principle指任何lambda 表达式都可以使用一个无参的lambda表达式包裹并立即调用。其形式是a + 1 <=> (lambda : a + 1)()

a <=> b 表示 a 等价 b


a := 1
expr := 1 + 1expr := func() int {return a + 1
}()

lambda表达式即可以是求值表达式,可以是匿名函数,本文这两个词等价

这个技巧也可以作用匿名函数表达式,下面两种写法的add1是等价的。
第二个add1在外层包裹了一个匿名函数,但是这个匿名函数立即调用了,返回了一个函数,返回的函数正式第一个add1的声明

type Fn func(int)intadd1 := func(a int) int{return a + 1
}add1 := func() Fn {return func(a int) int{return a + 1}
}()

Introduce Binding

Tennent Correspondence Principle类似,可以给表达式包裹的时候指定一个参数,这个参数在原表达式中不存在。其形式是lambda a: a + 1 <=> (lambda n: lambda a: a + 1)("any value")

相当于传入了一个无效参数。因为参数对原表达式无影响。

type Fn func(int)intadd1 := func(a int) int{return a + 1
}add1 := func(n interface{}) Fn {return func(a int) int{return a + 1}
}("任意值")

这个法则看似无用,但是是很多推导过程的中间环节。此外还有一种Rebinding绑定方法。其形式: lambda n: (lambda : lambda a + n)() <=> lambda x: (lambda n: lambda a + n)(x)

在多层嵌套的lambda表达式中。可以在中间嵌入一个匿名函数传递最外层的参数。第一个add1,中间有个立调用无参匿名函数。最内层使用了最外层的参数n,如果重命名最外层的n,那么最内层的也需要修改。

使用 Rebinding的方法就是将最外层的n通过中间无参立即调用的匿名函数传递进去。即第二个add1。这个转换的作用在于,如果将来需要提取最内层的匿名函数,其参数命名不会与最外层的耦合。


type Fn func(int)intadd1 := func(n int) Fn {return func() Fn {return func(a int) int{return a + n}}()
}add1 := func(x int) Fn {return func(n int) Fn {return func(a int) int{return a + n}}(x)
}
// fmt.Println(add1(1)(2))

Function Wrap

Function Wrap 也和Tennent Correspondence Principle相似。只不过它作用于它包裹的函数,其形式:// lambda a: a + 1 <=> lambda x: (lambda a: a + 1)(x)


add1 := func(x int) int{return (func(a int) int {return a + 1})(x)
}add1 := func(x int) int{return (func(a int) int {return a + 1})(x)
}
// fmt.Println(add1(2))

这里需要注意与Introduce Binding的区分, 一个是对包裹的函数本身立即调用,一个是对被包裹的函数进行调用

  1. lambda a: a + 1 <=> (lambda x: lambda a: a + 1)(x)
  2. lambda a: a + 1 <=> lambda x: (lambda a: a + 1)(x)

Inline Function

Inline Function顾名思义,就是把原来使用变量的地方直接用表达式替换。对于第一个 add := addN(1, add1)。使用 inline function的就是把 add1 变量消除,使用其函数体代入即可。即第二个add的形式。

type Fn func(int)intadd1 := func(n int) int {return 1 + n
}
addN := func(n int, add1 Fn) Fn{return add1
}
add := addN(1, add1)// addN := func(n int, add1 Fn) Fn{return add1
}
add := addN(1, func(n int) int {return 1 + n
})fmt.Println(add(2))

推导

掌握了上面四条Function Refactor 规则,就可以开始推导 Y-Combinator。推导过程大致分为三个过程。首先构造匿名函数,其次是匿名调用,最后是提取匿名函数。就像把大象放进冰箱的步骤一样简单。

构造匿名函数

Y-Combinator 的经典推导就是实现一个求阶乘函数的递归函数。求阶乘的递归函数如下:

    import "fmt"func fact(n int) int{if n <= 1{return 1}return n * fact(n - 1)}func main() {ans := fact(5)fmt.Println(ans)}

下面改写成匿名函数实现:

    type G func(int) intfunc main() {var fact G  // 声明 fact 变量,以便匿名函数里的 fact(n-1)引用fact = func(n int) int {if n <= 1 {return 1}return n * fact(n-1)}ans := fact(5)fmt.Println(ans)}

上面的代码看起来也像实现了匿名函数的递归调用。其实不然,声明fact的时候,匿名函数通过闭包可以访问,真正的匿名函数调用应该如下:

    (func(n int) int {if n <= 1 {return 1}return n * fact(n-1)})(10)

很不幸,编译器拒绝编译,因为找不到一个叫fact的函数。在现代编程语言中,将一个变量(标志符)与函数绑定,一般有三种方法:

  • 直接命名函数 如 func fact(){}
  • 赋值操作,即 var fact = func(){}
  • 参数绑定

前面两种我们都很熟悉,后面一种也不陌生。后者将一个函数的参数指定为函数,通过传递一个函数变量,在函数体内进行调用。即将参数和函数绑定。当然绑定既可以是入参,也可以是返回值的出参。

匿名函数递归调用

上面的匿名函数想要运行可以改下如下:

    type G func(G, int) intfunc main() {ans := (func(fact G, n int) int {if n <= 1 {return 1}return n * fact(fact, n-1)})(func(fact G, n int) int {if n <= 1 {return 1}return n * fact(fact, n-1)}, 5)fmt.Println(ans)}

上面的代码可以正常运行。其形式是 (lambda f, n: f(f, n))(lambda f, n: f(f, n))。匿名函数有两个参数,一个是类型为G的函数,第二个是 n。函数体的fact 来自参数。当匿名函数运行的时候,它的第一个参数就是它自己本身。一个函数调用自己本身,这就实现了递归调用。

匿名函数的提取

单参数匿名函数调用

将匿名函数作为参数传入到自身的参数之中以实现递归。这就是lambda calculus中的思想。通常lambda 表达式是单参数。通过一种叫柯里化 的技术可以实现。

下面进行推导。首先将观察单参数的匿名函数

    func(n int) int {if n <= 1 {return 1}return n * fact(n-1)}

利用技巧一,使用一个新的无参的匿名函数包裹上面的匿名函数

    type G func(int) intfunc() G {return func(n int) int {if n <= 1 {return 1}return n * fact(n-1)}}()

然后依据上面的思路,我们将匿名函数以外层的参数传递进去,即利用了技巧二

    type G func(int) intfunc(fact G) G {return func(n int) int {if n <= 1 {return 1}return n * fact(n-1)}}(fact)

此时再次回想前面匿名函数递归调用的方法,fact 就是其本身改下如下

    func(fact G) G {return func(n int) int {if n <= 1 {return 1}return n * fact(n-1)}}(func(n int) int {if n <= 1 {return 1}return n * fact(n-1)})

函数体内的问题解决了,但是参数里又出现了fact。因此不能直接传这个匿名函数,而是需要把外层的包裹函数开始当成参数,再次改下如下:

    type G func(int) intfunc(fact G) G {return func(n int) int {if n <= 1 {return 1}return n * fact(n-1)}}(func(fact G) G {return func(n int) int {if n <= 1 {return 1}return n * fact(n-1)}})

很不幸,依然无法编译。报错信息也很明显,最外层的函数的参数是一个 G,而自身是 func (G) G,两者类型不一致。以为要自身调用自身,因此最外层的函数定义为

type F func(F) G

再次改下 最外层的匿名函

    type G func(int) inttype X func(X) Gfunc main() {func(fact X) G {return func(n int) int {if n <= 1 {return 1}return n * fact(n-1)}}(func(fact X) G {return func(n int) int {if n <= 1 {return 1}return n * fact(n-1)}})}

依然很不幸,匿名函数调用不再报错。报错的是 n * fact(n-1) 错误原因是:

./main.go:13:13: invalid operation: n * fact(n - 1) (mismatched types int and G)
./main.go:13:21: cannot use n - 1 (type int) as type F in argument to fact

其实很好理解了,现在入参 fact的类型 type X func(X) G, 而与 n 相乘的应该是类型G。解决方法也很简单,既然 入参是X,而 X以自身为入参调用返回的结果就是G。那么上面的代码改下如下

    type G func(int) inttype X func(X) Gfn := func(fact X) G {return func(n int) int {if n <= 1 {return 1}return n * fact(fact)(n-1)}}(func(fact X) G {return func(n int) int {if n <= 1 {return 1}return n * fact(fact)(n-1)}})fn(5)

nice~ 现在可以正常运行了。我们实现了真正的匿名函数递归调用。代码没有声明fact, 但是我们却在函数调用中使用 fact。

实际上此时的 factX, 而不是 G。因此重命名为x。如下

    type G func(int) inttype X func(X) Gfact := func(x X) G {return func(n int) int {if n <= 1 {return 1}return n * x(x)(n-1)}}fx := fact(fact)fx(5)
单参数匿名函数提取
  1. 经过上面的步骤。距离目标完成了一半。下面的目标就是把上面的参数提取出来。观察 fx := fact(fact)。使用技巧一可以改成通用的函数:
    fact := func(x X) G {return func(n int) int {if n <= 1 {return 1}return n * x(x)(n-1)}}fn := func() G{return fact(fact)}()fn(5)
  1. 使用技巧二,将 fact 用外面的参数替代
    fact := func(x X) G {return func(n int) int {if n <= 1 {return 1}return n * x(x)(n-1)}}fn := func(x X) G{return x(x)}(fact)fn(5)
  1. 使用技巧四,将fact函数代入到 fn 表达式中,参数fact被消除。
fn := func(x X) G{return x(x)}(func(x X) G {return func(n int) int {if n <= 1 {return 1}return n * x(x)(n-1)}})fn(5)
  1. 下面的目标就是将 最内层的函数提取。使用技巧一包裹最内层的表达式。
    fn := func(x X) G {return x(x)}(func(x X) G {return func(n int) int {return (func() int {if n <= 1 {return 1}return n * x(x)(n-1)})()}})fn(5)
  1. 使用技巧二,将最内层的n由刚才包裹的无参匿名函数传递进去。这一步的作用是逐渐将n由外层传入。便于外层修改。
fn := func(x X) G {return x(x)}(func(x X) G {return func(n int) int {return (func(n int) int {if n <= 1 {return 1}return n * x(x)(n-1)})(n)}})fn(5)
  1. 接下来使用技巧一再对内层的匿名函数进行包裹。注意是最内层的匿名函数进行包裹,而不是对匿名汉调用后进行包裹,即 ( ()() )(n) 而不是 (()n)()
fn := func(x X) G {return x(x)}(func(x X) G {return func(n int) int {return (func() G {return func(n int) int {if n <= 1 {return 1}return n * x(x)(n-1)}}())(n)}})fn(5)
  1. 现在可以使用技巧二,传递一个参数,这个参数可以假想为我们需要的fact函数
    fn := func(x X) G {return x(x)}(func(x X) G {return func(n int) int {return (func(recursion_fact G) G {return func(n int) int {if n <= 1 {return 1}return n * recursion_fact(n-1)}}(recursion_fact))(n)}})fn(5)

此时是无法编译,由于 recursion_fact 就是目标分离函数,即 x(x), 并且x是最上层传递下来的,因此他们可以使用技巧二重新绑定,即替换。

    fn := func(x X) G {return x(x)}(func(x X) G {return func(n int) int {return (func(recursion_fact G) G {return func(n int) int {if n <= 1 {return 1}return n * recursion_fact(n-1)}}( x(x)))(n)}})fn(5)

现在可以编译成功。同时注意观察,最内层的匿名函数,和我们最初写的匿名函数一模一样了。接下来就只要把他作为整体再提取出去。

  1. 想要把最内层的匿名提取出去,根据上面的经验,还是依靠参数,而依靠参数,就需要往外层包裹。因此再次使用技巧一,将整个 fn 的最外层匿名函数进行包裹
    fn := func () G{return func(x X) G {return x(x)}(func(x X) G {return func(n int) int {return (func(recursion_fact G) G {return func(n int) int {if n <= 1 {return 1}return n * recursion_fact(n-1)}}(x(x)))(n)}})}()
  1. 技巧一就是为技巧二服务的,很自然使用技巧二
    fn := func (f interface{}) G{return func(x X) G {return x(x)}(func(x X) G {return func(n int) int {return (func(recursion_fact G) G {return func(n int) int {if n <= 1 {return 1}return n * recursion_fact(n-1)}}( x(x)))(n)}})}("任意值")
  1. 既然任意值都可以,那么就把我们最想提取出来的 匿名函数 recursion_fact 传递进去吧
type G func(int) int
type X func(X) G
type F func(G) Gfunc main() {fn := func (f F) G{return func(x X) G {return x(x)}(func(x X) G {return func(n int) int {return (func(recursion_fact G) G {return func(n int) int {if n <= 1 {return 1}return n * recursion_fact(n-1)}}( x(x)))(n)}})}(func(recursion_fact G) G {return func(n int) int {if n <= 1 {return 1}return n * recursion_fact(n-1)}})fn(5)
}
  1. 既然 f 是从外面传递,同时也是内层的阶乘函数,那么直接用 f 替换 recursion_fact 匿名函数即可。
type G func(int) int
type X func(X) G
type F func(G) Gfunc main() {fn := func(f F) G {return func(x X) G {return x(x)}(func(x X) G {return func(n int) int {return (f(x(x)))(n)}})}(func(recursion_fact G) G {return func(n int) int {if n <= 1 {return 1}return n * recursion_fact(n-1)}})fn(5)
}
  1. 目前为止,基本上已经推导完成。fn 中分为两部分。匿名函数定义部分,与匿名阶乘函数没有直接关系。匿名函数的参数,恰好是阶乘函数。重命名如下:
package maintype G func(int) int
type X func(X) G
type F func(G) Gfunc main() {g := func(recursion_fact G) G {return func(n int) int {if n <= 1 {return 1}return n * recursion_fact(n-1)}}Y := func(f F) G {return func(x X) G {return x(x)}(func(x X) G {return func(n int) int {return f(x(x))(n)}})}fn := Y(g)fn(5)
}

至此,Y-Combinator 已经推导完毕。下面是使用不动点的概念,可以把 x(x) 等价于 f(x(x))。

    g := func(recursion_fact G) G {return func(n int) int {if n <= 1 {return 1}return n * recursion_fact(n-1)}}Y := func(f F) G {return func(x X) G {return f(x(x))}(func(x X) G {return func(n int) int {return f(x(x))(n)}})}fn := Y(g)fn(5)

惰性求值

考虑与Y-Combinator的递归定义

Y = λf.(λx.(f (x x)) λx.(f (x x))) 

与我们上面的推导有一点点出入。可以直接写出golang的实现

type G func(int) int
type X func(X) G
type F func(G) Gfunc main() {g := func(g G) G {return func(n int) int {if n <= 1 {return 1}return n * g(n-1)}}Y := func(f F) G {return func(x X) G {return f(x(x))}(func(x X) G {return f(x(x))})}fn := Y(g)fn(5)
}

编译运行,程序会爆栈。fatal error: stack overflow。 因为在 Y 表达式的定义时候,f内的函数体是一个立即调用的定义。像Golang这样的语义不是惰性求值,而是严格求值,即会马上求值。当代入到函数体的时候,就会出现无穷的递归。因此会爆栈、解决办法就是再包裹一个匿名函数以惰性求值。

type G func(int) int
type X func(X) G
type F func(G) Gfunc main() {g := func(g G) G {return func(n int) int {if n <= 1 {return 1}return n * g(n-1)}}Y := func (f F) G {return func(x X) G {return f(x(x))}(func(x X) G {return func(n int) int {return f(x(x))(n)}})}fn := Y(g)fn(5)
}

延迟参数惰性求值的转换,再次对比Y-Combinator的定义和我们的推导。他们的形式上也几乎一样了。不同在于golang因为需要惰性求值,在 Y-Combinator的第二部分进行了一个匿名函数包裹而已。这种转换后的形式通常称之为 Z-Combinator。

总结

lambda calculus是一种计算模式,其与Turing machine ,Recursive function 三者是等价的。

lambda calculus中如果对匿名函数进行递归调用,需要用到 Y-Combinator组合算子。

Y-Combinator 的推导依赖支持高阶函数的语言,例如 python,golang,JavaScript等。推导过程需要掌握4个function refactoring的技巧。
golang python这些语言是严格求值(Eager evaluation)。而 Lisp,Haskell 这些是惰性求值(Lazy evaluation)。因此直接使用Y-Combinator的定义实现的代码,python和golang都需要处理后面作为参数调用的匿名函数体。这个转换后的通常称之为 Z-Combinator。

Y-Combinator 在日常编码工作中几乎没什么用,但是lambda calculus的精妙演算可以大开眼界,推导过程也是一项很好的思维训练。

最后编辑于:2025-06-15 09:57:57


喜欢的朋友记得点赞、收藏、关注哦!!!

http://www.lqws.cn/news/600571.html

相关文章:

  • Go语言的Map
  • 编写shell脚本扫描工具,扫描服务器开放了哪些端口(再尝试用python编写一个)
  • java web2(黑马)
  • 7.1_JAVA_其他
  • Excel
  • 【前端】vue工程环境配置
  • 洛谷P1379 八数码难题【A-star】
  • LangChain4j在Java企业应用中的实战指南-3
  • uniapp 中使用路由导航守卫,进行登录鉴权
  • css函数写个loading动画 | css预编译scss使用
  • MAC环境搭建SVN,并将TOMCAT集成到IDEA
  • 地震灾害的模拟
  • Springboot整合高德地图
  • filebeat收集日志到es
  • 大模型MCP技术之一句话安装Hadoop
  • 图神经网络(篇二)-基础知识
  • 安全左移(Shift Left Security):软件安全的演进之路
  • Badoo×亚矩云手机:社交约会革命的“云端心跳加速剂“
  • 计网学习笔记第1章 计算机网络体系结构(灰灰题库)
  • 微信小程序实现table表格
  • vue+three.js 加载模型,并让模型随航线飞行
  • 服务器种类与超融合
  • CSS 安装使用教程
  • mysql的自增id用完怎么办?
  • 【MobaXterm、Vim】使用合集1
  • 多容器应用与编排——AI教你学Docker
  • 单端输入转差分输出
  • ELK日志分析系统(filebeat+logstash+elasticsearch+kibana)
  • 学习字符串
  • AKAZE(Accelerated-KAZE)图像特征点检测算法详解和C++代码实现示例