當咱們提及函數式編程來講,咱們會看到以下函數式編程的長相:javascript
上面的那些東西太抽象了,仍是讓咱們來循序漸近地看一些例子吧。html
咱們先用一個最簡單的例子來講明一下什麼是函數式編程。java
先看一個非函數式的例子:python
1
2
3
4
|
int
cnt;
void
increment(){
cnt++;
}
|
那麼,函數式的應該怎麼寫呢?ios
1
2
3
|
int
increment(
int
cnt){
return
cnt+1;
}
|
你可能會以爲這個例子太普通了。是的,這個例子就是函數式編程的準則:不依賴於外部的數據,並且也不改變外部數據的值,而是返回一個新的值給你。shell
咱們再來看一個簡單例子:express
1
2
3
4
5
6
7
8
9
10
|
def
inc(x):
def
incx(y):
return
x
+
y
return
incx
inc2
=
inc(
2
)
inc5
=
inc(
5
)
print
inc2(
5
)
# 輸出 7
print
inc5(
5
)
# 輸出 10
|
咱們能夠看到上面那個例子inc()函數返回了另外一個函數incx(),因而咱們能夠用inc()函數來構造各類版本的inc函數,好比:inc2()和inc5()。這個技術其實就是上面所說的Currying技術。從這個技術上,你可能體會到函數式編程的理念:把函數當成變量來用,關注於描述問題而不是怎麼實現,這樣可讓代碼更易讀。編程
在函數式編程中,咱們不該該用循環迭代的方式,咱們應該用更爲高級的方法,以下所示的Python代碼數組
1
2
3
|
name_len
=
map
(
len
, [
"hao"
,
"chen"
,
"coolshell"
])
print
name_len
# 輸出 [3, 4, 9]
|
你能夠看到這樣的代碼很易讀,由於,這樣的代碼是在描述要幹什麼,而不是怎麼幹。bash
咱們再來看一個Python代碼的例子:
1
2
3
4
5
6
|
def
toUpper(item):
return
item.upper()
upper_name
=
map
(toUpper, [
"hao"
,
"chen"
,
"coolshell"
])
print
upper_name
# 輸出 ['HAO', 'CHEN', 'COOLSHELL']
|
順便說一下,上面的例子個是否是和咱們的STL的transform有些像?
1
2
3
4
5
6
7
8
9
10
11
12
|
#include <iostream>
#include <algorithm>
#include <string>
using
namespace
std;
int
main() {
string s=
"hello"
;
string out;
transform(s.begin(), s.end(), back_inserter(out), ::
toupper
);
cout << out << endl;
// 輸出:HELLO
}
|
在上面Python的那個例子中咱們能夠看到,咱們寫義了一個函數toUpper,這個函數沒有改變傳進來的值,只是把傳進來的值作個簡單的操做,而後返回。而後,咱們把其用在map函數中,就能夠很清楚地描述出咱們想要幹什麼。而不會去理解一個在循環中的怎麼實現的代碼,最終在讀了不少循環的邏輯後才發現原來是這個或那個意思。 下面,咱們看看描述實現方法的過程式編程是怎麼玩的(看上去是否是不如函數式的清晰?):
1
2
3
4
|
upname
=
[
'HAO'
,
'CHEN'
,
'COOLSHELL'
]
lowname
=
[]
for
i
in
range
(
len
(upname)):
lowname.append( upname[i].lower() )
|
對於map咱們別忘了lambda表達式:你能夠簡單地理解爲這是一個inline的匿名函數。下面的lambda表達式至關於:def func(x): return x*x
1
2
3
|
squares
=
map
(
lambda
x: x
*
x,
range
(
9
))
print
squares
# 輸出 [0, 1, 4, 9, 16, 25, 36, 49, 64]
|
咱們再來看看reduce怎麼玩?(下面的lambda表達式中有兩個參數,也就是說每次從列表中取兩個值,計算結果後把這個值再放回去,下面的表達式至關於:((((1+2)+3)+4)+5) )
1
2
|
print
reduce
(
lambda
x, y: x
+
y, [
1
,
2
,
3
,
4
,
5
])
# 輸出 15
|
Python中的除了map和reduce外,還有一些別的如filter, find, all, any的函數作輔助(其它函數式的語言也有),可讓你的代碼更簡潔,更易讀。 咱們再來看一個比較複雜的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
num
=
[
2
,
-
5
,
9
,
7
,
-
2
,
5
,
3
,
1
,
0
,
-
3
,
8
]
positive_num_cnt
=
0
positive_num_sum
=
0
for
i
in
range
(
len
(num)):
if
num[i] >
0
:
positive_num_cnt
+
=
1
positive_num_sum
+
=
num[i]
if
positive_num_cnt >
0
:
average
=
positive_num_sum
/
positive_num_cnt
print
average
# 輸出 5
|
若是用函數式編程,這個例子能夠寫成這樣:
1
2
|
positive_num
=
filter
(
lambda
x: x>
0
, num)
average
=
reduce
(
lambda
x,y: x
+
y, positive_num)
/
len
( positive_num )
|
C++11玩的法:
1
2
3
4
5
6
7
8
9
10
11
12
|
#include <iostream>
#include <algorithm>
#include <numeric>
#include <string>
#include <vector>
using
namespace
std;
vector num {2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8};
vector p_num;
copy_if(num.begin(), num.end(), back_inserter(p_num), [](
int
i){
return
(i>0);} );
int
average = accumulate(p_num.begin(), p_num.end(), 0) / p_num.size();
cout <<
"averge: "
<< average << endl;
|
咱們能夠看到,函數式編程有以下好處:
1)代碼更簡單了。
2)數據集,操做,返回值都放到了一塊兒。
3)你在讀代碼的時候,沒有了循環體,因而就能夠少了些臨時變量,以及變量倒來倒去邏輯。
4)你的代碼變成了在描述你要幹什麼,而不是怎麼去幹。
最後,咱們來看一下Map/Reduce這樣的函數是怎麼來實現的(下面是Javascript代碼)
1
2
3
4
5
6
7
|
var
map =
function
(mappingFunction, list) {
var
result = [];
forEach(list,
function
(item) {
result.push(mappingFunction(item));
});
return
result;
};
|
下面是reduce函數的javascript實現(謝謝 @下雨在家 修正的我原來的簡單版本)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function
reduce(actionFunction, list, initial){
var
accumulate;
var
temp;
if
(initial){
accumulate = initial;
}
else
{
accumulate = list.shfit();
}
temp = list.shift();
while
(temp){
accumulate = actionFunction(accumulate,temp);
temp = list.shift();
}
return
accumulate;
};
|
前面提到過屢次的函數式編程關注的是:describe what to do, rather than how to do it. 因而,咱們把之前的過程式的編程範式叫作 Imperative Programming – 指令式編程,而把函數式的這種範式叫作 Declarative Programming – 聲明式編程。
下面咱們看一下相關的示例(本示例來自這篇文章 )。
好比,咱們有3輛車比賽,簡單起見,咱們分別給這3輛車有70%的機率能夠往前走一步,一共有5次機會,咱們打出每一次這3輛車的前行狀態。
對於Imperative Programming來講,代碼以下(Python):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
from
random
import
random
time
=
5
car_positions
=
[
1
,
1
,
1
]
while
time:
# decrease time
time
-
=
1
print
''
for
i
in
range
(
len
(car_positions)):
# move car
if
random() >
0.3
:
car_positions[i]
+
=
1
# draw car
print
'-'
*
car_positions[i]
|
咱們能夠把這個兩重循環變成一些函數模塊,這樣有利於咱們更容易地閱讀代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
from
random
import
random
def
move_cars():
for
i, _
in
enumerate
(car_positions):
if
random() >
0.3
:
car_positions[i]
+
=
1
def
draw_car(car_position):
print
'-'
*
car_position
def
run_step_of_race():
global
time
time
-
=
1
move_cars()
def
draw():
print
''
for
car_position
in
car_positions:
draw_car(car_position)
time
=
5
car_positions
=
[
1
,
1
,
1
]
while
time:
run_step_of_race()
draw()
|
上面的代碼,咱們能夠從主循環開始,咱們能夠很清楚地看到程序的主幹,由於咱們把程序的邏輯分紅了幾個函數,這樣一來,咱們的代碼邏輯也會變得幾個小碎片,因而咱們讀代碼時要考慮的上下文就少了不少,閱讀代碼也會更容易。不像第一個示例,若是沒有註釋和說明,你仍是須要花些時間理解一下。而把代碼邏輯封裝成了函數後,咱們就至關於給每一個相對獨立的程序邏輯取了個名字,因而代碼成了自解釋的。
可是,你會發現,封裝成函數後,這些函數都會依賴於共享的變量來同步其狀態。因而,咱們在讀代碼的過程時,每當咱們進入到函數裏,一量讀到訪問了一個外部的變量,咱們立刻要去查看這個變量的上下文,而後還要在大腦裏推演這個變量的狀態, 咱們才知道程序的真正邏輯。也就是說,這些函數間必需知道其它函數是怎麼修改它們之間的共享變量的,因此,這些函數是有狀態的。
咱們知道,有狀態並非一件很好的事情,不管是對代碼重用,仍是對代碼的並行來講,都是有反作用的。所以,咱們要想個方法把這些狀態搞掉,因而出現了咱們的 Functional Programming 的編程範式。下面,咱們來看看函數式的方式應該怎麼寫?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
from
random
import
random
def
move_cars(car_positions):
return
map
(
lambda
x: x
+
1
if
random() >
0.3
else
x,
car_positions)
def
output_car(car_position):
return
'-'
*
car_position
def
run_step_of_race(state):
return
{
'time'
: state[
'time'
]
-
1
,
'car_positions'
: move_cars(state[
'car_positions'
])}
def
draw(state):
print
''
print
'\n'
.join(
map
(output_car, state[
'car_positions'
]))
def
race(state):
draw(state)
if
state[
'time'
]:
race(run_step_of_race(state))
race({
'time'
:
5
,
'car_positions'
: [
1
,
1
,
1
]})
|
上面的代碼依然把程序的邏輯分紅了函數,不過這些函數都是functional的。由於它們有三個症狀:
1)它們之間沒有共享的變量。
2)函數間經過參數和返回值來傳遞數據。
3)在函數裏沒有臨時變量。
咱們還能夠看到,for循環被遞歸取代了(見race函數)—— 遞歸是函數式編程中帶用到的技術,正如前面所說的,遞歸的本質就是描述問題是什麼。
pipeline 管道借鑑於Unix Shell的管道操做——把若干個命令串起來,前面命令的輸出成爲後面命令的輸入,如此完成一個流式計算。(注:管道絕對是一個偉大的發明,他的設哲學就是KISS – 讓每一個功能就作一件事,並把這件事作到極致,軟件或程序的拼裝會變得更爲簡單和直觀。這個設計理念影響很是深遠,包括今天的Web Service,雲計算,以及大數據的流式計算等等)
好比,咱們以下的shell命令:
1
|
ps
auwwx |
awk
'{print $2}'
|
sort
-n |
xargs
echo
|
若是咱們抽象成函數式的語言,就像下面這樣:
1
|
xargs( echo, sort(n, awk(
'print $2'
, ps(auwwx))) )
|
也能夠相似下面這個樣子:
1
|
pids
=
for_each(result, [ps_auwwx, awk_p2, sort_n, xargs_echo])
|
好了,讓咱們來看看函數式編程的Pipeline怎麼玩?
咱們先來看一個以下的程序,這個程序的process()有三個步驟:
1)找出偶數。
2)乘以3
3)轉成字符串返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
def
process(num):
# filter out non-evens
if
num
%
2
!
=
0
:
return
num
=
num
*
3
num
=
'The Number: %s'
%
num
return
num
nums
=
[
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
]
for
num
in
nums:
print
process(num)
# 輸出:
# None
# The Number: 6
# None
# The Number: 12
# None
# The Number: 18
# None
# The Number: 24
# None
# The Number: 30
|
咱們能夠看到,輸出的並不夠完美,另外,代碼閱讀上若是沒有註釋,你也會比較暈。下面,咱們來看看函數式的pipeline(第一種方式)應該怎麼寫?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
def
even_filter(nums):
for
num
in
nums:
if
num
%
2
=
=
0
:
yield
num
def
multiply_by_three(nums):
for
num
in
nums:
yield
num
*
3
def
convert_to_string(nums):
for
num
in
nums:
yield
'The Number: %s'
%
num
nums
=
[
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
]
pipeline
=
convert_to_string(multiply_by_three(even_filter(nums)))
for
num
in
pipeline:
print
num
# 輸出:
# The Number: 6
# The Number: 12
# The Number: 18
# The Number: 24
# The Number: 30
|
咱們動用了Python的關鍵字 yield,這個關鍵字主要是返回一個Generator,yield 是一個相似 return 的關鍵字,只是這個函數返回的是個Generator-生成器。所謂生成器的意思是,yield返回的是一個可迭代的對象,並無真正的執行函數。也就是說,只有其返回的迭代對象被真正迭代時,yield函數纔會正真的運行,運行到yield語句時就會停住,而後等下一次的迭代。(這個是個比較詭異的關鍵字)這就是lazy evluation。
好了,根據前面的原則——「使用Map & Reduce,不要使用循環」,那咱們用比較純樸的Map & Reduce吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
def
even_filter(nums):
return
filter
(
lambda
x: x
%
2
=
=
0
, nums)
def
multiply_by_three(nums):
return
map
(
lambda
x: x
*
3
, nums)
def
convert_to_string(nums):
return
map
(
lambda
x:
'The Number: %s'
%
x, nums)
nums
=
[
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
]
pipeline
=
convert_to_string(
multiply_by_three(
even_filter(nums)
)
)
for
num
in
pipeline:
print
num
|
可是他們的代碼須要嵌套使用函數,這個有點不爽,若是咱們能像下面這個樣子就行了(第二種方式)。
1
2
3
|
pipeline_func(nums, [even_filter,
multiply_by_three,
convert_to_string])
|
那麼,pipeline_func 實現以下:
1
2
3
4
|
def
pipeline_func(data, fns):
return
reduce
(
lambda
a, x: x(a),
fns,
data)
|
好了,在讀過這麼多的程序後,你能夠回頭看一下這篇文章的開頭對函數式編程的描述,可能你就更有感受了。
最後,我但願這篇淺顯易懂的文章能讓你感覺到函數式編程的思想,就像OO編程,泛型編程,過程式編程同樣,咱們不用太糾結是否是咱們的程序就是OO,就是functional的,咱們重要的品味其中的味道。
轉自:http://coolshell.cn/articles/10822.html (陳皓)