理解计算的本源,学习记录集:编程语言发展趋势,函数式编程

概述

首先,这个话题是个非常大的专题,其内容十分丰富。本文仅仅起到启发的作用,感兴趣的人可以自己去搜索相关专题,文末链接提供了一些参考资料。

本文的目的,首先在于简单的介绍下编程语言范型的发展和趋势,以及说明下相应的编程思想的演化、涉及的编程语言的简介;其次,介绍函数式编程的一些基本概念,抛砖引玉的作用;最后,介绍函数式编程思想在Java语言中的应用。

如有错误,不吝赐教。

编程语言的发展趋势

作为编程的基本工具,对编程语言的掌握是我们软件开发人员首要的技能。越是对编程语言的理解越深入,就越能利用好这个编程工具,高效率、高质量的进行软件编程。好吧,理解什么?如何理解呢?

我们来看专家如何说,参考ref1。

2-300x225

实际上简单一句话,编程语言的发展趋势,就是:在计算机系统和人类之间,能够尽量隐藏计算机硬件实现的一切细节,而能提供出一种完备的能力,让我们在编程时,尽量关注在要解决的问题本身,而不是在解决这个问题的细节步骤上面。通俗的说,我们只需要告诉计算机What,不要告诉它How

一个实例

为什么这样说?让我们先看看一个简单的例子:从0到100,获取其中的偶数,并打印出来。

  List<int> evenNumList = new List<int>();  //暴露出具体的类型,类型定义涉及计算机的具体实现。
  for(int i = 0; i < 100; i++)              // 为了获取偶数,需要引入多余的变量i。
  {
     if(0 == i % 2)
     {
        evenNumList.add(i);
     }else { continue;}
  }                                          // 整个for 语句,透露出了具体的算法:向List<int>中添加数据。
  for(int i = 0; i < evenNumList.length(); i++)
  { 
     Console.writeln(evenNumList[i]);         // 为了打印,同样引入了多余的变量i,分析我们要解决的问题,根本就无关这个i什么事
  }

 

上面代码示例,是从C、C++,Java,C#等编程语言的基本实现方式。这有什么问题吗?我们都习以为常了。

val evenNumList = (0 to 100).filter(_ % 2 == 0) // 从0到100,以是否整除2为条件,过滤出偶数集合。
evenNumList.foreach(arg => println(arg))        // 遍历这个偶数集合,打印出每一个数。
上面是Scala编程语言的写法。相比之下,哪一种语言关注的是要解决的问题本身,哪一种更多涉及了解决问题的细节实现步骤?除了一点语法上的理解之外(scala中的'_',占位符语法),一些不同点:

1) evenNumList的类型定义不需要了,一个val即可。类型定义涉及计算机实现的细节方面。

2) to, filter, foreach,这些更贴合自然语言的单词运用。这些单词直接反映出在解决这个问题的关键算法,至于其内部是怎么实现的,我们不关心。

3) 语法结构更加贴合自然语句,可读性大大提高。见上面的注释。

进步在哪里?编程语言所能反映出我们思维形式的抽象层次提高了。

前者,现在被称为“命令式编程语言”;后者被称为“描述式编程语言”,而函数式编程是描述式编程的一种形式

面向过程/面向对象、命令式/描述式,关联?

这有与我们熟悉的“面向过程”、“面向对象”有什么关联呢?比如,Java/C#是面向对象的编程语言,同时,它也是命令式的编程语言,仅在最新的Java 7中开始引入部分描述编程语言才有的功能,比如lambda表达式。

这里给出我的一点理解,以供探讨。“面向过程”和“面向对象”之分,着重在于从软件工程的角度,对软件框架的大方面,加以区分:面向过 程,是从软件功能入手,按照问题解决的过程,给出相应的软件实现,抽象层次不高,偏向于计算机实现的各种细节;面向对象,是从问题解决所涉及的各种对象之 间的关系入手,首先分析清楚各种关联结构,将问题解决得过程,化归到所属的具体对象身上,通过对象间的交互,给出相应的软件实现,对象概念的提出,是更高 层次的抽象。

