自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

手把手介紹函數(shù)式編程:從命令式重構(gòu)到函數(shù)式

新聞 前端
本文是一篇手把手的函數(shù)式編程入門介紹,借助代碼示例講解細(xì)膩。

本文是一篇手把手的函數(shù)式編程入門介紹,借助代碼示例講解細(xì)膩。但又不乏洞見,第一節(jié)中列舉和點評了函數(shù)式種種讓眼花繚亂的特質(zhì),給出了『理解函數(shù)式特質(zhì)的指南針:函數(shù)式代碼的核心特質(zhì)就一條, 無副作用 』,相信這個指南針對于有積極學(xué)過挖過函數(shù)式的同學(xué)看來更是有相知恨晚的感覺。

希望看了這篇文章之后,能在學(xué)習(xí)和使用函數(shù)式編程的旅途中不迷路哦,兄die~

PS:本人是在《 Functional Programming, Simplified(Scala edition) 》這本書了解到這篇文章。這本書由淺入深循序漸進(jìn)地對 FP 做了體系講解,力薦!

手把手介紹函數(shù)式編程:從命令式重構(gòu)到函數(shù)式

有很多函數(shù)式編程文章講解了抽象的函數(shù)式技術(shù),也就是組合( composition )、管道( pipelining )、高階函數(shù)( higher order function )。本文希望以另辟蹊徑的方式來講解函數(shù)式:首先展示我們平常編寫的命令式而非函數(shù)式的代碼示例,然后將這些示例重構(gòu)成函數(shù)式風(fēng)格。

本文的第一部分選用了簡短的數(shù)據(jù)轉(zhuǎn)換循環(huán),將它們重構(gòu)成函數(shù)式的 map 和 reduce 。第二部分則對更長的循環(huán)代碼,將它們分解成多個單元,然后重構(gòu)各個單元成函數(shù)式的。第三部分選用的是有一系列連續(xù)的數(shù)據(jù)轉(zhuǎn)換循環(huán)代碼,將其拆解成為一個函數(shù)式管道( functional pipeline)。

示例代碼用的是 Python 語言,因為多數(shù)人都覺得 Python 易于閱讀。示例代碼避免使用 Python 范的( pythonic )代碼,以便展示出各個語言通用的函數(shù)式技術(shù): map 、 reduce 和管道。所有示例都用的是 Python 2 。

[[276305]]

  • 理解函數(shù)式特質(zhì)的指南針
  • 不要迭代列表,使用 map 和 reduce
  • 聲明方式編寫代碼,而非命令式
  • 現(xiàn)在開始我們可以做什么?

理解函數(shù)式特質(zhì)的指南針

當(dāng)人們談?wù)摵瘮?shù)式編程時,提到了多到令人迷路的『函數(shù)式』特質(zhì)( characteristics ):

  • 人們會提到不可變數(shù)據(jù)( immutable data )、一等公民的函數(shù)( first class function )和尾調(diào)用優(yōu)化( tail call optimisation )。這些是 有助于函數(shù)式編程的語言特性 。
  • 人們也會提到 map 、 reduce 、管道、遞歸( recursing )、柯里化( currying )以及高階函數(shù)的使用。這些是 用于編寫函數(shù)式代碼的編程技術(shù) 。
  • 人們還會提到并行化( parallelization )、惰性求值( lazy evaluation )和確定性( determinism )。這些是 函數(shù)式程序的優(yōu)點 。

無視這一切。函數(shù)式代碼的核心特質(zhì)就一條: 無副作用 ( side effect )。即代碼邏輯不依賴于當(dāng)前函數(shù)之外的數(shù)據(jù),并且也不會更改當(dāng)前函數(shù)之外的數(shù)據(jù)。所有其他的『函數(shù)式』特質(zhì)都可以從這一條派生出來。在你學(xué)習(xí)過程中,請以此作為指南針。不要再迷路哦,兄die~

這是一個非函數(shù)式的函數(shù):

  1. a = 0 
  2. def increment(): 
  3.     global a 
  4.     a += 1 

而這是一個函數(shù)式的函數(shù):

  1. def increment(a): 
  2.     return a + 1 

