函数式编程与Scala

来源:互联网 发布:linux 如何设置ntp 编辑:程序博客网 时间:2024/05/07 16:08

1函数式编程

      函数式编程(FunctionalProgramming,简称FP)是一种编程范式,我们常见的编程范式有命令式编程(常用的面向对象编程就是),函数式编程,逻辑式编程。

 

      命令式编程、函数式编程的区别:

命令式编程是面向计算机硬件的抽象,抽象元素有变量(对一个存储单元),赋值语句(对应获取、存储指令),表达式(对应内存引用和算术运算),控制语句(对应跳转指令),总之命令式程序就是一个冯诺伊曼机的指令序列。而函数式编程是面向数学的抽象,将计算描述为一种表达式求值的过程,总之函数式程序就是一个表达式

      

函数式编程的本质:

      计算机学科中通常说的“函数”其实是指子过程。而函数式编程的“函数”就是指数学上的函数,即自变量从定义域到值域的映射。一个函数的值只取决于自变量的值,不依赖其他状态。比如sqrt(x)计算x的平方根,只要x不变,不论何时调用,调用几次,值都不变。函数式语言中函数是一等公民(第一类对象),可以在任何地方定义,可以作为函数的参数和返回值,可以进行组合。

      命令式编程中说的“变量”是指存储状态的单元。而函数式编程的“变量”就是数学上的变量,即一个值的名称。变量的值是不可变的(即Scala中的val),所以不允许像命令式编程那样多次给一个变量赋值。比如x=x+1,这在命令式编程中为一个赋值语句,而在数学中这个等式显然为假。

      再如,命令式编程中的控制语句(如条件语句)对应机器的跳转指令,而函数式编程的条件语句只是函数的语法糖。(如Scala中的if else不是语句而是三元运算符,是有返回值的)

      理论上说,函数式语言也不是通过冯诺伊曼体系结构的机器运行的,而是通过λ演算(lambda calculus)来运行的,即通过变量替换的方式进行:变量替换为其值或表达式,函数也替换为表达式,并根据运算符进行运算。大多数情况,函数式程序还是被编译成冯诺伊曼机的机器语言指令执行的。

 

      函数式编程的特性(好处):

      闭包和高阶函数:函数编程支持函数作为第一类对象,有时称为闭包或者仿函数(functor)对象。实质上,闭包是起函数的作用并可以像对象一样操作的对象。与此类似,FP语言支持高阶函数。高阶函数可以用另一个函数(间接地,用一个表达式)作为其输入参数,在某些情况下,它甚至返回一个函数作为其输出参数。这两种结构结合在一起使得可以用优雅的方式进行模块化编程,这是使用 FP 的最大好处。

      惰性计算:FP还引入了惰性计算的概念。在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。延迟的计算使您可以编写可能潜在地生成无穷输出的函数。因为不会计算多于程序的其余部分所需要的值,所以不需要担心由无穷计算所导致的 out-of-memory 错误。一个惰性计算的例子是生成无穷 Fibonacci列表的函数,但是对第nFibonacci数的计算相当于只是从可能的无穷列表中提取一项。

      递归:FP还有一个特点是用递归做为控制流程的机制。例如,Lisp处理的列表定义为在头元素后面有子列表,这种表示法使得它自己自然地对更小的子列表不断递归。

      引用透明性:FP通常还加强引用透明性,即如果提供同样的输入,那么函数总是返回同样的结果。就是说,表达式的值不依赖于可以改变值的全局状态。这使您可以从形式上推断程序行为,因为表达式的意义只取决于其子表达式而不是计算顺序或者其他表达式的副作用。这有助于验证正确性、简化算法,甚至有助于找出优化它的方法。

      无副作用:副作用是修改系统状态的语言结构。因为 FP语言不包含任何赋值语句,变量值一旦被指派就永远不会改变。而且,调用函数只会计算出结果 ──不会出现其他效果。因此,FP语言没有副作用。

 

      函数式编程语言:

      Lisp(最古老),HaskellErlangClojureScala等。Java程序员推荐学习最后两种。

 

2 Scala

2.1优势和劣势

