Scala学习笔记(5)-函数式编程

Scala是一种函数式编程语言,它具有函数式编程范式的诸多特点。需要说明的是,scala并不是一种纯函数式编程语言,比如在纯函数式编程语言中,没有可变变量,没有while循环语句,但scala有。本文是对Scala函数式编程特性的一个学习总结,共包括如下的章节:

  • 函数式编程的发展背景
  • 函数式编程的基本特征
  • scala中的函数与方法
  • scala函数的其它特性
  • 集合数据操作

参考资料:

1、如果要了解scala开发环境的搭建,可参考《Scala学习笔记(1)-快速起步》

2、如果要了解scala语言的基本语法,可参考《Scala学习笔记(2)-基础语法》

3、如果要了解scala语言的面向对象的编程知识,可参考《Scala学习笔记(3)-面向对象编程上篇》《Scala学习笔记(4)-面向对象编程下篇》

一、函数式编程的发展背景

函数式编程是近几年非常火热的一个词,其实函数数编程并不是一个新东西,它是很古老的,函数式编程语言的鼻主Lisp语言的产生时间甚至比C语言还早。只不过在最初的发展过程中,c语言等结构化的编程方式占据了主流,后来面向对象的编程兴起也成为了主流。而函数式编程一直长期停留在学术研究中,没有大规模的在软件产业中应用。

那现在函数式编程为什么又火热起来了呢?其背后的驱动力是什么呢? 这有很多原因,想到的可能的原因有,第一,是计算机性能的提升尤其是多核技术的发展,使得原来函数式编程最大的效率问题得以解决;第二,大数据的流行也推动了函数式编程的发展,因为函数式编程最擅长的就是处理数据,包括大数据中如Spark、Storm等流式大数据处理框架的流行,而这些框架都使用了支持函数式编程的语言实现的,如spark用的是scala语言,storm用的是clojure语言;第三,软件在各行各业的应用越来越广,也推动了技术和人的能力的提升。所有这些包括其它的一些因素,让大家越来越认识到函数式编程的一些固有优势,这形成了一个良性的循环,推动了函数式编程的应用广度和深度,使得函数式编程越来越成为主流的编程范式之一。甚至一些主流语言(如c++,java,python)都增加了对函数式编程的支持。

随着大家对函数式编程范式的使用,发现使用函数式编程,有如下的优点:

1、可以以更少的代码实现同样的功能,可极大的提升生产效率。

2、更容易编写多并发或多线程的应用,更易于编写利用多核的应用程序。

3、可以帮助写出健壮的代码

4、更容易写出易于阅读、理解的优雅代码

简单了解了函数式编程的历史背景后。我们来看下什么是函数式编程。函数式编程它是一种编程范式,如面向过程的编程、面向对象的编程都是一种编程范式。谈函数式编程肯定离不开编程语言,那有哪些编程语言是属于函数式编程语言呢?简单点可以分为三类,第一类是纯函数式编程语言,如原始的Lisp(不包括各种基于lisp的方言)、Haskell、F#等;第二类是混合型的,除了具备函数式编程的特性外,还有一些非函数式编程的特性,如scala,clojure(是基于Lisp的方言)等语言;第三类严格的说不能算函数式编程语言,但具备部分函数式编程语言的特性,如python , ruby, java8等。

二、函数式编程基本特征

对于函数式编程,有几个非常重要的和基础性的特征:

  1. 函数是一等公民:

所谓一等公民是指函数也有数据类型,函数与其他数据类型的变量或值一样,处于平等地位,可以赋值给其它变量,也可以作为函数参数,传入另一个函数,或者作为别的函数的返回值。

函数是一等公民是函数式编程范式最重要的特性和基础。

  1. 不可变数据

所有的状态(或变量)都是不可变的。你可以声明一个状态,但是不能改变这个状态。如果要变化,只能复制一个。

纯函数式编程语言不使用任何可变数据结构或变量。但在Scala等编程语言中,即支持不可变的数据结构或变量,也支持可变的。

3.强调函数没有"副作用"

所谓函数没有副作用,意味着函数要保持独立,一旦函数的输入确定,输出就是确定的,函数的执行不会影响系统的状态,不会修改外部状态。想象下,如果函数没有副作用,那函数的执行就可以缓存起来了,一旦函数执行过一次,如果再次执行,当输入和前面一样的情况下,就直接可以用前面执行的输出结果,根本就不用再次运算了,想象下,这个是否可大大提高程序运行的效率。

3、一切皆是表达式

