• 对函数式编程的一点理解
    • 小结

    对函数式编程的一点理解

    虽然有不少对“函数式编程”的解释,但我没有遇到让我满意的。我不是说解释要多么的具体、全面,或者让我立马会使用、变得很厉害,我就想知道它是怎么一回事儿。

    作者:@nixzhu


    先考虑一个过程。假设您性别男,爱好女。有位优雅的女士愿意与您做爱,不过这位女士对做爱有些要求:前戏至少要15分钟,性交要求在3到5分钟之间,最后要抱抱30分钟,说些情话。

    如果我们用程序来描述上面的过程,大概如下:

    1. let foreplayMinutes = 20
    2. let sexualIntercourseMinutes = 5
    3. let hugMinutes = 40
    4. let smallTalk = true
    5. if foreplayMinutes >= 15 {
    6. if sexualIntercourseMinutes >= 3 && sexualIntercourseMinutes <= 5 {
    7. if hugMinutes >= 30 && smallTalk {
    8. print("Satisfied!")
    9. }
    10. } else {
    11. print("Not satisfied!")
    12. }
    13. } else {
    14. print("Not satisfied!")
    15. }

    虽然这个过程比较简单,但那是因为我们做了简化,不然会变得少儿不宜。在真实场景下,我们会将明显可独立的部分抽象成函数,帮助我们更好地理解整个过程,于是变为:

    1. if foreplay(foreplayMinutes) {
    2. if sexualIntercourse(sexualIntercourseMinutes) {
    3. if hug(hugMinutes, smallTalk) {
    4. print("Satisfied!")
    5. } else {
    6. print("Not satisfied!")
    7. }
    8. } else {
    9. print("Not satisfied!")
    10. }
    11. } else {
    12. print("Not satisfied!")
    13. }

    其中foreplaysexualIntercoursehug都是函数,它们的实现大概如下:

    1. func foreplay(_ minutes: Int) -> Bool {
    2. return minutes >= 15 ? true : false
    3. }
    4. func sexualIntercourse(_ minutes: Int) -> Bool {
    5. return (minutes >= 3 && minutes <= 5) ? true : false
    6. }
    7. func hug(_ minutes: Int, _ smallTalk: Bool) -> Bool {
    8. return ((minutes >= 30) ? true : false) && smallTalk
    9. }

    因为要按照女士的要求做爱,所以这三个子过程的顺序并不能颠倒,但目前这三个函数并没有体现出顺序。

    我们在做的就是所谓的“编程”,那什么是“函数式编程”?

    先来分析一下最上面的过程:

    1. 首先我们定义了一些输入,如foreplayMinutes等;
    2. 然后我们(主要是您)先执行 foreplay,当这个过程符合要求后才进行下一步;
    3. 若三个子过程都满足要求,女士才会满足,中间某个过程一旦不满足,那就不可能满足了。

    再简化一点说,即:有输入,过程有顺序,过程可能有输出。那什么是“函数式编程”?

    别慌,快了。

    其实函数式编程是一种更高层的抽象(类似于我们将“过程”抽象成“函数”):我们假设有个“高阶函数”,它能接受任意数据和一个函数(利用范型),并返回经过此函数处理的数据。我们命名此函数为bind

    1. func bind<A, B>(_ a: A?, _ f: (A) -> B?) -> B? {
    2. if let x = a {
    3. return f(x)
    4. } else {
    5. return nil
    6. }
    7. }

    因为函数不一定有返回值,所以返回值是可选的。注意这里的a参数不一定要是可选的,但是为了bind的通用性(后面会看出来),让其可选比较好。

    而为了在子过程中体现顺序,我们要为它们增加参数:

    1. func foreplay(minutes: Int) -> Bool? {
    2. return minutes >= 15 ? true : false
    3. }
    4. func sexualIntercourse(foreplaySatisfied: Bool, minutes: Int) -> Bool? {
    5. if foreplaySatisfied {
    6. return (minutes >= 3 && minutes <= 5) ? true : false
    7. } else {
    8. return nil
    9. }
    10. }
    11. func hug(sexualIntercourseSatisfied: Bool, minutes: Int, smallTalk: Bool) -> Bool? {
    12. if sexualIntercourseSatisfied {
    13. return (minutes >= 30 ? true : false) && smallTalk
    14. } else {
    15. return nil
    16. }
    17. }

    注意,除了增加判断参数外,为了能适用于bind,函数的返回值也变为可选了。

    由此,该怎么用bind来描述上面的过程呢?

    1. var satisfied: Bool = false
    2. satisfied = bind(foreplayMinutes, foreplay) ?? false
    3. satisfied = bind(satisfied, sexualIntercourse) ?? false //错误
    4. satisfied = bind(satisfied, hug) ?? false //错误
    5. if satisfied {
    6. print("Satisfied!")
    7. } else {
    8. print("Not satisfied!")
    9. }

    可惜我们不能这么写,因为函数sexualIntercoursehug都不只接受一个参数,无法适用于bind。那怎么办?

    我们可以使用柯里化(Currying)技术改造我们的函数。所谓柯里化,就是将接受多个参数的函数变换成接受前几个参数(Swift的实现并不限定为第一个)的函数,而此函数返回一个“接受余下参数并能返回结果的新函数”。虽然柯里化的介绍并不直观,但其实改造很容易。我们只需要将多个参数分割成两个部分即可,我们直接传递参数给第一部分就可生成新函数,如:

    1. func add(a: Int, b: Int) -> Int {
    2. return a + b
    3. }
    4. add(3, 4) // 7
    5. func add(a: Int)(b: Int) -> Int { // 等价于上面的 add 函数
    6. return a + b
    7. }
    8. let addThree = add(3) // addThree 是一个新的函数,其可接受一个参数
    9. addThree(4) // 7

    由此,我们改造sexualIntercoursehug如下:

    1. func sexualIntercourse(_ minutes: Int) -> (_ foreplaySatisfied: Bool) -> Bool? {
    2. //...
    3. }
    4. func hug(_ minutes: Int, _ smallTalk: Bool) -> (_ sexualIntercourseSatisfied: Bool) -> Bool? {
    5. //...
    6. }

    注意hug函数,表明了我们对多参数的分割可以在任意位置。事实上,我们也可以写为:

    1. func hug(_ minutes: Int) -> (_ smallTalk: Bool) -> (_ sexualIntercourseSatisfied: Bool) -> Bool? {
    2. //...
    3. }

    当然使用时会更加灵活。

    之后,我们就能使用这两个柯里化函数了:

    1. //...
    2. satisfied = bind(satisfied, sexualIntercourse(sexualIntercourseMinutes)) ?? false
    3. satisfied = bind(satisfied, hug(hugMinutes, smallTalk)) ?? false
    4. //...

    此时,我们用bind将我们的过程顺序链接起来了。不过bind的语法依然让人困惑,我们可以增加一个中缀操作符:

    1. precedencegroup BindPrecedence {
    2. higherThan: NilCoalescingPrecedence
    3. associativity: left
    4. }
    5. infix operator >>>: BindPrecedence
    6. func >>><A, B>(_ a: A?, _ f: (A) -> B?) -> B? {
    7. return bind(a, f)
    8. }

    然后对应的计算就变为:

    1. let satisfied = foreplayMinutes >>> foreplay >>> sexualIntercourse(sexualIntercourseMinutes) >>> hug(hugMinutes, smallTalk) ?? false

    变成一条链了,顺序非常清楚。当然,我们写成这样可能更好看:

    1. let satisfied = foreplay(foreplayMinutes) >>> sexualIntercourse(sexualIntercourseMinutes) >>> hug(hugMinutes, smallTalk) ?? false

    三个过程会更清楚。

    在函数式编程的思想下,借助柯里化技术与自定义操作符,我们就能做到这样的“高阶操作”。

    上面只是将数据和函数bind在一起处理,那若是其它“合成”情况,或更高阶呢?例如有某个函数接受两个函数作为参数并生成一个新的函数又会怎么样(完全不理会数据了)。只要发挥你的想象力,那函数式编程也就不再神秘。

    小结

    函数式编程就是将函数当作一种数据类型,或者从更高的层面去看待数据和函数,使其可传递、可生成、可组合、可分解。有了这样的高级抽象,我们对函数的理解会更深刻,我们思考时就能更加自由。

    有一点可能很关键,就是“形式”。虽然都是代码,完成的功能也一样,但形式的不同能反映我们思考的不同。语言可以影响思维早就被发现了,这也是类似于“新闻联播”这样的节目可以成为思想控制工具的原因。在生活中,独立思考的阻碍比你想象的要多得多。

    如果你已开始用 Swift 写代码,可以考虑在某些地方试用这样的抽象观点,必有益处。


    欢迎转载,但请一定注明出处! https://github.com/nixzhu/dev-blog