Scala是一種函數(shù)式編程語言,它具有函數(shù)式編程范式的諸多特點(diǎn)。需要說明的是,scala并不是一種純函數(shù)式編程語言,比如在純函數(shù)式編程語言中,沒有可變變量,沒有while循環(huán)語句,但scala有。本文是對Scala函數(shù)式編程特性的一個(gè)學(xué)習(xí)總結(jié),共包括如下的章節(jié):
- 函數(shù)式編程的發(fā)展背景
- 函數(shù)式編程的基本特征
- scala中的函數(shù)與方法
- scala函數(shù)的其它特性
- 集合數(shù)據(jù)操作
參考資料:
1、如果要了解scala開發(fā)環(huán)境的搭建,可參考《Scala學(xué)習(xí)筆記(1)-快速起步》。
2、如果要了解scala語言的基本語法,可參考《Scala學(xué)習(xí)筆記(2)-基礎(chǔ)語法》。
3、如果要了解scala語言的面向?qū)ο蟮木幊讨R,可參考《Scala學(xué)習(xí)筆記(3)-面向?qū)ο缶幊躺掀?/a>和《Scala學(xué)習(xí)筆記(4)-面向?qū)ο缶幊滔缕?/a>。
一、函數(shù)式編程的發(fā)展背景
函數(shù)式編程是近幾年非常火熱的一個(gè)詞,其實(shí)函數(shù)數(shù)編程并不是一個(gè)新東西,它是很古老的,函數(shù)式編程語言的鼻主Lisp語言的產(chǎn)生時(shí)間甚至比C語言還早。只不過在最初的發(fā)展過程中,c語言等結(jié)構(gòu)化的編程方式占據(jù)了主流,后來面向?qū)ο蟮木幊膛d起也成為了主流。而函數(shù)式編程一直長期停留在學(xué)術(shù)研究中,沒有大規(guī)模的在軟件產(chǎn)業(yè)中應(yīng)用。
那現(xiàn)在函數(shù)式編程為什么又火熱起來了呢?其背后的驅(qū)動力是什么呢? 這有很多原因,想到的可能的原因有,第一,是計(jì)算機(jī)性能的提升尤其是多核技術(shù)的發(fā)展,使得原來函數(shù)式編程最大的效率問題得以解決;第二,大數(shù)據(jù)的流行也推動了函數(shù)式編程的發(fā)展,因?yàn)楹瘮?shù)式編程最擅長的就是處理數(shù)據(jù),包括大數(shù)據(jù)中如Spark、Storm等流式大數(shù)據(jù)處理框架的流行,而這些框架都使用了支持函數(shù)式編程的語言實(shí)現(xiàn)的,如spark用的是scala語言,storm用的是clojure語言;第三,軟件在各行各業(yè)的應(yīng)用越來越廣,也推動了技術(shù)和人的能力的提升。所有這些包括其它的一些因素,讓大家越來越認(rèn)識到函數(shù)式編程的一些固有優(yōu)勢,這形成了一個(gè)良性的循環(huán),推動了函數(shù)式編程的應(yīng)用廣度和深度,使得函數(shù)式編程越來越成為主流的編程范式之一。甚至一些主流語言(如c++,java,python)都增加了對函數(shù)式編程的支持。
隨著大家對函數(shù)式編程范式的使用,發(fā)現(xiàn)使用函數(shù)式編程,有如下的優(yōu)點(diǎn):
1、可以以更少的代碼實(shí)現(xiàn)同樣的功能,可極大的提升生產(chǎn)效率。
2、更容易編寫多并發(fā)或多線程的應(yīng)用,更易于編寫利用多核的應(yīng)用程序。
3、可以幫助寫出健壯的代碼
4、更容易寫出易于閱讀、理解的優(yōu)雅代碼
簡單了解了函數(shù)式編程的歷史背景后。我們來看下什么是函數(shù)式編程。函數(shù)式編程它是一種編程范式,如面向過程的編程、面向?qū)ο蟮木幊潭际且环N編程范式。談函數(shù)式編程肯定離不開編程語言,那有哪些編程語言是屬于函數(shù)式編程語言呢?簡單點(diǎn)可以分為三類,第一類是純函數(shù)式編程語言,如原始的Lisp(不包括各種基于lisp的方言)、Haskell、F#等;第二類是混合型的,除了具備函數(shù)式編程的特性外,還有一些非函數(shù)式編程的特性,如scala,clojure(是基于Lisp的方言)等語言;第三類嚴(yán)格的說不能算函數(shù)式編程語言,但具備部分函數(shù)式編程語言的特性,如python , ruby, java8等。
二、函數(shù)式編程基本特征
對于函數(shù)式編程,有幾個(gè)非常重要的和基礎(chǔ)性的特征:
- 函數(shù)是一等公民:
所謂一等公民是指函數(shù)也有數(shù)據(jù)類型,函數(shù)與其他數(shù)據(jù)類型的變量或值一樣,處于平等地位,可以賦值給其它變量,也可以作為函數(shù)參數(shù),傳入另一個(gè)函數(shù),或者作為別的函數(shù)的返回值。
函數(shù)是一等公民是函數(shù)式編程范式最重要的特性和基礎(chǔ)。
- 不可變數(shù)據(jù)
所有的狀態(tài)(或變量)都是不可變的。你可以聲明一個(gè)狀態(tài),但是不能改變這個(gè)狀態(tài)。如果要變化,只能復(fù)制一個(gè)。
純函數(shù)式編程語言不使用任何可變數(shù)據(jù)結(jié)構(gòu)或變量。但在Scala等編程語言中,即支持不可變的數(shù)據(jù)結(jié)構(gòu)或變量,也支持可變的。
3.強(qiáng)調(diào)函數(shù)沒有"副作用"
所謂函數(shù)沒有副作用,意味著函數(shù)要保持獨(dú)立,一旦函數(shù)的輸入確定,輸出就是確定的,函數(shù)的執(zhí)行不會影響系統(tǒng)的狀態(tài),不會修改外部狀態(tài)。想象下,如果函數(shù)沒有副作用,那函數(shù)的執(zhí)行就可以緩存起來了,一旦函數(shù)執(zhí)行過一次,如果再次執(zhí)行,當(dāng)輸入和前面一樣的情況下,就直接可以用前面執(zhí)行的輸出結(jié)果,根本就不用再次運(yùn)算了,想象下,這個(gè)是否可大大提高程序運(yùn)行的效率。
3、一切皆是表達(dá)式
在函數(shù)式編程語言中,每一個(gè)語句都是一個(gè)表達(dá)式,都會有返回值。比如scala中的if-else控制結(jié)構(gòu)就是一個(gè)有返回值的表達(dá)式。這與命令式編程語言中的if-else語句顯著不同。這一特性有助于用不可變量編寫應(yīng)用程序。
除了這幾個(gè)核心特征外,還有很多其它特點(diǎn),比如:
支持匿名函數(shù)(在java中稱為lambda表達(dá)式)
遞歸不再是可有可無的,而是處于一個(gè)核心的位置(循環(huán)沒了,就是靠遞歸來解決的,遞歸最大的好處就簡化代碼,它可以把一個(gè)復(fù)雜的問題用很簡單的代碼描述出來。遞歸的精髓是描述問題,而這正是函數(shù)式編程的精髓。)
支持惰性求值,就是在需要的時(shí)候才計(jì)算表達(dá)式
除了這些特點(diǎn)外,理解函數(shù)式編程與傳統(tǒng)編程的區(qū)別,還有一個(gè)思維上的轉(zhuǎn)變。
傳統(tǒng)的編程語言,是一種命令式語言,命令式語言是跟“狀態(tài)”打交道的,基礎(chǔ)是“語句”(執(zhí)行某種操作,如賦值語句)。命令式程序可以看成一個(gè)指令序列,通過改變狀態(tài)的值來實(shí)現(xiàn)算法。
函數(shù)式語言是跟“值”打交道的,基礎(chǔ)是“表達(dá)式”(是一個(gè)單純的運(yùn)算過程,總是有返回值),函數(shù)式程序可以看成一個(gè)由輸入到輸出的函數(shù),而這個(gè)函數(shù)又由其他函數(shù)或者自身來構(gòu)造。實(shí)現(xiàn)算法的過程就是由語言提供的原始函數(shù)逐步構(gòu)造一個(gè)符合算法要求的函數(shù)的過程。
三、scala中的函數(shù)與方法
(一)基本概念
Scala語言即支持面向函數(shù)的編程,也支持面向?qū)ο蟮木幊蹋詓cala中既有函數(shù)也有方法,大多數(shù)情況下我們都可以不去理會它們之間的區(qū)別。但是有時(shí)候我們必須要了解他們之間的不同。
從廣義上說,方法也是一種函數(shù),只不過方法是面向?qū)ο缶幊讨械母拍睿椒ㄊ穷惖囊徊糠郑仨毻ㄟ^對象來調(diào)用。
在scala中,我們可以這么來區(qū)分方法與函數(shù)。使用關(guān)鍵字def定義的函數(shù)我們稱為方法,類似java類中的方法。而使用=>定義的函數(shù)我們稱為函數(shù),類似java8中的Lambda表達(dá)式。在本文中我們提到的方法與函數(shù)指按照這個(gè)規(guī)則來區(qū)分的。
下面我們來看一個(gè)例子:
class A{
def test1(a:Int)={
println(a)
a*2
}
val test2 = (a:Int)=>{
println(a)
a*2
}
}
上面代碼定義了一個(gè)類A,該類有兩個(gè)成員:
1)一是通過def關(guān)鍵字定義的方法test1,該方法有一個(gè)整型參數(shù),返回值也是一個(gè)整數(shù)。
2)二是通過關(guān)鍵字val定義了的變量test2,這個(gè)test2變量的值是一個(gè)函數(shù),也就是說變量test2的類型是一個(gè)函數(shù)類型。
我們只看test1方法的參數(shù)列表及后面的部分,再看test2變量的值的部分,發(fā)現(xiàn)非常類似,區(qū)別就是 = 和 =>。
下面我們看看如何使用test1方法和test2變量,在scala命令行中執(zhí)行,結(jié)果如下:
scala> val a = new A()
a: A = A@790765dc
scala> a.test1(10)
10
res16: Int = 20
scala> a.test2(10)
res17: Int = 20
可以看出,使用的方式和結(jié)果是一樣的。
scala中的函數(shù),在scala中還有幾個(gè)叫法,如Lambda表達(dá)式、Lambda函數(shù)、函數(shù)字面值、匿名函數(shù),看到這些叫法,都是一個(gè)意思。
(二)方法和函數(shù)的區(qū)別
1、用途上的區(qū)別
方法正如面向?qū)ο缶幊讨械母拍睿鳛轭惖某蓡T出現(xiàn),然后通過對象來調(diào)用,如:
class Plus{
def plus(a:Int,b:Int)= a+b
}
val obj =new Plus
obj.plus(2,3)
說明:也可以在方法的內(nèi)部通過def關(guān)鍵字定義一個(gè)內(nèi)部方法,不過這個(gè)方法只在包括它的方法內(nèi)有效。
函數(shù)最大的應(yīng)用場景是作為高階函數(shù)(可以是類的方法)的輸入或輸出。所謂高階函數(shù),指該函數(shù)的輸入?yún)?shù)中有函數(shù)類型的參數(shù),或者返回值類型是一個(gè)函數(shù)類型。如下面例子:
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個(gè)參數(shù)就是一個(gè)函數(shù)類型。在調(diào)用該方法時(shí),可以傳入該函數(shù)類型的不同函數(shù)實(shí)現(xiàn),如 (a:Int,b:Int)=>a+b 和 (a:Int,b:Int)=>a*b 分別是同一函數(shù)類型的兩個(gè)不同實(shí)例。
在scala中,函數(shù)是一種數(shù)據(jù)類型,其類型名的格式是:
(函數(shù)參數(shù)類型列表)=>函數(shù)返回值類型
函數(shù)可以沒有參數(shù),但()不能省,也可以由多個(gè)參數(shù)類型,之間以逗號分隔。注意,參數(shù)類型列表中只有類型名,沒有參數(shù)名,不要與具體的函數(shù)定義混淆了。
即: (Int,Int)=>Int 是一個(gè)函數(shù)類型; (a:Int,b:Int)=>a+b是該類型的一個(gè)實(shí)現(xiàn),是一個(gè)具體的函數(shù)。
我們再看一個(gè)返回函數(shù)類型的例子:
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方法返回的是一個(gè)函數(shù)類型,類型為(Int,Int)=>Int 。調(diào)用該方法得到的是一個(gè)(Int,Int)=>Int 類型的一個(gè)實(shí)例。
總結(jié)下,方法通常是作為類用來實(shí)現(xiàn)其設(shè)計(jì)、完成其某個(gè)功能的一段邏輯代碼。而函數(shù)則是用于完成特定的計(jì)算邏輯。
2、函數(shù)是一個(gè)值
在Scala中函數(shù)是一個(gè)值(即某個(gè)函數(shù)類型的一個(gè)值),但方法不是值。所以一個(gè)方法不能賦值給一個(gè)變量,而函數(shù)可以。函數(shù)即可以賦值給一個(gè)變量,也可以作為高階函數(shù)的輸入或輸出,但方法不可以。下面我們看一個(gè)例子:
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
^
從上面代碼的運(yùn)行可以看出,將test1函數(shù)賦值給一個(gè)變量是可以的,但將test2函數(shù)賦值給變量是會報(bào)錯(cuò)的。
我們在看一個(gè)例子:
class Person{
def cal()={
12
}
val re = cal //這里不是方法賦值,而是方法調(diào)用
println(re)
}
上面代碼中的val re = cal 不是將方法cal賦值給變量re,而是調(diào)用方法cal,將方法的執(zhí)行返回值返回給變量re。這是因?yàn)榉椒]有參數(shù)時(shí),調(diào)用時(shí)可以省去()。
四、scala函數(shù)的其它特性
下面介紹scala函數(shù)的其它特性。
(一)高階函數(shù)
所謂高階函數(shù)(或方法),指該函數(shù)的輸入?yún)?shù)中有函數(shù)類型的參數(shù),或者返回值類型是一個(gè)函數(shù)類型。
上面已經(jīng)有例子介紹,這里不再介紹。
(二)閉包
所謂閉包,是指一個(gè)函數(shù)可以使用包含它的上下文中(如方法)的數(shù)據(jù)。我們先看一個(gè)例子:
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方法返回了一個(gè)函數(shù),該函數(shù)使用了fun方法的參數(shù)num(是一個(gè)局部變量)。
scala中的函數(shù),還有柯里化、部分應(yīng)用函數(shù)、偏函數(shù)等函數(shù)式編程中的一些概念,在本文中不再介紹。
五、集合數(shù)據(jù)操作
函數(shù)式編程中最大的應(yīng)用場景是對集合數(shù)據(jù)的操作,大數(shù)據(jù)的處理核心也是對數(shù)據(jù)集合的處理,只不過是數(shù)據(jù)量非常大的集合。
scala通過高階函數(shù)提供了豐富的集合數(shù)據(jù)相關(guān)操作。我們下面通過例子來說明。
先看如何對一個(gè)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方法是一個(gè)高階函數(shù),其參數(shù)類型為(T)=>Boolean ,即參數(shù)是一個(gè)泛型函數(shù),該函數(shù)輸入?yún)?shù)類型是List中的元素類型,輸出值是一個(gè)布爾值。filter方法的功能是對每個(gè)元素都調(diào)用下其參數(shù)函數(shù),所有返回這為true的元素都會被保留,最后返回被保留的元素的一個(gè)新的List列表。
我們再看一個(gè)map操作,map操作的功能是一個(gè)集合映射成另一個(gè)新的集合,原集合的每個(gè)元素通過函數(shù)運(yùn)算得到一個(gè)新的值。如下面例子:
object Hello {
def main(args: Array[String]){
var list = List("hello","good","tom")
var newlist = list.map((item)=>item.length)
println(newlist)
}
}
運(yùn)行上面代碼,輸出List(5, 4, 3) ,這是因?yàn)閙ap方法也是一個(gè)高階函數(shù),每個(gè)元素通過其函數(shù)參數(shù)轉(zhuǎn)換成了一個(gè)新的元素,所有轉(zhuǎn)換的元素形成一個(gè)新的列表被返回。
scala的集合類型有多種豐富的高階方法,可以完成各種數(shù)據(jù)計(jì)算,我們這里只是舉例兩個(gè)很簡單的例子,來感受下函數(shù)式編程在集合數(shù)據(jù)處理中的強(qiáng)大作用。
六、小結(jié)
scala的函數(shù)式編程涉及的知識很多,本文只是介紹了最基礎(chǔ)的部分。