SKI Combinator Calculus
本文借助 Codewars 一道题目 Sure, but can you SKI? (I) 来简单学习 SKI。
参考 wiki。
SKI
SKI 组合子计算 是一个计算系统,和lambda演算一样,是图灵完备的。
SKI 顾名思义,由三种基本组合子构成,计算规则如下:
I x = xK x y = xS x y z = x z (y z)
很明显,I就是id :: a -> a,K就是const :: a -> b -> a,只有S看似复杂一些,S用来实现“函数应用”。
需要指出,
S K _ x = K x (_ x) = x = I x,所以可以使用SKK来代替I,可见I不是必须的,只是一个语法糖,所以SKI也称作SK演算。
与lambda演算转换
SKI 演算只使用这三种组合子,所以是一种组合子风格的计算系统。将lambda演算转换为SKI,关键就是将lambda演算写成point-free风格,消去参数。
显然,这种转换并不是唯一的,或许有必要追求所谓最短转换,不过在此仅介绍一种通用的转换方法:
\x -> x:写作I\x -> A:其中A不含x,直接写作K A;\x -> A x:其中A不含x,直接写作A;\x -> A B:转化为\x -> S (\x -> A) (\x -> B) x。
按照这种通用思路,可以将任意lambda演算转换为SKI,不过这种方法的转化可能是指数增长的,非常可怕。
下面举一些例子来说明:
1 | rev :: a -> (a -> b) -> b |
1 | comp :: SKI ((b -> c) -> (a -> b) -> (a -> c)) |
1 | flip' :: SKI ((a -> b -> c) -> (b -> a -> c)) |
从
flip就可以看出来,同一个lambda表达式可以有非常多等价的SKI表达,不同的抽象方法可能得到长得完全不一样的表达式,这或许就是优化空间,笑
1 | rotr :: SKI (a -> (c -> a -> b) -> c -> b) |
1 | rotv :: SKI (a -> b -> (a -> b -> c) -> c) |
1 | join :: SKI ((a -> a -> b) -> a -> b) |
上面几个例子都是用于控制函数参数位置的组合子,使用他们可以更方便的控制更复杂的结构。
下面以Bool类型为例子,说明使用SKI实现lambda演算版本的布尔类型。
首先介绍Church encoding版本的Bool类型:
1 | type Bool' a = a -> a -> a |
可以看出,Church encoding版本的Bool类型本质上是位置选择函数,选择第一个位置还是第二个,所以我们可以借助上面实现的各种参数位置的控制函数来实现下面的函数。
当然这是因为任何一个二元类型A的计算A -> r都同构于r -> r -> r,这放到同构和Church encoding专题再详细讨论。
1 | true :: SKI (Bool' a) |
比较有意思的是,根据不同层次的抽象,写出来的代码复杂度千差万别。
如果只是简单的将true和false当作两个位置选择函数,然后每次将完整的参数全部带入然后化简,比如and = \x y a b -> x (y a b) b,那就太复杂了,实际上借助我们对布尔运算的先验知识,可以很容易写出下面的等价描述:
and = \x y -> x y For = \x y -> x T yxor = \x y -> x (not y) y
有趣的是,还可以借助先验知识继续化简:
and = \x -> x I (K F)or = \x -> x T Ixor = \x -> x not I
而这两者之间的互相转换,仅仅只有SKI的信息应该是无法完成的,换言之,这二者的等价依赖Bool'这个类型。(或许我太菜了没看出来)
SKI 的意义与价值
不懂,或许未来就懂了,再回来填坑。
目前来看,SKI是一个足够小的规则集,是图灵完备,问题是这玩意儿和lambda演算比有啥优势吗,人家lambda演算也只有两条基本规则阿,而且比SKI方便表达多了。或许主要魅力在于“组合子”?point-free? 不懂,有屁用阿……