但在面向对象编程中,对象的某个具体方法的实现中,仍然是类似一条条的指令式的实现方式,即需要按照某个算法,清晰的写清楚具体的实现 步骤。即对具体方法的实现过程,仍处于较低层次的抽象。而相比这种“命令式编程”,“描述性编程”,正如上面的例子,to,filter方法,是对集合的 添加、判断等具体方法的进一步抽象。最终就如前面所说的,我们要把注意力关注在问题解决上,而不是具体计算机怎么去实现。

从“命令式编程”到“描述式编程”,需要一系列思维方式的转变,还要理解一些新的概念,最终结果,那就是我们的编程能力得到了进化,就如同从“面向过程”到“面向对象”的进化,软件开发技术会不断前进。

多核计算时代

描述式编程兴起的另一个原因,并发。其实软件技术的进步,依靠于现实问题的复杂性不断增加,人们对软件能力的需要不断提高,硬件的能力随着摩尔定律同样在快速发展。随着多核cpu的出现,软件如何利用这些新的硬件能力,从而最大化软件的效能,是个永恒的问题。

“多核革命的一个有趣之处在于,它对于并发的思维方式会有所改变。传统的并发思维是在单个CPU上执行多个逻辑任务,使用旧有的分时方 式、时间片模型来执行多个任务。但是,你想一下便会发现如今的并发情况正好相反,现在是要将一个逻辑上的任务放在多个CPU上执行。这改变了我们编写程序 的方式,这意味着对于语言或是API来说,我们需要有办法来分解任务,把它拆分成多个小任务后独立的执行,而传统的编程语言中并不关注这点。”

从多核而来的多线程编程,尽量提高软件的响应性和处理速度,有此而来的并发问题,越来越突出了。这里涉及的一些科学理论还是很复杂的, 总之我们现在经常接触的共享-锁机制,已经是问题多多,在多核计算时代,这种计算结构已不能胜任了,新的并发处理机制势必代替之。而在函数式编程中,大多 都不是用共享-锁的机制来解决并发问题的。

混合型编程范式

相对来说,C#是比Java更现代化的编程语言,它提供了更高阶的抽象,比如属性和事件,当然,你也可以说这只是语法糖。C#中的LINQ,foreach可以说是借鉴了声明式编程的思想的产物,但语言整体仍然不算“正统”。在ref1中Anders Hejlsberg提到,将来的编程语言应该是朝着混合型发展,即同时对命令式和声明式编程的完备支持,软件开发需要它们两者,而不是只是其中之一,过去是狭隘在前者,以后,要两方面都要使用。在这方面,现在流行的一些JVM Languages(如scala编程语言)和F#,走在前列。

目前在JVM Languages里呼声很高的新秀,就是scala编程语言了,有兴趣者,自行学习吧。而对于F#,.net平台的开发也是离不开它了,从微软的智能手机平台,到桌面、网络开发平台,F#会越来越发挥出重要的作用。

函数式编程(Functional Programming)

下面,让我们初探函数式编程,通常,你可以把它理解为“描述式编程”的代名词。

上面提到,从“命令式编程”到“描述式编程”,是需要思维方式上的转换的,这个过程对熟悉前者的开发人员来说,需要点时间,但是,一旦掌握,我们就会感到编程技术的一片新天地了。

概念

显然的,我们都要问:这是个什么东西呢?不会又是什么旧瓶新装的概念吧?函数不是早就有了吗?跟C/C++/Java中的函数有什么区别呢?

疑问总是驱动我们学习新知。

首先,要纠正的一个对它的认知:这玩意的历史其实十分古老,甚至超过计算机发明的历史,它一直活跃在学术界,而不是商业界,这也许是我 们不太熟悉它的一个原因。也正因为它活跃在学术界,主要用来解决AI编程问题,所以其概念中有很多“学术”的味道,需要较好的数学基础才能较好地理解,这 么高的门槛,也是导致它过去不能成为商业界里的具有很高生产力的编程语言的另一个原因。再次,它本身的发展和实现实在有赖于计算机硬件性能的发展,不然其 执行性能显然比不过C/C++这种贴近计算机硬件的编程语言,自然也就竞争不过它们了。

