笔记: 关于 SKI 组合子及其实现

题叶

按照 Wiki, 组合子(Combinator)狭义的定义是:
https://wiki.haskell.org/Comb...

A function or definition with no free variables.

比如 \f -> \a -> \b -> f b a 定义的函数, 没有自由变量, 就是组合子.
参考这样的例子, Haskell 当中按照类型签名定义的函数都是这样写的.
大概 Haskell 里边基本上都是用着组合子在编程的, 而不是主流语言习惯意义的"函数".

SKI 组合子是更基础的组合子, 往前追溯到 Haskell 以前几十年的逻辑学的研究.
1924 苏联人发现的 SKI, 后来 Haskell Curry 的版本是 BCKW:
https://esolangs.org/wiki/Com...

这些基本的算子, 注意跟 Haskell 语法一样, 都是柯理化的函数, 从左边开始计算.

S f g x = f x (g x);
K x y = x;
I x = x;

写成 lambda 表达式的话:

S = λx.λy.λz.(xz)(yz)
K = λx.λy.x
I = λx.x

其中 I 可以用另两个表示, 也就是说最简的版本是 SK 组合子逻辑.

S K K x = I x = x

相比 SKI, 另一个版本更直观一点, 大概都能对应到 Haskell 里的一些函数,

B x y z = x (y z);
C x y z = x z y;
K x y = x;
W x y = x y y;

网上的说法, 本来这些组合子就跟 Haskell 当中有对应关系
https://stackoverflow.com/que...
比如一些常见的 Combinators 对应 Haskell 或者 js 中的函数.
https://gist.github.com/Avaq/...

关于组合子的详细的介绍, 推导, 数字表示, 具体看这个文章了
Fun with Combinators: https://doisinkidney.com/post...

更详细的版本要看 Wiki 了..
https://en.wikipedia.org/wiki...
lambda 表达式都是可以转化为 SK 组合子的表达式的,
而 lambda 表达式当中使用了自由变量, SK 组合子当中全部都是组合子函数.
以前用于研究的语言, Unlambda, Miranda, 程序内不是转化到组合子再执行的.
不过由于组合子本身表示的程序会很冗长, 到 GHC 就改变很多进行性能优化了.

有个视频把 Combinator 用的树的结构具体展示了一下
"An Introduction to Combinator Compilers and Graph Reduction Machines" by David Graunke
https://www.youtube.com/watch...

Wolfram 的演讲视频, 中间涉及到很多 Combinators, 包括 SKI 原作者的历史
"Combinators: A 100-Year Celebration"
https://www.youtube.com/watch...

代码实现

有翻到别人的实现, 具体代码我还没能看懂...
https://github.com/ngzhian/ski

有翻到一个比较直观的版本, 某大佬实现的 combinator 的示例程序,
https://crypto.stanford.edu/~...

true = \x y -> x
false = \x y -> y
0 = \f x -> x
1 = \f x -> f x
succ = \n f x -> f(n f x)
pred = \n f x -> n(\g h -> h (g f)) (\u -> x) (\u ->u)
mul = \m n f -> m(n f)
is0 = \n -> n (\x -> false) true
Y = \f -> (\x -> x x)(\x -> f(x x))
fact = Y(\f n -> (is0 n) 1 (mul n (f (pred n))))
main = fact (succ (succ (succ 1)))  -- Compute 4!

对应到 SK 组合子结构...

s(k(s(skk)(skk)))(s(k(ss(k(s(skk)(skk)))))k)(s(k(s(k(s(s(s(s(skk)(k(k(sk))))(kk))(sk))))(s(s(ks)k))))(s(k(ss(k(s(k(s(k(s(k(ss(k(sk))))))(s(k(s(k(ss(kk)))k)))))(s(k(ss(k(s(k(s(k(s(k(s(k(s(skk)))k))))(s(skk))))k))))k)))))k))(s(s(ks)k)(s(s(ks)k)(s(s(ks)k)(skk))))uz

具体实现还是没看懂..
大致意义, 以前也听到过说 church number 这种基于 lambda 的数字表示,
比较抽象, 这次看到真的基于这种方式表达的代码实现了, 开始相信了.
理论上 SK 组合子有着图灵等价的能力(?), 应该是能表示任何的算法的.

unlambda 是一门很早的基于 SK(I) 组合子实现的语言,
http://www.madore.org/~david/...
这个名字 unlambda 是因为里边用了 SK 代替了 lambda 表达式.
就像前面说的组合子的用法, "everything is a function".
代码就显得极其晦涩了, 我就有点奇怪为什么作者一点圆括号都不用...

对应上边的用函数表达条件表达式 true, false 的写法, 在 lambda calculus 也有,
https://medium.com/@marinalim...
但是这个在理解上也要注意, 涉及到 lazy evaluation 的概念了,
http://augustss.blogspot.com/...
if cond t-branch f-branch 普通函数的版本, 所有参数都被求值的,
但作为条件表达式, 比如在 JavaScript, 明显只有其中一个表达式被求值,
在 strict evaluation 的语言当中, 基本上就是这样了, 参数会先求值掉, 再调用函数,
而这种条件表达式, 就需要语言内部或者 macro 提供能力, 来对分支进行求值.
在 Haskell 这样的语言, 却结果 lazy evaluation 能统一地实现该功能.
其中 branch 的代码表示作为 chunk 存在, 真的命中分支了, 才进行展开求值.
lambda calculus 和组合子这种数学化的表示, 应该也是对应 lazy 的版本.

其他

还有个 paper 详细解释了一下用组合子模拟数值和计算的具体过程.
Combinatory Logic: From Philosophy and Mathematics to Computer Science
以及 lambda 写法 https://dotink.co/posts/lambda/

Combinator 在 JavaScript 提供抽象能力的一些展示.
https://codeburst.io/combinat...
这个例子透露出来, Combinator 提供了强大的抽象能力,
我们平时编写程序, 函数内部没有对 free variables 的约束, 可以随便引用,
随便引用也就意味着这个函数的依赖, 甚至隐形依赖, 就会很多,
而基于 Combinator 不能使用 free variables, 所有依赖就只能从函数参数进入,
这样, 基于函数参数就有很多控制的手段了, 就有强大的抽象能力.
这样做伴随的问题就是抽象能力很强, 代码容易晦涩.


后续新闻中出现的 Binary Lambda Calculus (BLC) 实现:
https://www.ioccc.org/2012/tr...

阅读 122

题叶
ClojureScript 爱好者.

ClojureScript 爱好者.

17.2k 声望
2.6k 粉丝
0 条评论
你知道吗?

ClojureScript 爱好者.

17.2k 声望
2.6k 粉丝
宣传栏