函数式编程 : 一个程序猿进化的故事

[comment]: # 函数式编程 : 一个程序猿进化的故事

阿袁工作的第1天: 函数式编程的历史

阿袁中午和阿静一起吃午餐。阿袁说起他最近看的《艾伦·图灵传 如谜的解谜者》。
由于阿袁最近在学习Scala,所以关注了一下图灵传中关于函数式编程的一些历史。
关于函数式编程的故事,可以从1928年开始讲起:希尔伯特在当年的一个大会上,提出了他的问题:

  • 第一,数学是完备的吗?
    是不是每个命题都能证明或证伪。
  • 第二,数学是相容的吗?
    永远不会推出矛盾的命题?
  • 第三,可判定性问题:数学是可判定的吗?
    是否存在一个算法,可以应用于任何命题,然后自动给出该命题的真假?

希尔伯特的哲学企图是:每个问题的答案都将会是“是”。我想这个信念来自于对数学的神圣信仰。

不幸的是,在这同一个大会上,第一个问题就被否定了。一个年轻的捷克数学家,柯特·哥德尔的宣布,能够证明,
算术一定是不完备的:存在既不能证明,也不能证伪的命题。

注:欧几里得几何的五大公理并不是一个反例。欧几里得几何可以被一阶公理化为一个完备的系统。
(这句话啥意思?)我的理解是:公理是一个定义,或者说是不证自明的。

随后,哥德尔不完备定理的第二定理又否定了第二个命题:“数学是相容的吗?”

对于第三问题(可判定性问题),在1936年,丘奇(Alonzo Church)和艾伦·图灵分别证明了存在不可解的问题。
图灵提出的图灵机模型,而丘奇提出了一个基于lambda演算(lambda calculus)的模型,这两个模型被图灵证明是等价的。
图灵在图灵机的思想上,继续思考,逐步设计出早期的计算机(一个英国版的计算机,比冯诺依曼的计算机更早被建造出来。冯诺依曼对图灵机也是认可的。),并且考虑人工智能的问题。
而lambda演算概念,则被发扬光大成了函数式编程思想。
伟大的数学!

函数式编程是基于表达式(expression)的一种编程范式。
函数式编程将计算视为对数学函数的求值过程。

  • 函数式编程是:

    • 声明式编程(declarative programming),其含义是基于表达式(expression)。
    • 基于表达式的含义:表达式是用来求值的。
    • 倾向避免使用可变数据。
  • 面向对象编程的特点:

    • 命令式编程(imperative programming),其含义是基于陈述(statement)。
    • 基于陈述的含义:语句是用来执行的。

阿袁工作的第2天: 函数式编程:告别对象,迎接函数

阿袁和阿静中午又在一起,继续讨论函数式编程。
“我认为,我们可以把函数式编程理解成在做数学计算。这种编程风格是一种面向表达式(expression-oriented)风格。”,阿静慢慢地说道。
"我也是这么想的。所以,作为一个面向对象的程序员,我们先要把对象的概念舍去掉。"
“是啊,倒空一些,才能学习到新的知识。”
“我们怎么考虑class的作用呢?”
“在面向对象中,class的一个主要作用的封装。”
“那么,在函数式编程中,class的作用应该是对算法(函数)的分类了。”
“正解!我们做一个游戏,看看如果把一个面向对象的程序,变成面向表达式的程序。”
“好啊,我先用Scala写一个面向对象的例子。”

// 这个例子的主要功能是对一个List排序。
// 这是一个基于面向对象思想的实现。
object Main {

    // 一个支持排序的class。
    // 这个class,需要外部提供一个比较器。
    class ListSorter[T](a: List[T]) {
        def data: List[T] = a
        def sort(comparer: IComparer[T]): List[T] = {
            return data.sortWith(comparer.compare)
        }
    }

    // 我们为比较器定义一个interface,带一个比较函数compare。
    trait IComparer[T]{
        def compare(a: T, b: T): Boolean
    }

    // 这个一个具体的比较器,实现了比较器IComparer。
    class IntComparer extends IComparer[Int] {
        override def compare(a: Int, b: Int): Boolean = {
            return a < b
        }
    }
    