正是计算机硬件本身的发展和显示问题的日益复杂,具体的说,我认为正是多核计算时代的到来,直接把函数式编程从幕后带到了前台。

函数式编程,其基础在于λ 演算,直接根源于计算机理论科学中的可计算性理论,以及计算机的最初模型:图灵机。简单的说,在理论上,需要一种数学工具,用来证明什么问题是可以用计算 机来实现的,又或者是不能用计算机来实现的。比如,从广义上讲如“为我烹制一个汉堡”这样的问题是无法用计算机来解决的(至少在目前).从这里看来,人工 智能的领域,这些就是基础了。

函数式编程的数学基础是个专业话题,这里无法详细说明。

从一个易于理解的角度,函数式编程和命令式编程的根本区别,在于如何看计算机的计算模型上:命令式编程把计算机看成按照一条条指令,顺序执行的计算模型,这个我们已经熟知;而函数式编程把计算机看成按照数学中函数调用的方式有区别吗?我个人的看法,命令式编程是按照计算机的硬件构成方式进行编程,强迫我们要熟悉硬件的工作原理;而函数式编程是屏蔽这些硬件实现上的细节,专注在问题解决的数学模型上,为什么会这样?也许,那些计算计科学家认为,涉及硬件实现的细节的方面,就超出了他们工作的范畴,那些应该是电子物理工程方面的内容了。不过,函数式编程的实现,肯定还是要基于硬件细节的,但这种理念认为,那就不是我们程序员要关心的事了。

数学专业中的“函数”概念,是非常庞大的,而C/C++/Java中的“函数”被称为“过程/方法”,对应其数学模型,只是对应于一阶逻辑;而函数式编程中的“函数”,对应于数学中二阶逻辑,即高阶函数。(这里的一阶逻辑、二阶逻辑,这里不再说明,google之)

通俗的说,C/C++/Java,包括C#,都不能在方法中,传递另一个方法本身作为参数,也不能返回一个方法本身,因为方法不是一个对象,最多是传递一个函数引用,和返回一个函数引用,这个引用是个对象,但不是它所引用的函数本身

从低阶函数到高阶函数,对于现在的编程需要来说,有什么意义呢?答案在于高阶函数体现出了一个不变性(immutability)!,另一方面,做为现实问题的数学建模,高阶函数体现出来的抽象,更加符合现实世界中对象的复杂联系,也就是说,支持高阶函数的编程语言,更加能够真实的映射出现实世界里的事物间的关系,这也就是说进一步提高了编程语言的能力。

从命令式编程到描述式编程,个人感觉,这不是在绕圈子嘛!为什么从一开始,函数式编程没能成为市场上的主流?反思学习编程的过程,那么 费劲要接近底层原理,到现在为止,我们的脑袋已经变成“命令式”的思维方式了。不过,想想也是,计算机要想快速成为信息社会的基石,靠那些实验室里的科学 家是不行的,一开始从硬件实现的角度,来设计和实现编程语言,这样才能迅速发挥计算机硬件的能力,软件开发人员的职责,其实就是要理解两个世界的逻辑,并 负责翻译和转化。而函数式编程,对计算机的硬件实现是很高的要求,这种思想从一种逻辑认识方式到现实实现,总要一段发展的空间和时间。

现在是时候了。

编程思想

在介绍函数式编程思想的具体内容前,还是想告诉大家,从根本上来说,函数式编程(描述性编程)与命令式编程的区别,在于它们如何看待计算机的计 算模型的不同,是将对问题解决的思维方式,从偏向硬件实现的指令序列,转变到问题本身的数学建模上来。这一点,要首先知悉和理解。

其实每当工业界面临了不容易解决的问题,总会从学术界寻求帮助。现在随着多核计算时代的带来,多线程开发和并发遇到了难题,这是函数式编程走向前台的直接因素之一。

在命令式编程中,经常可见x=x+1;这种形式的语句,这对于数学家来说,他会说“啊,这不是true”,但是如果换成y = x + 1,这显然就是数学上一个函数y = f(x)了,输入一个确定x的值,就有一个确定的y与之对应。这就是“不可变性”。