ScalaJava相比的优势:

      函数式编程。FPOO有哪些优势,ScalaJava差不多也有哪些优势。Java8中引入了函数式编程,就是对这一优势的极大认同。然而Java由于其天生缺陷,包袱过于沉重,Java8FP的支持仍然比原生FP语言有很大的差距(例如,并不是所有函数都可以作为第一类对象传入其他函数中,函数必须被显式地定义为Lambda表达式或者使用函数式接口)。

      类型系统支持。Scala支持自动类型推断,如需定义一个从IntegerStringMapJava需要写Map<Integer, String> intStringMap = new HashMap<Integer,String>(); 然后再依次插入成员。在Scala中只需val intStringMap = Map(1->”I”, 2->”II”, 3->”III”) 直接装入成员,Scala会进行自动推断。从某种程度上来说,Scala的类型更加安全,且是图灵完备的(一切可计算的问题都能计算,Java就不是)。

      超强的表达能力。做同一件事,Scala的代码行基本只有Java1/2甚至更少,例如:

      Java5以前:


ArrayList<Integer> numbers = new ArrayList<Integer>();numbers.add(Integer.valueOf(1));numbers.add(Integer.valueOf(2));numbers.add(Integer.valueOf(3));numbers.add(Integer.valueOf(4));ArrayList<Integer> numbers2 = new ArrayList<Integer>();for (int i=0; i<numbers.size(); i++) {numbers2.add(numbers.get(i)*2);}

      Java5及之后:

ArrayList<Integer> numbers = new ArrayList<Integer>(Arrays.asList(1,2,3,4));ArrayList<Integer> numbers2 = new ArrayList<Integer>();for (Integer i : numbers) {numbers2.add(i*2);}

      Scala

    val numbers = List(1,2,3,4)    val numbers2 = numbers.map(x=>x*2)

      对并发编程的支持更好。Java中如果需要写带返回值的并发,可以用java.util.concurrent下的Future接口,但问题是Future不是一个Listenable,无法进行回调。而Scala本身就鼓励异步编程,ScalaFuture可以并发地执行,提供更快,异步,非阻塞的并发代码,并且支持回调。

      对设计模式的依赖大大降低。四人帮的《设计模式》相信很多人都看过,大部分程序员认为这些模式是语言无关的,其实这是不对的。对FP来说,很多模式在语言层面就提供了,不需要编写额外的代码。一些设计模式和Scala语言层面的对应:

      singletonpattern—— object

      visitorpattern—— pattern matching

      factorypattern—— apply mathod

builder pattern—— currying

      immutablepattern—— val

dependencyinject—— cake pattern

举例,在Java中想做一个singleton,要么用staticblock,要么利用Enum的单例特性做。在Scala中只要声明为object,即为单例。更进一步,在Java中想要延迟加载一个单例,只能使用同步代码块实现:

public class Singleton {private static Singleton instance;public synchronized static Singleton getInstance() {if (instance == null)instance = new Singleton();return instance;}}

Scala中只要在object中将变量修饰为lazy即可。

      强大的面向对象。Scala虽然是一种FP语言,但绝不是一种过程式语言,它提供了非常强大的面向对象能力。比如,对多继承问题的处理,Java的方法是使用接口(interface),但接口有较多的局限性,比如接口不能有构造器,内部除抽象方法外只能有public static final的常量,只能在Class层面实现接口。Scala的特质(trait)则灵活得多,可以在Class和对象层面混入(mixin)特质,在对象层面这种混入是动态的。

 

      劣势:

      语法复杂,学习曲线非常高。

      生态圈还比较小。(但是可以无缝衔接Javajar包,一定程度上解决这一问题)

      一些场景下的编程比较麻烦,比如IO

      对于大企业和旧项目,在Java上有比较多的人才、类库的积累,想切换到Scala比较困难。

 

2.2为什么推荐学习Scala

      高逼格。

为程序员提供了另一种抽象和思考的方式。

      对企业来说,FP技能是过滤普通程序员和优秀程序员的漏斗。

 

3 Scala的应用

3.1 Akka

      Akka是一个用 Scala 编写的库,用于简化编写容错的、高可伸缩性的 Java Scala Actor模型应用。Akka代码主要包括两块:底层分发(akka.dispatch包)和上层模型(akka.actor包)。

 

      底层分发:

      主要使用了ForkJoin机制。这块代码和Java7中引入的ForkJoin机制一样,都是由大神DougLea写的。从某种意义上说,ForkJoinTask是多核单进程版本的MapReduce Job,因此对数据开发人员来说理解应该不难。如果一个应用能被分解成多个子任务,并且组合多个子任务的结果就能够获得最终的答案,那么这个应用就适合用 ForkJoin 模式来解决。其原理如下图。


      ForkJoinPool:实现了ExecutorService,提供executesubmit等线程池基本方法,池中的线程都是ForkJoinWorkerThread

ForkJoinWorkerThread:继承自Thread,包含了自己的ForkJoin任务队列,在处理完自己任务队列中任务的时候,可以从其他Worker的队列中偷任务来执行;