在函数式编程语言中,每一个语句都是一个表达式,都会有返回值。比如scala中的if-else控制结构就是一个有返回值的表达式。这与命令式编程语言中的if-else语句显著不同。这一特性有助于用不可变量编写应用程序。

除了这几个核心特征外,还有很多其它特点,比如:

  • 支持匿名函数(在java中称为lambda表达式)

  • 递归不再是可有可无的,而是处于一个核心的位置(循环没了,就是靠递归来解决的,递归最大的好处就简化代码,它可以把一个复杂的问题用很简单的代码描述出来。递归的精髓是描述问题,而这正是函数式编程的精髓。)

  • 支持惰性求值,就是在需要的时候才计算表达式

除了这些特点外,理解函数式编程与传统编程的区别,还有一个思维上的转变。

传统的编程语言,是一种命令式语言,命令式语言是跟“状态”打交道的,基础是“语句”(执行某种操作,如赋值语句。命令式程序可以看成一个指令序列,通过改变状态的值来实现算法。

函数式语言是跟“值”打交道的,基础是“表达式”(是一个单纯的运算过程,总是有返回值),函数式程序可以看成一个由输入到输出的函数,而这个函数又由其他函数或者自身来构造。实现算法的过程就是由语言提供的原始函数逐步构造一个符合算法要求的函数的过程。

三、scala中的函数与方法

(一)基本概念

Scala语言即支持面向函数的编程,也支持面向对象的编程,所以scala中既有函数也有方法,大多数情况下我们都可以不去理会它们之间的区别。但是有时候我们必须要了解他们之间的不同。

从广义上说,方法也是一种函数,只不过方法是面向对象编程中的概念,方法是类的一部分,必须通过对象来调用。

在scala中,我们可以这么来区分方法与函数。使用关键字def定义的函数我们称为方法,类似java类中的方法。而使用=>定义的函数我们称为函数,类似java8中的Lambda表达式。在本文中我们提到的方法与函数指按照这个规则来区分的。

下面我们来看一个例子:

class A{
  def test1(a:Int)={
    println(a)
    a*2
  }
  val test2 = (a:Int)=>{
     println(a)
     a*2
  } 
}

上面代码定义了一个类A,该类有两个成员:

1)一是通过def关键字定义的方法test1,该方法有一个整型参数,返回值也是一个整数。

2)二是通过关键字val定义了的变量test2,这个test2变量的值是一个函数,也就是说变量test2的类型是一个函数类型。

我们只看test1方法的参数列表及后面的部分,再看test2变量的值的部分,发现非常类似,区别就是 = 和 =>。

下面我们看看如何使用test1方法和test2变量,在scala命令行中执行,结果如下:

scala> val a = new A()
a: A = A@790765dc

scala> a.test1(10)
10
res16: Int = 20

scala> a.test2(10)
res17: Int = 20

可以看出,使用的方式和结果是一样的。

scala中的函数,在scala中还有几个叫法,如Lambda表达式、Lambda函数、函数字面值、匿名函数,看到这些叫法,都是一个意思。

(二)方法和函数的区别

1、用途上的区别

方法正如面向对象编程中的概念,作为类的成员出现,然后通过对象来调用,如:

class Plus{
  def plus(a:Int,b:Int)= a+b
}

val obj =new Plus
obj.plus(2,3)

说明:也可以在方法的内部通过def关键字定义一个内部方法,不过这个方法只在包括它的方法内有效。

函数最大的应用场景是作为高阶函数(可以是类的方法)的输入或输出。所谓高阶函数,指该函数的输入参数中有函数类型的参数,或者返回值类型是一个函数类型。如下面例子:

class A{
  def cal(a:Int,b:Int,f:(Int,Int)=>Int )= f(a,b)
}

val obj =new A
obj.cal(2,3,(a:Int,b:Int)=>a+b)
obj.cal(2,3,(a:Int,b:Int)=>a*b)

上面代码中类A中的cal方法的第3个参数就是一个函数类型。在调用该方法时,可以传入该函数类型的不同函数实现,如 (a:Int,b:Int)=>a+b 和 (a:Int,b:Int)=>a*b 分别是同一函数类型的两个不同实例。

在scala中,函数是一种数据类型,其类型名的格式是:

(函数参数类型列表)=>函数返回值类型

函数可以没有参数,但()不能省,也可以由多个参数类型,之间以逗号分隔。注意,参数类型列表中只有类型名,没有参数名,不要与具体的函数定义混淆了。

