1

Swift 中的柯里化

新手上路的学习笔记,如有错误还望指出,不胜感激。

上集:理论预备

在学习柯里化的过程中接触到了三个有趣的概念,在此和各位分享一下。

偏函数 (Partial Function)

偏函数是只对函数定义域的一个子集进行定义的函数,是一个数学概念。

偏函数定义如下:

从输入值集合 X 到可能的输出值集合 Y 的函数 f (记作f:X→Y) 是 X 和 Y 的关系,若 f 满足多个输入可以映射到一个输出,但一个输入不能映射到多个输出,则为偏函数。

换句话说,定义域 X 中可能存在某些值,在值域 Y 中没有对应的值。从定义来看,偏函数是函数的超集。也就是说,函数都是偏函数,但偏函数不都是函数。

偏函数应用 (Partial Application || Partial Function Application)

上面说的概念是数学中的概念,和我们将要接触的内容无关。和我们关系比较大的是偏函数应用 (Partial Application)。

偏函数应用的定义如下:

在计算机科学领域,偏函数应用是指通过固定原函数的一部分参数生成新函数的过程,新函数的参数数目少于原函数。

通过数学公式演示一下,比如原函数 f 有三个参数: f:(X×Y×Z)→N ,通过绑定第一个参数 X ,我们可以得到一个新的函数: partial(f):(Y×Z)→N

编程中的偏函数,比如廖雪峰老师在偏函数中提及的 functools 模块的用法,其实就是偏函数应用,又叫局部函数应用,有道词典翻译为切一刀在此表过不提。

为避免混淆,下文中提及的偏函数均为编程中的偏函数应用 (Partial Application)。

偏函数有点像是给函数的参数设置默认值一样,不过偏函数更加灵活。比如下面这段 c 语言的代码中,foo23 函数就是 foo 函数的偏函数,参数 b 的值被绑定为 23

int foo(int a, int b, int c) {
  return a + b + c;
}

int foo23(int a, int c) {
  return foo(a, 23, c);
}

柯里化

柯里化和偏函数有些关系,但是是两个完全不同的概念。

柯里化定义如下:

