摘要:想閱讀更多優質文章請猛戳博客一年百來篇優質文章等著你本系列的第一篇學會使用函數式編程的程序員第部分組合函數作為程序員,我們是懶惰的。柯里化又稱部分求值。一旦使用函數式語言,任何東西都是不可變的。在函數式語言中,這個函數稱為。
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!
本系列的第一篇:
學會使用函數式編程的程序員(第1部分)
組合函數 (Function Composition)作為程序員,我們是懶惰的。我們不想構建、測試和部署我們編寫的一遍又一遍的代碼。我們總是試圖找出一次性完成工作的方法,以及如何重用它來做其他事情。
代碼重用聽起來很棒,但是實現起來很難。如果代碼業務性過于具體,就很難重用它。如時代碼太過通用簡單,又很少人使用。所以我們需要平衡兩者,一種制作更小的、可重用的部件的方法,我們可以將其作為構建塊來構建更復雜的功能。
在函數式編程中,函數是我們的構建塊。每個函數都有各自的功能,然后我們把需要的功能(函數)組合起來完成我們的需求,這種方式有點像樂高的積木,在編程中我們稱為 組合函數。
看下以下兩個函數:
var add10 = function(value) { return value + 10; }; var mult5 = function(value) { return value * 5; };
上面寫法有點冗長了,我們用箭頭函數改寫一下:
var add10 = value => value + 10; var mult5 = value => value * 5;
現在我們需要有個函數將傳入的參數先加上 10 ,然后在乘以 5, 如下:
var mult5AfterAdd10 = value => 5 * (value + 10)
盡管這是一個非常簡單的例子,但仍然不想從頭編寫這個函數。首先,這里可能會犯一個錯誤,比如忘記括號。第二,我們已經有了一個加 10 的函數 add10 和一個乘以 5 的函數 mult5 ,所以這里我們就在寫已經重復的代碼了。
使用函數 add10,mult5 來重構 mult5AfterAdd10 :
var mult5AfterAdd10 = value => mult5(add10(value));
我們只是使用現有的函數來創建 mult5AfterAdd10,但是還有更好的方法。
在數學中, f ° g 是函數組合,叫作“f 由 g 組合”,或者更常見的是 “f after g”。 因此 (f ° g)(x) 等效于f(g(x)) 表示調用 g 之后調用 f。
在我們的例子中,我們有 mult5 ° add10 或 “add10 after mult5”,因此我們的函數的名稱叫做 mult5AfterAdd10。由于Javascript本身不做函數組合,看看 Elm 是怎么寫的:
add10 value = value + 10 mult5 value = value * 5 mult5AfterAdd10 value = (mult5 << add10) value
在 Elm 中 << 表示使用組合函數,在上例中 value 傳給函數 add10 然后將其結果傳遞給 mult5。還可以這樣組合任意多個函數:
f x = (g << h << s << r << t) x
這里 x 傳遞給函數 t,函數 t 的結果傳遞給 r,函數 t 的結果傳遞給 s,以此類推。在Javascript中做類似的事情,它看起來會像 g(h(s(r(t(x))))),一個括號噩夢。
Point-Free NotationPoint-Free Notation就是在編寫函數時不需要指定參數的編程風格。一開始,這風格看起來有點奇怪,但是隨著不斷深入,你會逐漸喜歡這種簡潔的方式。
在 multi5AfterAdd10 中,你會注意到 value 被指定了兩次。一次在參數列表,另一次是在它被使用時。
// 這個函數需要一個參數 mult5AfterAdd10 value = (mult5 << add10) value
但是這個參數不是必須的,因為該函數組合的最右邊一個函數也就是 add10 期望相同的參數。下面的 point-free 版本是等效的:
// 這也是一個需要1個參數的函數 mult5AfterAdd10 = (mult5 << add10)
使用 point-free 版本有很多好處。
首先,我們不需要指定冗余的參數。由于不必指定參數,所以也就不必考慮為它們命名。
由于更簡短使得更容易閱讀。本例比較簡單,想象一下如果一個函數有多個參數的情況。
天堂里的煩惱到目前為止,我們已經了解了組合函數如何工作以及如何通過 point-free 風格使函數簡潔、清晰、靈活。
現在,我們嘗試將這些知識應用到一個稍微不同的場景。想象一下我使用 add 來替換 add10:
add x y = x + y mult5 value = value * 5
現在如何使用這兩個函數來組合函數 mult5After10 呢?
我們可能會這樣寫:
-- 這是錯誤的!!! mult5AfterAdd10 = (mult5 << add) 10
但這行不通。為什么? 因為 add 需要兩個參數。
這在 Elm 中并不明顯,請嘗試用Javascript編寫:
var mult5AfterAdd10 = mult5(add(10)); // 這個行不通
這段代碼是錯誤的,但是為什么?
因為這里 add 函數只能獲取到兩個參數(它的函數定義中指定了兩個參數)中的一個(實際只傳遞了一個參數),所以它會將一個錯誤的結果傳遞給 mult5。這最終會產生一個錯誤的結果。
事實上,在 Elm 中,編譯器甚至不允許你編寫這種格式錯誤的代碼(這是 Elm 的優點之一)。
我們再試一次:
var mult5AfterAdd10 = y => mult5(add(10, y)); // not point-free
這個不是point-free風格但是我覺得還行。但是現在我不再僅僅組合函數。我在寫一個新函數。同樣如果這個函數更復雜,例如,我想使用一些其他的東西來組合mult5AfterAdd10,我真的會遇到麻煩。
由于我們不能將這個兩個函數對接將會出現函數組合的作用受限。這太糟糕了,因為函數組合是如此強大。
如果我們能提前給add函數一個參數然后在調用 mult5AfterAdd10 時得到第二個參數那就更好了。這種轉化我們叫做 柯里化。
柯里化 (Currying)Currying 又稱部分求值。一個 Currying 的函數首先會接受一些參數,接受了這些參數之后,該函數并不會立即求值,而是繼續返回另外一個函數,剛才傳入的參數在函數形成的閉包中被保存起來。待到函數被真正需要求值的時候,之前傳入的所有參數都會被一次性用于求值
上例我們在組合函數 mult5和 add(in) 時遇到問題的是,mult5 使用一個參數,add 使用兩個參數。我們可以通過限制所有函數只取一個參數來輕松地解決這個問題。我只需編寫一個使用兩個參數但每次只接受一個參數的add函數,函數柯里化就是幫我們這種工作的。
柯里化函數一次只接受一個參數。
我們先賦值 add 的第1個參數,然后再組合上 mult5,得到 mult5AfterAdd10 函數。當 mult5AfterAdd10 函數被調用的時候,add 得到了它的第 2 個參數。
JavaScript 實現方式如下:
var add = x => y => x + y
此時的 add 函數先后分兩次得到第 1 個和第 2 個參數。具體地說,add函數接受單參x,返回一個也接受單參 y的函數,這個函數最終返回 x+y 的結果。
現在可以利用這個 add 函數來實現一個可行的 mult5AfterAdd10* :
var compose = (f, g) => x => f(g(x)); var mult5AfterAdd10 = compose(mult5, add(10));
compose 有兩個參數 f 和 g,然后返回一個函數,該函數有一個參數 x,并傳給函數 f,當函數被調用時,先調用函數 g,返回的結果作為函數 f的參數。
總結一下,我們到底做了什么?我們就是將簡單常見的add函數轉化成了柯里化函數,這樣add函數就變得更加自由靈活了。我們先將第1個參數10輸入,而當mult5AfterAdd10函數被調用的時候,最后1個參數才有了確定的值。
柯里化與重構(Curring and Refactoring)函數柯里化允許和鼓勵你分隔復雜功能變成更小更容易分析的部分。這些小的邏輯單元顯然是更容易理解和測試的,然后你的應用就會變成干凈而整潔的組合,由一些小單元組成的組合。
例如,我們有以下兩個函數,它們分別將輸入字符串用單花括號和雙花括號包裹起來:
bracketed = function (str) { retrun "{" + str + "}" } doubleBracketed = function (str) { retrun "{{" + str + "}}" }
調用方式如下:
var bracketedJoe = bracketed("小智") var doubleBracketedJoe = doubleBracketed("小智")
可以將 bracket 和 doubleBracket 轉化為更變通的函數:
generalBracket = function( prefix , str ,suffix ) { retrun prefix ++ str ++ suffix }
但每次我們調用 generalBracket 函數的時候,都得這么傳參:
var bracketedJoe = generalBracket("{", "小智", "}") var doubleBracketedJoe = generalBracket("{{", "小智", "}}")
之前參數只需要輸入1個,但定義了2個獨立的函數;現在函數統一了,每次卻需要傳入3個參數,這個不是我們想要的,我們真正想要的是兩全其美。
因為生成小括號雙括號功能但一,重新調整一下 我們將 generalBracket 三個參數中的 prefix,str 各柯里化成一個函數,如下:
generalBracket = function( prefix ) { return function( suffix ){ return function(str){ return prefix + str + suffix } } }
這樣,如果我們要打印單括號或者雙括號,如下:
// 生成單括號 var bracketedJoe = generalBracket("{")("}") bracketedJoe("小智") // {小智} // 生成雙括號 var bracketedJoe = generalBracket("{{")("}}") bracketedJoe("小智") // {{小智}}常見的函數式函數(Functional Function)
函數式語言中3個常見的函數:Map,Filter,Reduce。
如下JavaScript代碼:
for (var i = 0; i < something.length; ++i) { // do stuff }
這段代碼存在一個很大的問題,但不是bug。問題在于它有很多重復代碼(boilerplate code)。如果你用命令式語言來編程,比如Java,C#,JavaScript,PHP,Python等等,你會發現這樣的代碼你寫地最多。這就是問題所在。
現在讓我們一步一步的解決問題,最后封裝成一個看不見 for 語法函數:
先用名為 things 的數組來修改上述代碼:
var things = [1, 2, 3, 4]; for (var i = 0; i < things.length; ++i) { things[i] = things[i] * 10; // 警告:值被改變! } console.log(things); // [10, 20, 30, 40]
這樣做法很不對,數值被改變了!
在重新修改一次:
var things = [1, 2, 3, 4]; var newThings = []; for (var i = 0; i < things.length; ++i) { newThings[i] = things[i] * 10; } console.log(newThings); // [10, 20, 30, 40]
這里沒有修改things數值,但卻卻修改了newThings。暫時先不管這個,畢竟我們現在用的是 JavaScript。一旦使用函數式語言,任何東西都是不可變的。
現在將代碼封裝成一個函數,我們將其命名為 map,因為這個函數的功能就是將一個數組的每個值映射(map)到新數組的一個新值。
var map = (f, array) => { var newArray = []; for (var i = 0; i < array.length; ++i) { newArray[i] = f(array[i]); } return newArray; };
函數 f 作為參數傳入,那么函數 map 可以對 array 數組的每項進行任意的操作。
現在使用 map 重寫之前的代碼:
var things = [1, 2, 3, 4]; var newThings = map(v => v * 10, things);
這里沒有 for 循環!而且代碼更具可讀性,也更易分析。
現在讓我們寫另一個常見的函數來過濾數組中的元素:
var filter = (pred, array) => { var newArray = []; for (var i = 0; i < array.length; ++i) { if (pred(array[i])) newArray[newArray.length] = array[i]; } return newArray; };
當某些項需要被保留的時候,斷言函數 pred 返回TRUE,否則返回FALSE。
使用過濾器過濾奇數:
var isOdd = x => x % 2 !== 0; var numbers = [1, 2, 3, 4, 5]; var oddNumbers = filter(isOdd, numbers); console.log(oddNumbers); // [1, 3, 5]
比起用 for 循環的手動編程,filter 函數簡單多了。最后一個常見函數叫reduce。通常這個函數用來將一個數列歸約(reduce)成一個數值,但事實上它能做很多事情。
在函數式語言中,這個函數稱為 fold。
var reduce = (f, start, array) => { var acc = start; for (var i = 0; i < array.length; ++i) acc = f(array[i], acc); // f() 有2個參數 return acc; });
reduce函數接受一個歸約函數 f,一個初始值 start,以及一個數組 array。
這三個函數,map,filter,reduce能讓我們繞過for循環這種重復的方式,對數組做一些常見的操作。但在函數式語言中只有遞歸沒有循環,這三個函數就更有用了。附帶提一句,在函數式語言中,遞歸函數不僅非常有用,還必不可少。
原文:
https://medium.com/@cscalfani...
https://medium.com/@cscalfani...
編輯中可能存在的bug沒法實時知道,事后為了解決這些bug,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具Fundebug。
你的點贊是我持續分享好東西的動力,歡迎點贊!
歡迎加入前端大家庭,里面會經常分享一些技術資源。文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/100440.html
摘要:即使你有一個多線程程序,大多數線程都被阻塞等待完成,例如文件,網絡等等。但是只要能夠提升我們程序的效率,要付出努力來寫好多線程程序這是值得的。然而,多線程有兩個主要問題多線程程序難于編寫讀取解釋測試和調試。 showImg(https://segmentfault.com/img/bVblEzw?w=800&h=355); 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等...
摘要:函數式編程的目標是盡量寫更多的純函數,并將其與程序的其他部分隔離開來。在函數式編程中,是非法的。函數式編程使用參數保存狀態,最好的例子就是遞歸。函數式編程使用遞歸進行循環。在函數式編程中,函數是一級公民。 showImg(https://segmentfault.com/img/bVblxCO?w=1600&h=710); 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等...
摘要:今天這篇文章主要介紹函數式編程的思想。函數式編程通過最小化變化使得代碼更易理解。在函數式編程里面,組合是一個非常非常非常重要的思想。可以看到函數式編程在開發中具有聲明模式。而函數式編程旨在盡可能的提高代碼的無狀態性和不變性。 最開始接觸函數式編程的時候是在小米工作的時候,那個時候看老大以前寫的代碼各種 compose,然后一些 ramda 的一些工具函數,看著很吃力,然后極力吐槽函數式...
摘要:所以我覺得函數式編程領域更像學者的領域。函數式編程的原則是完善的,經過了深入的研究和審查,并且可以被驗證。函數式編程是編寫可讀代碼的最有效工具之一可能還有其他。我知道很多函數式編程編程者會認為形式主義本身有助于學習。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson - 《You-Dont-Know-JS》作者 關于譯者:這是一個流淌著滬江血液...
摘要:本文與大家分享一些編程語言的入門書籍,其中不乏經典。全書貫穿的主體是如何思考設計開發的方法,而具體的編程語言,只是提供一個具體場景方便介紹的媒介。入門入門容易理解而且讀起來幽默風趣,對于編程初學者和語言新手而言是理想的書籍。 本文與大家分享一些Python編程語言的入門書籍,其中不乏經典。我在這里分享的,大部分是這些書的英文版,如果有中文版的我也加上了。有關書籍的介紹,大部分截取自是官...
閱讀 2025·2023-04-25 14:50
閱讀 2907·2021-11-17 09:33
閱讀 2611·2019-08-30 13:07
閱讀 2838·2019-08-29 16:57
閱讀 908·2019-08-29 15:26
閱讀 3540·2019-08-29 13:08
閱讀 1990·2019-08-29 12:32
閱讀 3383·2019-08-26 13:57