在我们熟悉的多线程编程中,基本上都是基于共享-锁的机制来实现,我们需要对共享的状态进行特殊的加锁机制,以防止出现共享状态被改变的情况,但加了锁,就可能导致“死锁”。这是因为在命令式编程中,“可变性”,或者说状态转化,是很平常的,变量仅仅表示内存空间中的一个可变区域,程序的运行,可以抽象出来一个状态机模型,各种状态之间相互转换,这样就存在着“函数的副作用”,对多线程编程和并发,十分不利。

那么函数式编程就不需要状态了吗?不是的,在命令式编程中,是依靠一个个的变量保存了状态;而在函数式编程中,状态只作为传入函数的参数而存在。

在函数式编程中,引入了Actor模型,用来处理并发。这里不赘述。

就函数编程思想的内容来说,涉及下面一些概念:

  1. Lambda表达式
  2. 函数闭包
  3. 函数是一等的,无副作用,可以传入和返回
  4. 柯里化
  5. 引用透明
  6. 惰性求值
  7. 尾递归
  8. 模式匹配

这些内容,全部说明出来,将会有很多文字量,所以这里不再赘述,参考ref7中的链接内容。ref2中的内容比较专业。

FP in java

我记得以前有段时间就在研究如何用C语言来模拟编写面向对象的代码,这叫做OO in C。其实编程思想是通用的,不管用什么编程语言,都有实现的方式,只是没有原生支持的编程语言那么简洁、漂亮。

在Java中应用函数式编程的思想,就好比是用一种新的方式来写Java程序,我们能得到什么好处呢?

  1. 更高的模块化,带来更好的功能内聚,好更容易测试
  2. 更好的代码易读性,带来降低维护成本

这部分参考ref3、ref4。ref5、ref6是Java中支持函数式编程的开源库。

所以,FP in Java,是说借鉴函数式编程的思想,主要是不可变性、闭包,改进已有的Java程序实现方式,让我们的Java程序更具模块化、易读性。也有一些开发库可供使用。

不可变性,使用final, readonly

在函数式编程中,变量只是表达式的别名,这样我们就不必把所有东西打在一行里。变量是不能更改的,所有变量只能被赋值一次。在Java中,要被声明为final。

  final int i = 5;
  final int j = i + 3;

这里,我们用“final变量”来模拟了函数式编程中的表达式别名。对于类来说,情况要复杂些:

1) 把所有的域声明成final的。

在Java中把域定义成final的时候,你必须或是在声明的时候或是在构造函数中初始化它们。如果你的IDE抱怨你没有在声明场合初始化它们的话,别紧张;当你在构造函数中写入适当的代码后,它就会意识到你知道你在做什么。

2)把类声明成final的,这样它就不会被重写。

如果类可以被重写的话,那它的方法的行为也可以被重写,因此你最安全的选择就是不允许子类化。这里提一下,这就是Java的String类使用的策略。

3) 不要提供一个无参数的构造函数。

如果你有一个不可变对象的话,你就必须要在构造函数中设置其将会包含的任何状态。如果你没有状态要设置的话,那要一个对象来干什么?无 状态类的静态方法一样会起到很好的作用;因此,你永远也不应该为一个不可变类提供一个无参数的构造函数。如果你正在使用的框架基于某些原因需要这样的构造 函数的话,看看你能不能通过提供一个私有的无参数构造函数(这是经由反射可见的)来满足这一要求。

需要注意的一点是,无参数构造函数的缺失违反了JavaBeans的标准,该标准坚持要有一个默认的构造函数。不过JavaBeans无论如何都不可能是不可变的,这是由setXXX方法的工作方式决定了的。

4) 至少提供一个构造函数

如果你没有提供一个无参数构造函数的话,那么这就是你给对象添加一些状态的最后机会了!

5) 除了了构造函数之外,不再提供任何的可变方法。

你不仅要避免典型的受JavaBeans启发的setXXX方法,还必须注意不要返回可变的对象引用。对象引用被声明成final的,这是实情,但这并不意味这你不能改变它所指向的内容。因此,你需要确保你是防御性地拷贝了从getXXX方法中返回的任何对象引用。