    // 测试一下
    def main(args: Array[String]): Unit = {
        val list = List(9,1,6,3,5)
        val sorter = new ListSorter[Int](list)
        // 在调用sort方法时,传入一个具体比较器对象。
        println(sorter.sort(new IntComparer()))
    }
}

在这个例子中,ListSorter需要外部提供一个比较方法。为了解决这个问题,面向对象的思路是:

  • 对外部功能,定义了一个接口。并在接口中,声明这个比较函数。
  • ListSorter的sort函数,通过接口来使用外部的比较方法。
  • 外部:定义了一个具体类,实现了这个接口。
  • 调用者:在调用ListSorter的sort函数时,传入一个具体类的对象。

“现在,我们的任务就是:把这个例子改成面向表达式的风格。”
“首先,把sort函数的输入参数comparer变成一个函数类型。”
“这样,我们就不需要IComparer,这个接口了。”
“IntComparer就可以从一个封装类,变成一个带比较函数的静态类。”

函数式编程的第一个例子:

// 这个例子的主要功能是对一个List排序。
// 这是一个基于面向表达式的实现。
object Main {
    // 一个支持排序的class。
    // 这个class,需要外部提供一个比较函数。
    class ListSorter[T](a: List[T]) {
        def data: List[T] = a
        def sort(f: (T, T) => Boolean): List[T] = {
            return data.sortWith(f)
        }
    }

    // 实现了一个比较函数。
    object IntComparer {
        def compare(a: Int, b: Int): Boolean = {
            return a < b
        }
    }

    def main(args: Array[String]): Unit = {
        val list = List(9,1,6,3,5)
        val sorter = new ListSorter[Int](list)

        // use function rather than object
        println(sorter.sort(IntComparer.compare))
        // use function with lambda expression
        println(sorter.sort( (a, b) => a < b ))
        // use function with underscore
        println(sorter.sort( _ < _ ))

        // fluent infix style
        println(sorter sort IntComparer.compare)
        // fluent infix style with lambda expression
        println(sorter sort {(a, b) => a < b})
        // fluent infix style with underscore
        println(sorter sort { _ < _ })
    }
}

注:这里面实现了多种风格。
lambda expression,可以看成匿名函数的实现方法。
underscore: underscore在scala中有多种含义。这里是一种匿名函数的实现,scala会根据上下文推测"_"的含义。
infix style: 可以看出,不需要"."了。

“太好了,我们向函数式编程迈出了第一步!”

阿袁工作的第3天: 函数式编程:再纯粹一些

“在昨天的例子中,我们还是实例化了ListSorter。”
“是啊,按照函数式编程的思想,我们需要把ListSorter的sort方法看成一个函数。”
“另外,我还学到了一点,在面向表达式风格中,不要写return。最后一条expression的结果就应该是函数的返回值。”
“嗯,好的,我们继续改改看。”
函数式编程的改进版:

// 这个例子的主要功能是对一个List排序。
// 这是一个基于面向表达式的实现。
// * Changed ListSorter as module
// * Do not use return
object Main {
    object ListSorter {
        def sort[T](a: List[T], f: (T, T) => Boolean): List[T] = {
            // Do not use return
            a.sortWith(f)
        }
    }

    object IntComparer {
        def compare(a: Int, b: Int): Boolean = {
            // Do not use return
            a < b
        }
    }

    def main(args: Array[String]): Unit = {
        val list = List(9,1,6,3,5)
        // use function rather than object
        println(ListSorter.sort(list, IntComparer.compare))
        // use function with lambda
        println(ListSorter.sort[Int](list, (a, b) => a < b))
        // use function with underscore
        println(ListSorter.sort[Int](list, _ < _))
    }
}

发现了吗? fluent infix style没有了。这是因为,infix操作支持有一个参数的函数。

阿袁工作的第4天: 函数式编程:卷积(currying)

“fluent infix style有点接近人类的语言,使用好的话,可以增加可读性。”
"但是,它也有个限制,只支持有一个参数的函数。"
“其实,卷积可以解决这个问题。"
"卷积?"
“给你举个例子。一般的函数是这样的。”

    def normalFunc(a: Int, b: Int, c:Int): Int = {
        a + b + c
    }

"而卷积函数变成这样,参数被分隔一个一个的。"

    def curriedFunc(a: Int)(b: Int)(c:Int): Int = {
        a + b + c
    }