ForkJoinTask:实现了Future接口,可以直接作为ForkJoinPool.submit的返回值,提供的fork方法将自己放到当前Worker线程的任务队列中,join方法让当前线程等待任务完成,或者通过偷过来等方式自己执行该任务。

      为了性能考虑,这三个类紧耦合,存在大量互相访问成员属性的情况,Doug Lea老先生说,这种比较ugly的实现,能让性能提高四倍,可以每秒处理10亿级别的ForkJoin任务。为了处理并发,大量使用了sun.misc.Unsafe类中提供的直接对内存的CAScompare and swap)原子操作。

 

      上层模型:

      上层实现了Actor模型,怎么理解这个模型呢?

假设有两个人:学生和聪明的老师。学生每天早上都会给老师发送邮件,而聪明的老师都会回复一句名言。这里需要解释:

  1、学生发送邮件。一旦发送成功,邮件不能再修改。这天然就具备了不可变性;

  2、老师会自己决定何时检查邮箱;

  3、老师还会回复一封邮件(也是不可变的);

  4、学生会自己决定何时检查邮箱;

  5、学生不会一直等待回信(非阻塞的)

现在,假设有三个聪明的老师和三个学生。每个学生都会给每个老师发送邮件。这会发生什么事?其实什么都没改变!每个人都有他自己的邮箱。这里需要注意的一点:默认情况下,邮箱里面的邮件是按照他们先后达到的次序进行阅读和处理的。

学生和老师变成我们的ActorEmail Inbox对应Mailbox组件。请求和响应不可修改、它们是不可变对象。最后,Actor中的MessageDispatcher组件将管理mailbox,并且将消息路由到对应的Mailbox中。


      JavaScala中使用Akka的代码从略。只说几条Akka的使用原则:

1、           尽量保持Actor职责单一

2、           一定要避免阻塞

3、           根据业务需求制定监控和错误处理策略


3.2 Kafka

      Kafka是使用Scala编写的一个分布式消息系统,以可水平扩展和高吞吐率而被广泛使用,StormSpark等都支持以Kafka作为数据源。Kafka的主要模型为发布/订阅模型,决定了消息的收集和消费模型是PushPull的,队列中的消息持久化到硬盘而非内存。它最重要的几点特性:消息持久化的时间复杂度为O(1);高吞吐率,普通PC单机每秒10w条以上消息传输;支持消息分区,分布式消费,同时保证每个Partition内的消息顺序传输;对离线和批量的数据处理系统都支持很好;支持在线水平扩展。

      Kafkabroker(即Kafka的工作节点)的架构如下图所示:


    具体代码分析见另一篇blog。

3.3 Spark

      Spark是一种开源的类HadoopMapReduce的通用并行框架,拥有Hadoop MapReduce所具有的优点;但不同于MapReduce的是Job中间输出结果可以保存在内存中,从而不再需要读写HDFS,因此Spark能更好地适用于数据挖掘与机器学习等需要迭代的MapReduce的算法。Spark是使用Scala编写的,Spark shell使用的交互语言也是Spark,即使是以jar方式编写Spark任务,使用Scala也比使用Java简洁。

      Spark中主要元素有:RDD是只读的数据分区集合,简称数据集,作用于RDD上的操作分为 transformationaction。经transformation处理后,数据集A将被转化为数据集B,但不真正进行运算;经action处理之后,数据集中的内容会被规约为一个具体的数值,且仅当进行action处理时,RDD和其父RDD上的所有操作才被提交执行。

      从任务代码到任务运行,涉及到的组件如下图:


      这里我用Scala实现了一个简单的Spark作业(命令行版本),用于实现以下功能:以空白符对一个文件中的单词进行分割,并统计词频,然后按词频从高到低排序,如遇到同词频的词,按字母顺序排序。Scala的实现不到20行,而用Java实现的话约需50行。如果用JavaMapReduce程序,更是需要200行左右才能实现,可见Scala的表达能力之强:

val textFile = sc.textFile("file:///software/spark/README.md")val result = textFile.flatMap(line => line.split("\\s+")).map(word => (word, 1)).reduceByKey(_ + _)val rdd1=result.map(x => (x._2,x._1)).groupByKey.sortByKey(false).map(x => (x._1,x._2.toList.sortWith(_<_)))val rdd2=rdd1.flatMap{ x =>     val len = x._2.length     val array = new Array[(Int,String)](len)     for(i <- 0 until len) {array(i) = (x._1,x._2(i))}     array}rdd2.collect().foreach(println)

1 0
原创粉丝点击