“传统的”不可变类
public final class Address {
    private final String name;
    private final List streets;
    private final String city;
    private final String state;
    private final String zip;

    public Address(String name, List streets,
                   String city, String state, String zip) {
        this.name = name;
        this.streets = streets;
        this.city = city;
        this.state = state;
        this.zip = zip;
    }

    public String getName() {
        return name;
    }

    public List getStreets() {
        return Collections.unmodifiableList(streets); // 注意这个unmodifiableList()方法,使用了java中已有的一些对不可变性的支持。
    }

    public String getCity() {
        return city;
    }

    public String getState() {
        return state;
    }

    public String getZip() {
        return zip;
    }
}


更清晰的不可变类
public final class Address {
    public final List streets;
    public final String city;  // 这些public的,但是final的成员,节省了那些样板式的代码,还带来了不可变性。
    public final String state;
    public final String zip;

    public Address(List streets, String city, String state, String zip) {
        this.streets = streets;
        this.city = city;
        this.state = state;
        this.zip = zip;
    }

    public final List getStreets() {
        return Collections.unmodifiableList(streets);
    }
}


闭包,使用内部类、匿名类

在最新的Java 7中,已支持Labmda表达式和闭包,这里说的方式是在Java7之前的实现方式。

闭包的一些定义:

# 是引用了自由变量的函数。这个函数通常被定义在另一个外部函数中,并且引用了外部函数中的变量。 -- <<wikipedia>>

# 是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。-- <<Java编程思想>>

# 是一个匿名的代码块,可以接受参数,并返回一个返回值,也可以引用和使用在它周围的,可见域中定义的变量。-- Groovy ['ɡru:vi]

# 是一个表达式,它具有自由变量及邦定这些变量的上下文环境。

# 闭包允许你将一些行为封装,将它像一个对象一样传来递去,而且它依然能够访问到原来第一次声明时的上下文。

# 是指拥有多个变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

# 闭包是可以包含自由(未绑定)变量的代码块;这些变量不是在这个代码块或者任何全局上下文中定义的,而是在定义代码块的环境中定义。

闭包的价值在于可以作为函数对象或者匿名函数,持有上下文数据,作为一等对象进行传递和保存。

借用一个非常好的说法来:对象是附有行为的数据,而闭包是附有数据的行为。

在JAVA中,方法不能独立于类而存在,也没法直接传递一个函数本身,它是通过“接口+内部类”实现的,所以在Java中,闭包实际上是一个类的方法,在这个方法中,可以引用这个类定义的上下文中的变量

public void anonymousExample() {
    String nonFinalVariable = "Non Final Example";
    String variable = "Outer Method Variable";
    new Thread(new Runnable() {
        String variable = "Runnable Class Member";
        public void run() {                       //这个run(),就是一个闭包,定义一个匿名类 new Runnable()中,其中可以使用定义在方法中变量nonFinalVariable和variable.
            String variable = "Run Method Variable";
            //Below line gives compilation error.
            //System.out.println("->" + nonFinalVariable);
            System.out.println("->" + variable);
            System.out.println("->" + this.variable); 
       }
    }).start();
}


总结

所谓的FP in Java,只是应用函数式的编程思想,在Java中进行有限模拟,何时用,怎么用,完全还要开发人员根据实际情况进行选择,理解“不变性”的概念是基础,如何才能应用的好,是要在开发中不断总结的。

参考

ref1

C#之父Anders Hejlsberg演讲解读:编程语言大趋势

ref2

IBM DevelopWorks 函数式思维

ref3

Java 语言中的函数编程 - 利用闭包和高阶函数编写模块化的 Java 代码

ref4

EBook: Function Programming for Java Developers.pdf

ref5

Function Java

ref6

funcito

ref7

为什么大家都觉得函数式编程将会流行?

函数式编程扫盲篇

函数式编程初探

posted @ 2013-01-04 14:45 Leo L.Cao 阅读(...) 评论(...) 编辑 收藏