"卷积的思想是: 每次只给函数的一个参数赋值。这样的一个主要用途是:局部函数(partial function application),
可以想象为把一个计算分成多个步骤计算(multiple stage computation)。这是调用的方法:"

        // Usage: Currying in partial function application
        val add2OneByOne = curriedFunc(1) _
        // call a curried function variable with a normal arugment
        println(add2OneByOne(2)(3))
        // output: 6

“卷积带来的一个附加益处,就是支持了多参数函数的infix操作。”

        // Usage: call a curried function with an expression in fluent infix style
        println(curriedFunc {1} {2} {3})
        // output: 6

“scala真强大啊!我们继续改改看。”

// 这个例子的主要功能是对一个List排序。
// 这是一个基于面向表达式的实现。
// * Using currying
object Main {
    object ListSorter {
        // curried function (a)(b)
        def sort[T](a: List[T])(f: (T, T) => Boolean): List[T] = {
            a.sortWith(f)
        }
    }

    object IntComparer {
        def compare(a: Int, b: Int): Boolean = {
            a < b
        }
    }

    def main(args: Array[String]): Unit = {
        val list = List(9,1,6,3,5)
        // use function rather than object
        println(ListSorter.sort(list)(IntComparer.compare))
        // use function with lambda expression
        println(ListSorter.sort(list)((a, b) => a < b ))
        // use function with underscore
        println(ListSorter.sort(list)(_ < _ ))

        // fluent infix style
        println(ListSorter.sort {list} {IntComparer.compare})
        // fluent infix style with lambda expression
        println(ListSorter.sort {list} {(a, b) => a < b})
        // fluent infix style with underscore
        println(ListSorter.sort {list} { _ < _ })

        // currying usage: partial function application
        val sortWith = ListSorter.sort(list) _
        // fluent infix style
        println(sortWith(IntComparer.compare))
        // fluent infix style with lambda expression
        println(sortWith {(a, b) => a < b})
        // fluent infix style with underscore
        println(sortWith { _ < _ })
    }
}

阿袁工作的第5天: 函数式编程:如何处理null

"今天有个新的认识。在面向对象语言中,我们经常使用null。但是在数学计算中,null是没有意义的。"
“那么要使用什么呢?”
“如果返回值类型是一个集合,最好返回空集合。”
“如果返回值类型是一个值,scala提供了一个Option的泛型类,提供了一个None对象,表示返回的值是没有值。”
“代码示例如下。”

// 这个例子的主要功能是说明使用Nil和None、
object Main {
    object NilNoneSample {
        // 使用空集合。不要使用null。
        def getEmptyList(): Seq[Int] = {
            Nil
        }
        
        // 使用空集合。不要使用null。
        def getEmptyVector(): Vector[Int] = {
            Vector()
        }
        
        // 对于可能返回“没有值”的结果,使用Option泛型类。
        def getValueIfLargeThanZero(a: Int): Option[Int] = {
            if (a > 0) Option(a) else None
        }
    }

    def main(args: Array[String]): Unit = {
        // use empty collection replace null
        println(NilNoneSample.getEmptyList)
        // output: List()
        println(NilNoneSample.getEmptyVector)
        // output: Vector()

        // use None replace null
        println(NilNoneSample.getValueIfLargeThanZero(1).get)
        // output: 1
        println(NilNoneSample.getValueIfLargeThanZero(-1).isEmpty)
        // output: true
    }
}

阿袁的日记

2016年9月X日 星期六
最近和阿静接触的机会多了很多。也学会了一些函数式编程的概念。
总结一下:
函数式编程的风格,即面向表达式编程风格,有如下要求:

  • 把类看是算法的分类。
  • 使用函数代替对象。
  • 对于变量和参数,尽量使用:值(最好是不变的),Collection和函数等类型。
  • 尽量使用不可变的数据类型。(重申一遍)
  • 避免使用return语句。
  • 对于集合类型,使用空集合来代替null。
  • 对于其他数据类型,使用None代替null。
  • 可以使用卷积来方便于多步骤计算的要求。

参照

posted @ 2016-09-18 16:25  SNYang  阅读(2800)  评论(6编辑  收藏  举报