即: (Int,Int)=>Int 是一个函数类型; (a:Int,b:Int)=>a+b是该类型的一个实现,是一个具体的函数。

我们再看一个返回函数类型的例子:

class A{
  def cal():(Int,Int)=>Int = (a:Int,b:Int)=>a+b
}

val obj =new A
val f = obj.cal()
f(2,3)

上面代码定义的cal方法返回的是一个函数类型,类型为(Int,Int)=>Int 。调用该方法得到的是一个(Int,Int)=>Int 类型的一个实例。

总结下,方法通常是作为类用来实现其设计、完成其某个功能的一段逻辑代码。而函数则是用于完成特定的计算逻辑。

2、函数是一个值

在Scala中函数是一个值(即某个函数类型的一个值),但方法不是值。所以一个方法不能赋值给一个变量,而函数可以。函数即可以赋值给一个变量,也可以作为高阶函数的输入或输出,但方法不可以。下面我们看一个例子:

scala> class A{
     |   val test1 = (a:Int,b:Int)=>a+b
     |   def test2(a:Int,b:Int)=a+b
     | }
defined class A

scala> val a = new A()
a: A = A@3566bf22

scala> val f1 = a.test1
f1: (Int, Int) => Int = A$$Lambda$1238/4211087@e99145d

scala> val f2 = a.test2
<console>:12: error: missing argument list for method test2 in class A
Unapplied methods are only converted to functions when a function type is expect
You can make this conversion explicit by writing `test2 _` or `test2(_,_)` inste
       val f2 = a.test2
                  ^

从上面代码的运行可以看出,将test1函数赋值给一个变量是可以的,但将test2函数赋值给变量是会报错的。

我们在看一个例子:

class Person{
  def cal()={
    12
  }
  val re = cal  //这里不是方法赋值,而是方法调用
  println(re)
}

上面代码中的val re = cal 不是将方法cal赋值给变量re,而是调用方法cal,将方法的执行返回值返回给变量re。这是因为方法没有参数时,调用时可以省去()。

四、scala函数的其它特性

下面介绍scala函数的其它特性。

(一)高阶函数

所谓高阶函数(或方法),指该函数的输入参数中有函数类型的参数,或者返回值类型是一个函数类型。

上面已经有例子介绍,这里不再介绍。

(二)闭包

所谓闭包,是指一个函数可以使用包含它的上下文中(如方法)的数据。我们先看一个例子:

class Demo{
  def fun(num:Int) ={
    (x:Int)=>x*num
  }
}

object Hello {
  def main(args: Array[String]){
    val demo = new Demo
    val f = demo.fun(10)
    println(f(2))
  }
}

上面代码中,Demo类中的fun方法返回了一个函数,该函数使用了fun方法的参数num(是一个局部变量)。

scala中的函数,还有柯里化、部分应用函数、偏函数等函数式编程中的一些概念,在本文中不再介绍。

五、集合数据操作

函数式编程中最大的应用场景是对集合数据的操作,大数据的处理核心也是对数据集合的处理,只不过是数据量非常大的集合。

scala通过高阶函数提供了丰富的集合数据相关操作。我们下面通过例子来说明。

先看如何对一个List集合做过滤的操作,代码如下:

object Hello {
  def main(args: Array[String]){
    var list = List(1,2,3,4,5)
    var newlist  = list.filter((item)=>item%2==0)
    println(newlist)
  }
}

scala集合类List的filter方法是一个高阶函数,其参数类型为(T)=>Boolean ,即参数是一个泛型函数,该函数输入参数类型是List中的元素类型,输出值是一个布尔值。filter方法的功能是对每个元素都调用下其参数函数,所有返回这为true的元素都会被保留,最后返回被保留的元素的一个新的List列表。

我们再看一个map操作,map操作的功能是一个集合映射成另一个新的集合,原集合的每个元素通过函数运算得到一个新的值。如下面例子:

object Hello {
  def main(args: Array[String]){
    var list = List("hello","good","tom")
    var newlist  = list.map((item)=>item.length)
    println(newlist)
  }
}

运行上面代码,输出List(5, 4, 3) ,这是因为map方法也是一个高阶函数,每个元素通过其函数参数转换成了一个新的元素,所有转换的元素形成一个新的列表被返回。

scala的集合类型有多种丰富的高阶方法,可以完成各种数据计算,我们这里只是举例两个很简单的例子,来感受下函数式编程在集合数据处理中的强大作用。

六、小结

scala的函数式编程涉及的知识很多,本文只是介绍了最基础的部分。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容