不要迭代列表,使用 map 和 reduce

map

map 輸入一個函數(shù)和一個集合,創(chuàng)建一個新的空集合,在原來集合的每個元素上運行該函數(shù),并將各個返回值插入到新集合中,然后返回新的集合。

這是一個簡單的 map ,它接受一個名字列表并返回這些名字的長度列表:

  1. name_lengths = map(len, ["Mary""Isla""Sam"]) 
  2. print name_lengths 
  3. # => [443

這是一個 map ,對傳遞的集合中的每個數(shù)字進(jìn)行平方:

  1. squares = map(lambda x: x * x, [01234]) 
  2.  
  3. print squares 
  4. # => [014916

這個 map 沒有輸入命名函數(shù),而是一個匿名的內(nèi)聯(lián)函數(shù),用 lambda 關(guān)鍵字來定義。 lambda的參數(shù)定義在冒號的左側(cè)。函數(shù)體定義在冒號的右側(cè)。(隱式)返回的是函數(shù)體的運行結(jié)果。

下面的非函數(shù)式代碼輸入一個真實名字的列表,替換成隨機(jī)分配的代號。

  1. import random 
  2.  
  3. names = ['Mary''Isla''Sam'
  4. code_names = ['Mr. Pink''Mr. Orange''Mr. Blonde'
  5.  
  6. for i in range(len(names)): 
  7.     names[i] = random.choice(code_names) 
  8.  
  9. print names 
  10. # => ['Mr. Blonde''Mr. Blonde''Mr. Blonde'

(如你所見,這個算法可能會為多個秘密特工分配相同的秘密代號,希望這不會因此導(dǎo)致混淆了秘密任務(wù)。)

可以用 map 重寫成:

  1. import random 
  2.  
  3. names = ['Mary''Isla''Sam'
  4.  
  5. secret_names = map(lambda x: random.choice(['Mr. Pink'
  6.                                             'Mr. Orange'
  7.                                             'Mr. Blonde']), 
  8.                    names) 

練習(xí)1:嘗試將下面的代碼重寫為 map ,輸入一個真實名字列表,替換成用更可靠策略生成的代號。

  1. names = ['Mary''Isla''Sam'
  2.  
  3. for i in range(len(names)): 
  4.     names[i] = hash(names[i]) 
  5.  
  6. print names 
  7. # => [63068197961336869418135353348168144921, -1228887169324443034

(希望特工會留下美好的回憶,在秘密任務(wù)期間能記得住搭檔的秘密代號。)

我的實現(xiàn)方案:

  1. names = ['Mary''Isla''Sam'
  2.  
  3. secret_names = map(hash, names) 

reduce

reduce 輸入一個函數(shù)和一個集合,返回通過合并集合元素所創(chuàng)建的值。

這是一個簡單的 reduce ,返回集合所有元素的總和。

  1. sum = reduce(lambda a, x: a + x, [01234]) 
  2.  
  3. print sum 
  4. # => 10 

x 是迭代的當(dāng)前元素。 a 是累加器( accumulator ),它是在前一個元素上執(zhí)行 lambda 的返回值。 reduce() 遍歷所有集合元素。對于每一個元素,運行以當(dāng)前的 a 和 x 為參數(shù)運行 lambda ,返回結(jié)果作為下一次迭代的 a 。

在第一次迭代時, a 是什么值?并沒有前一個的迭代結(jié)果可以傳遞。 reduce() 使用集合中的第一個元素作為第一次迭代中的 a 值,從集合的第二個元素開始迭代。也就是說,第一個 x 是集合的第二個元素。

下面的代碼計算單詞 'Sam' 在字符串列表中出現(xiàn)的次數(shù):

  1. sentences = ['Mary read a story to Sam and Isla.'
  2.              'Isla cuddled Sam.'
  3.              'Sam chortled.'
  4.  
  5. sam_count = 0 
  6. for sentence in sentences: 
  7.     sam_count += sentence.count('Sam'
  8.  
  9. print sam_count 
  10. # => 3 

這與下面使用 reduce 的代碼相同:

  1. sentences = ['Mary read a story to Sam and Isla.'
  2.              'Isla cuddled Sam.'
  3.              'Sam chortled.'
  4.  
  5. sam_count = reduce(lambda a, x: a + x.count('Sam'), 
  6.                    sentences, 
  7.                    0

這段代碼是如何產(chǎn)生初始的 a 值? 'Sam' 出現(xiàn)次數(shù)的初始值不能是 'Mary read a story to Sam and Isla.' 。初始累加器用 reduce() 的第三個參數(shù)指定。這樣就允許使用與集合元素不同類型的值。

為什么 map 和 reduce 更好?

  1. 這樣的做法通常會是一行簡潔的代碼。
  2. 迭代的重要部分 —— 集合、操作和返回值 —— 以 map 和 reduce 方式總是在相同的位置。
  3. 循環(huán)中的代碼可能會影響在它之前定義的變量或在它之后運行的代碼。按照約定, map 和 reduce 都是函數(shù)式的。
  4. map 和 reduce 是基本原子操作。
    • 閱讀 for 循環(huán)時,必須一行一行地才能理解整體邏輯。往往沒有什么規(guī)則能保證以一個固定結(jié)構(gòu)來明確代碼的表義。
    • 相比之下, map 和 reduce 則是一目了然表現(xiàn)出了可以組合出復(fù)雜算法的構(gòu)建塊( building block )及其相關(guān)的元素,代碼閱讀者可以迅速理解并抓住整體脈絡(luò)?!号丁@段代碼正在轉(zhuǎn)換每個集合元素;丟棄了一些轉(zhuǎn)換結(jié)果;然后將剩下的元素合并成單個輸出結(jié)果?!?/li>
  5. map 和 reduce 有很多朋友,提供有用的、對基本行為微整的版本。比如: filter 、 all、 any 和 find 。

練習(xí)2:嘗試使用 map 、 reduce 和 filter 重寫下面的代碼。 filter 需要一個函數(shù)和一個集合,返回結(jié)果是函數(shù)返回 True 的所有集合元素。

  1. people = [{'name''Mary''height'160}, 
  2.           {'name''Isla''height'80}, 
  3.           {'name''Sam'}] 
  4.  
  5. height_total = 0 
  6. height_count = 0 
  7. for person in people: 
  8.     if 'height' in person: 
  9.         height_total += person['height'
  10.         height_count += 1 
  11.  
  12. if height_count > 0
  13.     average_height = height_total / height_count 
  14.  
  15.     print average_height 
  16.     # => 120 

如果上面這段代碼看起來有些燒腦,我們試試不以在數(shù)據(jù)上操作為中心的思考方式。而是想一想數(shù)據(jù)所經(jīng)歷的狀態(tài):從人字典的列表轉(zhuǎn)換成平均身高。不要將多個轉(zhuǎn)換混在一起。每個轉(zhuǎn)換放在一個單獨的行上,并將結(jié)果分配一個有描述性命名的變量。代碼工作之后,再合并縮減代碼。

我的實現(xiàn)方案:

  1. people = [{'name''Mary''height'160}, 
  2.           {'name''Isla''height'80}, 
  3.           {'name''Sam'}] 
  4.  
  5. heights = map(lambda x: x['height'], 
  6.               filter(lambda x: 'height' in x, people)) 
  7.  
  8. if len(heights) > 0
  9.     from operator import add 
  10.     average_height = reduce(add, heights) / len(heights) 

聲明方式編寫代碼,而非命令式

下面的程序演示三輛賽車的比賽。每過一段時間,賽車可能向前跑了,也可能拋錨而原地不動。在每個時間段,程序打印出目前為止的賽車路徑。五個時間段后比賽結(jié)束。

這是個示例輸出:

  1. -- 
  2. -- 
  3.  
  4. -- 
  5. -- 
  6. --- 
  7.  
  8. --- 
  9. -- 
  10. --- 
  11.  
  12. ---- 
  13. --- 
  14. ---- 
  15.  
  16. ---- 
  17. ---- 
  18. ----- 

這是程序?qū)崿F(xiàn):

  1. from random import random 
  2.  
  3. time = 5 
  4. car_positions = [111
  5.  
  6. while time: 
  7.     # decrease time 
  8.     time -= 1 
  9.  
  10.     print '' 
  11.     for i in range(len(car_positions)): 
  12.         # move car 
  13.         if random() > 0.3
  14.         car_positions[i] += 1 
  15.  
  16.         # draw car 
  17.         print '-' * car_positions[i] 

這份代碼是命令式的。函數(shù)式版本則是聲明性的,描述要做什么,而不是如何做。

使用函數(shù)

通過將代碼片段打包到函數(shù)中,程序可以更具聲明性。

  1. from random import random 
  2.  
  3. def move_cars(): 
  4.     for i, _ in enumerate(car_positions): 
  5.         if random() > 0.3
  6.             car_positions[i] += 1 
  7.  
  8. def draw_car(car_position): 
  9.     print '-' * car_position 
  10.  
  11. def run_step_of_race(): 
  12.     global time 
  13.     time -= 1 
  14.     move_cars() 
  15.  
  16. def draw(): 
  17.     print '' 
  18.     for car_position in car_positions: 
  19.         draw_car(car_position) 
  20.  
  21. time = 5 
  22. car_positions = [111
  23.  
  24. while time: 
  25.     run_step_of_race() 
  26.     draw() 

要理解這個程序,讀者只需讀一下主循環(huán)?!喝绻€剩下時間,請跑一步,然后畫出線圖。再次檢查時間?!蝗绻x者想要了解更多關(guān)于比賽步驟或畫圖的含義,可以閱讀對應(yīng)函數(shù)的代碼。

沒什么要再說明的了。 代碼是自描述的。

拆分代碼成函數(shù)是一種很好的、簡單易行的方法,能使代碼更具可讀性。

這個技術(shù)使用函數(shù),但將函數(shù)用作子例程( sub-routine ),用于打包代碼。對照上文說的指南針,這樣的代碼并不是函數(shù)式的。實現(xiàn)中的函數(shù)使用了沒有作為參數(shù)傳遞的狀態(tài),即通過更改外部變量而不是返回值來影響函數(shù)周圍的代碼。要確認(rèn)函數(shù)真正做了什么,讀者必須仔細(xì)閱讀每一行。如果找到一個外部變量,必須反查它的源頭,并檢查其他哪些函數(shù)更改了這個變量。

消除狀態(tài)

下面是賽車代碼的函數(shù)式版本:

  1. from random import random 
  2.  
  3. def move_cars(car_positions): 
  4.     return map(lambda x: x + 1 if random() > 0.3 else x, 
  5.                car_positions) 
  6.  
  7. def output_car(car_position): 
  8.     return '-' * car_position 
  9.  
  10. def run_step_of_race(state): 
  11.     return {'time': state['time'] - 1
  12.             'car_positions': move_cars(state['car_positions'])} 
  13.  
  14. def draw(state): 
  15.     print '' 
  16.     print '\n'.join(map(output_car, state['car_positions'])) 
  17.  
  18. def race(state): 
  19.     draw(state) 
  20.     if state['time']: 
  21.         race(run_step_of_race(state)) 
  22.  
  23. race({'time'5
  24.       'car_positions': [111]}) 

代碼仍然是分解成函數(shù)。但這些函數(shù)是函數(shù)式的,有三個跡象表明這點:

  1. 不再有任何共享變量。 time 與 car_position 作為參數(shù)傳入 race() 。
  2. 函數(shù)是有參數(shù)的。
  3. 在函數(shù)內(nèi)部沒有變量實例化。所有數(shù)據(jù)更改都使用返回值完成?;?nbsp;run_step_of_race() 的結(jié)果, race() 做遞歸調(diào)用。每當(dāng)一個步驟生成一個新狀態(tài)時,立即傳遞到下一步。

讓我們另外再來看看這么兩個函數(shù), zero() 和 one() :

  1. def zero(s): 
  2.     if s[0] == "0"
  3.         return s[1:] 
  4.  
  5. def one(s): 
  6.     if s[0] == "1"
  7.         return s[1:] 

zero() 輸入一個字符串 s 。如果第一個字符是 '0' ,則返回字符串的其余部分。如果不是,則返回 None , Python 函數(shù)的默認(rèn)返回值。 one() 做同樣的事情,但關(guān)注的是第一個字符 '1'。

假設(shè)有一個叫做 rule_sequence() 的函數(shù),輸入一個字符串和規(guī)則函數(shù)的列表,比如 zero()和 one() :

  1. 調(diào)用字符串上的第一個規(guī)則。
  2. 除非 None 返回,否則它將獲取返回值并在其上調(diào)用第二個規(guī)則。
  3. 除非 None 返回,否則它將獲取返回值并在其上調(diào)用第三個規(guī)則。
  4. 等等。
  5. 如果任何規(guī)則返回 None ,則 rule_sequence() 停止并返回 None 。
  6. 否則,它返回最終規(guī)則的返回值。

下面是一些示例輸入和輸出:

  1. print rule_sequence('0101', [zero, one, zero]) 
  2. # => 1 
  3.  
  4. print rule_sequence('0101', [zero, zero]) 
  5. # => None 

這是命令式版本的 rule_sequence() 實現(xiàn):

  1. def rule_sequence(s, rules): 
  2.     for rule in rules: 
  3.         s = rule(s) 
  4.         if s == None: 
  5.             break 
  6.  
  7.     return s 

練習(xí)3:上面的代碼使用循環(huán)來實現(xiàn)。通過重寫為遞歸來使其更具聲明性。

我的實現(xiàn)方案:

  1. def rule_sequence(s, rules): 
  2.     if s == None or not rules: 
  3.         return s 
  4.     else
  5.         return rule_sequence(rules[0](s), rules[1:]) 

使用管道

在上一節(jié)中,我們重寫一些命令性循環(huán)成為調(diào)用輔助函數(shù)的遞歸。在本節(jié)中,將使用稱為管道的技術(shù)重寫另一類型的命令循環(huán)。

下面的循環(huán)對樂隊字典執(zhí)行轉(zhuǎn)換,字典包含了樂隊名、錯誤的所屬國家和活躍狀態(tài)。

  1. bands = [{'name''sunset rubdown''country''UK''active': False}, 
  2.          {'name''women''country''Germany''active': False}, 
  3.          {'name''a silver mt. zion''country''Spain''active': True}] 
  4.  
  5. def format_bands(bands): 
  6.     for band in bands: 
  7.         band['country'] = 'Canada' 
  8.         band['name'] = band['name'].replace('.'''
  9.         band['name'] = band['name'].title() 
  10.  
  11. format_bands(bands) 
  12.  
  13. print bands 
  14. # => [{'name''Sunset Rubdown''active': False, 'country''Canada'}, 
  15. #     {'name''Women''active': False, 'country''Canada' }, 
  16. #     {'name''A Silver Mt Zion''active': True, 'country''Canada'}] 

看到這樣的函數(shù)命名讓人感受到一絲的憂慮,命名中的 format 表義非常模糊。仔細(xì)檢查代碼后,憂慮逆流成河。在循環(huán)的實現(xiàn)中做了三件事:

  1. 'country' 鍵的值設(shè)置成了 'Canada' 。
  2. 刪除了樂隊名中的標(biāo)點符號。
  3. 樂隊名改成首字母大寫。

我們很難看出這段代碼意圖是什么,也很難看出這段代碼是否完成了它看起來要做的事情。代碼難以重用、難以測試且難以并行化。

與下面實現(xiàn)對比一下:

  1. print pipeline_each(bands, [set_canada_as_country, 
  2.                             strip_punctuation_from_name, 
  3.                             capitalize_names]) 

這段代碼很容易理解。給人的印象是輔助函數(shù)是函數(shù)式的,因為它們看過來是串聯(lián)在一起的。前一個函數(shù)的輸出成為下一個的輸入。如果是函數(shù)式的,就很容易驗證。也易于重用、易于測試且易于并行化。

pipeline_each() 的功能就是將樂隊一次一個地傳遞給一個轉(zhuǎn)換函數(shù),比如 set_canada_as_country() 。將轉(zhuǎn)換函數(shù)應(yīng)用于所有樂隊后, pipeline_each() 將轉(zhuǎn)換后的樂隊打包起來。然后,打包的樂隊傳遞給下一個轉(zhuǎn)換函數(shù)。

我們來看看轉(zhuǎn)換函數(shù)。

  1. def assoc(_d, key, value): 
  2.     from copy import deepcopy 
  3.     d = deepcopy(_d) 
  4.     d[key] = value 
  5.     return d 
  6.  
  7. def set_canada_as_country(band): 
  8.     return assoc(band, 'country'"Canada"
  9.  
  10. def strip_punctuation_from_name(band): 
  11.     return assoc(band, 'name', band['name'].replace('.''')) 
  12.  
  13. def capitalize_names(band): 
  14.     return assoc(band, 'name', band['name'].title()) 

每個函數(shù)都將樂隊的一個鍵與一個新值相關(guān)聯(lián)。如果不變更原樂隊,沒有簡單的方法可以直接實現(xiàn)。 assoc() 通過使用 deepcopy() 生成傳入字典的副本來解決此問題。每個轉(zhuǎn)換函數(shù)都對副本進(jìn)行修改并返回該副本。

一切似乎都很好。當(dāng)鍵與新值相關(guān)聯(lián)時,可以保護(hù)原樂隊字典免于被變更。但是上面的代碼中還有另外兩個潛在的變更。在 strip_punctuation_from_name() 中,原來的樂隊名通過調(diào)用 replace() 生成無標(biāo)點的樂隊名。在 capitalize_names() 中,原來的樂隊名通過調(diào)用 title() 生成大寫樂隊名。如果 replace() 和 title() 不是函數(shù)式的,則 strip_punctuation_from_name()和 capitalize_names() 也將不是函數(shù)式的。

幸運的是, replace() 和 title() 不會變更他們操作的字符串。這是因為字符串在 Python中是不可變的( immutable )。例如,當(dāng) replace() 對樂隊名字符串進(jìn)行操作時,將復(fù)制原來的樂隊名并在副本上執(zhí)行 replace() 調(diào)用。Phew~有驚無險!

Python 中字符串和字典之間在可變性上不同的這種對比彰顯了像 Clojure 這樣語言的吸引力。 Clojure 程序員完全不需要考慮是否會改變數(shù)據(jù)。 Clojure 的數(shù)據(jù)結(jié)構(gòu)是不可變的。

練習(xí)4:嘗試編寫 pipeline_each 函數(shù)的實現(xiàn)。想想操作的順序。數(shù)組中的樂隊一次一個傳遞到第一個變換函數(shù)。然后返回的結(jié)果樂隊數(shù)組中一次一個樂隊傳遞給第二個變換函數(shù)。以此類推。

我的實現(xiàn)方案:

  1. def pipeline_each(data, fns): 
  2.     return reduce(lambda a, x: map(x, a), 
  3.                   fns, 
  4.                   data) 

所有三個轉(zhuǎn)換函數(shù)都可以歸結(jié)為對傳入的樂隊的特定字段進(jìn)行更改??梢杂?nbsp;call() 來抽象, call() 傳入一個函數(shù)和鍵名,用鍵對應(yīng)的值來調(diào)用這個函數(shù)。

  1. set_canada_as_country = call(lambda x: 'Canada''country'
  2. strip_punctuation_from_name = call(lambda x: x.replace('.'''), 'name'
  3. capitalize_names = call(str.title, 'name'
  4.  
  5. print pipeline_each(bands, [set_canada_as_country, 
  6.                     strip_punctuation_from_name, 
  7.                     capitalize_names]) 

或者,如果我們愿意為了簡潔而犧牲一些可讀性,那么可以寫成:

  1. print pipeline_each(bands, [call(lambda x: 'Canada''country'), 
  2.                             call(lambda x: x.replace('.'''), 'name'), 
  3.                             call(str.title, 'name')]) 

call() 的實現(xiàn)代碼:

  1. def assoc(_d, key, value): 
  2.     from copy import deepcopy 
  3.     d = deepcopy(_d) 
  4.     d[key] = value 
  5.     return d 
  6.  
  7. def call(fn, key): 
  8.     def apply_fn(record): 
  9.         return assoc(record, key, fn(record.get(key))) 
  10.     return apply_fn 

上面的實現(xiàn)中有不少內(nèi)容要講,讓我們一點一點地來說明:

  1. call() 是一個高階函數(shù)。高階函數(shù)是指將函數(shù)作為參數(shù),或返回函數(shù)?;蛘?,就像 call(),輸入和返回2者都是函數(shù)。
  2. apply_fn() 看起來與三個轉(zhuǎn)換函數(shù)非常相似。輸入一個記錄(一個樂隊), record[key] 是查找出值;再以值為參數(shù)調(diào)用 fn ,將調(diào)用結(jié)果賦值回記錄的副本;最后返回副本。
  3. call() 不做任何實際的事。而是調(diào)用 apply_fn() 時完成需要做的事。在上面的示例的 pipeline_each() 中,一個 apply_fn() 實例會設(shè)置傳入樂隊的 'country' 成 'Canada' ;另一個實例則將傳入樂隊的名字轉(zhuǎn)成大寫。
  4. 當(dāng)運行一個 apply_fn() 實例, fn 和 key 2個變量并沒有在自己的作用域中,既不是 apply_fn() 的參數(shù),也不是本地變量。但2者仍然可以訪問。
    • 當(dāng)定義一個函數(shù)時,會保存這個函數(shù)能閉包進(jìn)來( close over )的變量引用:在這個函數(shù)外層作用域中定義的變量。這些變量可以在該函數(shù)內(nèi)使用。
    • 當(dāng)函數(shù)運行并且其代碼引用變量時, Python 會在本地變量和參數(shù)中查找變量。如果沒有找到,則會在保存的引用中查找閉包進(jìn)來的變量。就在這里,會發(fā)現(xiàn) fn 和 key 。
  5. 在 call() 代碼中沒有涉及樂隊列表。這是因為 call() ,無論要處理的對象是什么,可以為任何程序生成管道函數(shù)。函數(shù)式編程的一大關(guān)注點就是構(gòu)建通用的、可重用的和可組合的函數(shù)所組成的庫。

完美!閉包( closure )、高階函數(shù)以及變量作用域,在上面的幾段代碼中都涉及了。嗯,理解完了上面這些內(nèi)容,是時候來個驢肉火燒打賞一下自己。 :sushi:

最后還差實現(xiàn)一段處理樂隊的邏輯:刪除除名字和國家之外的內(nèi)容。 extract_name_and_country() 可以把這些信息提取出來:

  1. def extract_name_and_country(band): 
  2.     plucked_band = {} 
  3.     plucked_band['name'] = band['name'
  4.     plucked_band['country'] = band['country'
  5.     return plucked_band 
  6.  
  7. print pipeline_each(bands, [call(lambda x: 'Canada''country'), 
  8.                             call(lambda x: x.replace('.'''), 'name'), 
  9.                             call(str.title, 'name'), 
  10.                             extract_name_and_country]) 
  11.  
  12. # => [{'name''Sunset Rubdown''country''Canada'}, 
  13. #     {'name''Women''country''Canada'}, 
  14. #     {'name''A Silver Mt Zion''country''Canada'}] 

extract_name_and_country() 本可以寫成名為 pluck() 的通用函數(shù)。 pluck() 使用起來是這個樣子:

【譯注】作者這里用了虛擬語氣『*本*可以』。

言外之意是,在實踐中為了更具體直白地表達(dá)出業(yè)務(wù),可能不需要進(jìn)一步抽象成 pluck() 。

  1. print pipeline_each(bands, [call(lambda x: 'Canada''country'), 
  2.                             call(lambda x: x.replace('.'''), 'name'), 
  3.                             call(str.title, 'name'), 
  4.                             pluck(['name''country'])]) 

練習(xí)5: pluck() 輸入是要從每條記錄中提取鍵的列表。試著實現(xiàn)一下。它會是一個高階函數(shù)。

我的實現(xiàn)方案:

  1. def pluck(keys): 
  2.     def pluck_fn(record): 
  3.         return reduce(lambda a, x: assoc(a, x, record[x]), 
  4.                       keys, 
  5.                       {}) 
  6.     return pluck_fn 

現(xiàn)在開始我們可以做什么?

函數(shù)式代碼與其他風(fēng)格的代碼可以很好地共存。本文中的轉(zhuǎn)換實現(xiàn)可以應(yīng)用于任何語言的任何代碼庫。試著應(yīng)用到你自己的代碼中。

想想特工瑪麗、伊絲拉和山姆。轉(zhuǎn)換列表迭代為 map 和 reduce 。

想想車賽。將代碼分解為函數(shù)。將這些函數(shù)轉(zhuǎn)成函數(shù)式的。將重復(fù)過程的循環(huán)轉(zhuǎn)成遞歸。

想想樂隊。將一系列操作轉(zhuǎn)為管道。

注:

  1. 不可變數(shù)據(jù)是無法更改的。某些語言(如 Clojure )默認(rèn)就是所有值都不可變。任何『變更』操作都會復(fù)制該值,更改副本然后返回更改后的副本。這消除了不完整模型下程序可能進(jìn)入狀態(tài)所帶來的 Bug 。
  2. 支持一等公民函數(shù)的語言允許像任何其他值一樣對待函數(shù)。這意味著函數(shù)可以創(chuàng)建,傳遞給函數(shù),從函數(shù)返回,以及存儲在數(shù)據(jù)結(jié)構(gòu)中。
  3. 尾調(diào)用優(yōu)化是一個編程語言特性。函數(shù)遞歸調(diào)用時,會創(chuàng)建一個新的棧幀( stack frame)。棧幀用于存儲當(dāng)前函數(shù)調(diào)用的參數(shù)和本地值。如果函數(shù)遞歸很多次,解釋器或編譯器可能會耗盡內(nèi)存。支持尾調(diào)用優(yōu)化的語言為其整個遞歸調(diào)用序列重用相同的棧幀。像 Python 這樣沒有尾調(diào)用優(yōu)化的語言通常會限制函數(shù)遞歸的次數(shù)(如數(shù)千次)。對于上面例子中 race() 函數(shù),因為只有5個時間段,所以是安全的。
  4. 柯里化( currying )是指將一個帶有多個參數(shù)的函數(shù)轉(zhuǎn)換成另一個函數(shù),這個函數(shù)接受第一個參數(shù),并返回一個接受下一個參數(shù)的函數(shù),依此類推所有參數(shù)。
  5. 并行化( parallelization )是指,在沒有同步的情況下,相同的代碼可以并發(fā)運行。這些并發(fā)處理通常運行在多個處理器上。
  6. 惰性求值( lazy evaluation )是一種編譯器技術(shù),可以避免在需要結(jié)果之前運行代碼。
  7. 如果每次重復(fù)執(zhí)行都產(chǎn)生相同的結(jié)果,則過程就是確定性的。

 

責(zé)任編輯:張燕妮 來源: github
相關(guān)推薦

2009-06-09 13:18:56

Scala函數(shù)式命令式

2009-06-22 14:59:51

AOP實現(xiàn)原理聲明式編程命令式編程

2013-09-09 09:41:34

2011-03-08 15:47:32

函數(shù)式編程

2016-10-31 20:46:22

函數(shù)式編程Javascript

2025-03-11 10:00:20

Golang編程函數(shù)

2020-09-24 10:57:12

編程函數(shù)式前端

2023-12-14 15:31:43

函數(shù)式編程python編程

2022-09-22 08:19:26

WebFlux函數(shù)式編程

2011-08-24 09:13:40

編程

2009-07-21 17:16:34

Scala函數(shù)式指令式

2017-06-08 14:25:46

Kotlin函數(shù)

2010-01-28 14:51:24

Scala后函數(shù)式

2014-09-05 10:15:41

函數(shù)式編程

2013-07-09 09:43:04

函數(shù)式思維函數(shù)式編程編程

2010-03-11 10:34:22

Scala

2012-09-21 09:21:44

函數(shù)式編程函數(shù)式語言編程

2020-09-23 07:50:45

Java函數(shù)式編程

2020-09-22 11:00:11

Java技術(shù)開發(fā)

2016-08-11 10:11:07

JavaScript函數(shù)編程
點贊
收藏

51CTO技術(shù)棧公眾號