柯里化(英语:Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

通过数学公式来简单的演示一下,原始的函数是一个普通的函数:f:(X×Y) → Z ,柯里化之后函数变成了:curry(f):X → (Y→Z)

我们用几段 js 代码演示一下柯里化的过程。首先先看一个普通的函数 foo ,返回参数的平方:

var foo = function(a) {
    return a * a;
}

假设我们的函数都只能有一个参数,那么可以用下面的方式模拟出一个多参数函数:

var foo = function(a) {
    return function(b) {
        return a * a + b * b;
    }
}

我们可以这样调用 foo(3)(4)

柯里化解决的问题是:将多参数函数转变为一系列单参数函数的链式调用。

柯里化背后的基本想法是函数可以局部应用,意思是一些参数值可以在函数调用之前被指定,并且返回一个新的函数。也就是偏函数的基本思想。

小结

可以看到,柯里化将函数进行了~~肢解~~拆分,这样我们可以很容易的实现偏函数。比如:

var foo = function(a) {
    return function(b) {
        return a * a + b * b;
    }
}
var foo3 = foo(3);
foo3(4);

这就出来了。函数 foo3 就是 foo 函数的偏函数。

简单总结一下偏函数和柯里化以及两者的关系:

  • 偏函数应用:固定原函数的几个参数值,从而得到一个新的函数。
  • 函数柯里化:一种使用匿名单参数函数来实现多参数函数的方法。
  • 关系:柯里化能够轻松的实现某些偏函数应用。

中集:柯里化与偏函数

前面扯了很多有的没的,接下来我们来看看在 Swift 中柯里化。

节目预告:下面的例子极为简单,让各位见笑了。希望通过这些最简单的例子演示柯里化最关键的部分。

传统手段:老老实实传参数

我们先来写个最简单的东西好了:计数器,每次运算后+1即可:

func add(a:Int) -> Int{
    return a + 1;
}

var a = 0

a = add(a)   // 1
a = add(a)   // 2

但是每次都+1,步子有点小了。有时我们可能也需要+2,+3,+4,那么简单,我们把需要加的步长放在参数里就行:

func add(a:Int, b:Int) -> Int{
    return a + b ;
}

var a = 0

a = add(a, 1)   // 1
a = add(a, 2)   // 3

似乎没什么问题。那么问题来了:我大部分时间都只要+1啊,我可能只是100次调用有90次要+1,为了剩下的那10次,每行代码都要多个参数,是不是麻烦了点?OK嫌麻烦我们可以通过设置默认值的方式解决。

进阶手段:可以设置默认值

在函数定义的时候我们就给它定好默认值是1,那不就得了:

func add(a:Int, b:Int=1) -> Int{
    return a + b ;
}

var a = 0

a = add(a)   // 1
a = add(a, b:2)   // 3

如果你要+1,行,你别写参数我给你调好了自动+1;如果你要+2+3,行,爱加几加几,自己写到参数里去。

似乎没什么问题。那么问题来了:我有一半的时间要+1,有一半的时间要+2,还有些时间+3+4,怎么玩?

唉可真是磨人的小妖精。

柯学手段:通过柯里化实现

和前面的例子不一样的是,柯里化的函数返回一个新的函数而不是计算后的结果。计算后的结果要通过返回的新函数计算获得:

func add(b:Int)(a:Int) -> Int{
    return a + b ;
}

let addOne = add(1)
let addTwo = add(2)

var a = 0

a = addOne(a: a)    // 1
a = addTwo(a: a)    // 3

在这个例子里,我们生成两个新的函数:addOneaddTwo 分别进行+1和+2操作。可以看到,通过柯里化实现偏函数是十分方便的,一切都顺其自然,水到渠成。

通过这个例子可以看出,柯里化是偏函数的方法论,偏函数是柯里化背后的~~男人~~思想。

下集:实例方法与柯里化

实际上,Swift 中的实例方法也是柯里化方法,你可以传个实例作为第一个参数。

我们还就和计数器杠上了,再次以计数器为例:

class Counter {
    var b: Int = 1

    func add(a:Int) -> Int{
        return a + b ;
    }
}

我们可以初始化实例对象然后来调用:

let counter = Counter()
var a = counter.add(1)  // 2

我们也可以这样做:

let add = Counter.add   // Function
let counter = Counter()
var a = add(counter)(1) // 2

这两个是完全等价的。

有点神奇啊,不是吗?注意, let add = Counter.add 这个定义后面没有小括号。也就是说,我们并没有调用它,只是指向它,像一个指针一样指了过去。

我们可以按住 option 键查看一下 add 的类型:let add: Counter -> (Int) -> Int 。它的参数是一个 Counter 类型的实例,返回的是另一个新函数,这个新函数的参数类型是 Int ,返回的类型也是 Int ,和类里定义的函数类型是一样的。

看起来好像挺有趣的,不过问题来了:有啥用呢?

这个问题我还没办法回答,因为我也是刚刚接触这部分内容。我只能做一名知识的搬运工了。

Instance Methods are Curried Functions in Swift 这篇文章里,作者举了一个例子:用 Target-Action 模式实现一个 Control

protocol TargetAction {
    func performAction()
}

struct TargetActionWrapper<T: AnyObject> : TargetAction {
    weak var target: T?
    let action: (T) -> () -> ()

    func performAction() -> () {
        if let t = target {
            action(t)()
        }
    }
}

enum ControlEvent {
    case TouchUpInside
    case ValueChanged
    // ...
}

class Control {
    var actions = [ControlEvent: TargetAction]()

    func setTarget<T: AnyObject>(target: T, action: (T) -> () -> (), controlEvent: ControlEvent) {
        actions[controlEvent] = TargetActionWrapper(target: target, action: action)
    }

    func removeTargetForControlEvent(controlEvent: ControlEvent) {
        actions[controlEvent] = nil
    }

    func performActionForControlEvent(controlEvent: ControlEvent) {
        actions[controlEvent]?.performAction()
    }
}

用法和平时没什么两样:

class MyViewController {
    let button = Control()

    func viewDidLoad() {
        button.setTarget(self, action: MyViewController.onButtonTap, controlEvent: .TouchUpInside)
    }

    func onButtonTap() {
        println("Button was tapped")
    }

下集的下集:其他

由于没有在实战中用过,暂时不对柯里化做太多评价。就目前的了解来看,它可以让我们很方便的对函数进行局部调用,让代码更加灵活,语意更加清晰。

如果想感受一下原汁原味的柯里化,不妨接触一下 Haskell 这门语言。 Haskell 中多参数函数都是通过柯里化实现的。它的作者是 Haskell Brooks Curry,是的就是柯里化 (Curring) 的命名者 (不过并不是他发明的)。

使用 Mac 的朋友可以下载 Haskell Platform for Mac ,然后通过 ghci 命令试一试 Haskell 的代码。

当然也有反柯里化,感兴趣的同学可以继续了解一下:)


References


callmewhy
970 声望132 粉丝