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 main

func main() {
    func(a int) int {
        return a + 1

    }(2)

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

    add1(2)
}

高阶函数(High Order)

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

  • 接受一个或多个函数作为输入
  • 输出一个函数
package main

type Fn func(int)int

func 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 + 1

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

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

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

type Fn func(int)int

add1 := 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)int

add1 := 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)int

add1 := 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)int

add1 := 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) int

    func 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) int

    func 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) int
    func() G {
        return func(n int) int {
            if n <= 1 {
                return 1
            }
            return n * fact(n-1)
        }
    }()

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

    type G func(int) int

    func(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) int

    func(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) int
    type X func(X) G

    func 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) int
    type X func(X) G

    fn := 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) int
    type X func(X) G

    fact := 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) G

func 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) G

func 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 main

type G func(int) int
type X func(X) G
type F func(G) G

func 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) G

func 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) G

func 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的精妙演算可以大开眼界,推导过程也是一项很好的思维训练。

你可能感兴趣的:(Y-Combinator推导的Golang描述)