摘要:本文試圖盡可能系統的描述函數式編程。函數式編程使用參數保存狀態,最好的例子就是遞歸。柯里化函數有利于指定函數行為,并將現有函數組合為新函數。
JavaScript函數式編程 摘要
以往經常看到”函數式編程“這一名詞,卻始終沒有花時間去學習,暑期實習結束之后一直忙于邊養老邊減肥,81天成功瘦身30斤+ ,開始回歸正常的學習生活。
便在看《JavaScript函數式編程》這本書,以系統了解函數式編程的知識。本文試圖盡可能系統的描述JavaScript函數式編程。當然認識暫時停留于本書介紹的程度,如有錯誤之處,還請指正。
注:本書采用的函數式庫Underscore。一下部分代碼運行時,需引入Underscore。
函數式編程簡介我們用一句話來直白的描述函數式編程:
函數式編程通過使用函數來將值轉換成抽象單元,接著用于構建軟件系統。
概括的來說,函數式編程包括以下技術
確定抽象,并為其構建函數
利用已有的函數來構建更為復雜的抽象
通過將現有的函數傳給其他函數來構建更加復雜的抽象
注:JavaScript并不僅限于函數式編程語言,以下是另外3種常用的編程方式。
命令式編程: 通過詳細描述行為的編程方式
基于原型的面向對象編程: 基于原型對象及其實例的編程方式
元編程:對JavaScript執行模型數據進行編寫和操作的編程方式
函數式編程的一些特性 純函數純函數堅持以下屬性(堅持純度的標準不僅將有助于使程序更容易測試,也更容易推理。)
其結果只能從它的參數的值來計算
不能依賴于能被外部操作改變的數據
不能改變外部狀態
不變性 —— 沒有副作用所謂"副作用"(side effect),指的是函數內部與外部互動(最典型的情況,就是修改全局變量的值),產生運算以外的其他結果。
函數式編程強調沒有"副作用",意味著函數要保持獨立,所有功能就是返回一個新的值,沒有其他行為,尤其是不得修改外部變量的值。
上一點已經提到,函數式編程只是返回新的值,不修改系統變量。因此,不修改變量,也是它的一個重要特點。在其他類型的語言中,變量往往用來保存"狀態"(state)。不修改變量,意味著狀態不能保存在變量中。
函數式編程使用參數保存狀態,最好的例子就是遞歸。下面的代碼是一個將字符串逆序排列的函數,它演示了不同的參數如何決定了運算所處的"狀態"。
function reverse(string) { if(string.length == 0) { return string; } else { return reverse(string.substring(1, string.length)) + string.substring(0, 1); } }函數是一等公民
“一等”這個術語通常用來描述值。當函數被看作“一等公民”時,那它就可以去任何值可以去的地方,很少有限制。比如數字在Javascript里就是一等公民,同程
作為一等公民的函數就會擁有類似數字的性質。
var fortytwo = function(){return 42} // 函數與數字一樣可以存儲為變量 var fortytwo = [32, function(){return 42}] // 函數與數字一樣可以存儲為數組的一個元素 var fortytwo = {number: 32, fun: function(){return 42}} // 函數與數字一樣可以作為對象的成員變量 32 + (function(){return 42}) () // 函數與數字一樣可以在使用時直接創建出來 // 函數與數字一樣可以被傳遞給另一個函數 function weirdAdd(n, f){ return n + f()} weirdAdd(32, function(){return 42}) // 函數與數字一樣可以被另一個函數返回 return 32; return function(){return 42}Applicative編程
Applicative編程是特殊函數式編程的一種形式。Applicative編程的三個典型例子是map,reduce,filter
函數A作為參數提供給函數B。 (即定義一個函數,讓它接收一個函數,然后調用它)
_.find(["a","b",3,"d"], _.isNumber) // _.find與_.isNumber都是Underscore中的方法 // 自行實現一個Applicative函數 function exam(fun, coll) { return fun(coll); } // 調用 exam(function(e){ return e.join(",") }, [1,2,3]) // 結果 ”1,2,3“高階函數
定義:一個高階函數應該可以執行以下至少一項操作。
以一個函數作為參數
返回一個函數作為結果
以其他函數為參數的函數 關于傳遞函數的思考: max,finder,best// max是一個高階函數 var people = [{name: "Fred", age: 65}, {name: "Lucy", age: 36}]; _.max(people, function(p) { return p.age }); //=> {name: "Fred", age: 65}
但是,在某些方面這個函數是受限的,并不是真正的函數式。具體來說,對于_.max而言,比較總是需要通過大于運算符(>)來完成。
不過,我們可以創建一個新的函數finder。它接收兩個函數:一個用來生成可比較的值,而另一個用來比較兩個值并返回當中的”最佳“值。
function finder(valueFun, bestFun, coll) { return _.reduce(coll, function(best, current) { var bestValue = valueFun(best); var currentValue = valueFun(current); return (bestValue === bestFun(bestValue, currentValue)) ? best : current; }); }
在任何情況下,我們現在都可以用finder來找到不同類型的”最佳“值:
finder(function(e){return e.age}, Math.max, people) // => {name: ”Fred", age: 65} finder(function(e){return e.name}, function(x, y){ return (x.charAt((0) === "L") ? x : y),people}) // 偏好首字母為L的人 // => {name:"Lucy", age: 36}
縮減一點
函數finder短小精悍,并且能按照我們預期來工作,但為了滿足最大程度的靈活性,它重復了一些邏輯。
// 在 finder函數中 return (bestValue === bestFun(bestValue, currentValue)) ? best : current; // 在輸入的函數參數中 return (x.charAt((0) === "L") ? x : y
你會發現上述兩者的邏輯是完全相同的。finder的實現可以根據以下兩個假設來縮減。
如果第一個參數比第二個參數“更好”,比較最佳值的函數返回為true
比較最佳值的函數知道如何“分解”它的參數
在以上假設的基礎下,我們可以實現一個更簡潔的best函數。
function best(fun, coll) { return _.reduce(coll, function(x, y) { return fun(x, y) ? x : y; }); } best(function(x,y) { return x > y }, [1,2,3,4,5]); //=> 5關于傳遞函數的更多思考:重復,反復和條件迭代
首先,從一個簡單的函數repeat開始。它以一個數字和一個值為參數,將該值進行多次復制,并放入一個數組中:
function repeat(times, VALUE) { return _.map(_.range(times), function() { return VALUE; }); } repeat(4, "Major"); //=> ["Major", "Major", "Major", "Major"]
使用函數,而不是值
通過將參數從值替換為函數,打開了一個充滿可能性的世界。
function repeatedly(times, fun) { return _.map(_.range(times), fun); } repeatedly(3, function() { return Math.floor((Math.random()*10)+1); }); //=> [1, 3, 8]
再次強調,“使用函數,而不是值”
我們常常會知道函數應該被調用多少次,但有時候也知道什么時候推出并不取決于“次數”,而是條件!因此我可以定義另一個名為iterateUntil的函數。
iterateUntil接收2個參數,一個用來執行一些動作,另一個用來進行結果檢查。
function iterateUntil(fun, check, init) { var ret = []; var result = fun(init); while (check(result)) { ret.push(result); result = fun(result); } return ret; };返回其他函數的函數
function invoker (NAME, METHOD) { // 接收一個方法,并在任何給定的對象上調用它 return function(target /* args ... */) { if (!existy(target)) fail("Must provide a target"); var targetMethod = target[NAME]; var args = _.rest(arguments); return doWhen((existy(targetMethod) && METHOD === targetMethod), function() { return targetMethod.apply(target, args); }); }; }; var rev = invoker("reverse", Array.prototype.reverse); _.map([[1,2,3]], rev); //=> [[3,2,1]]
高階函數捕獲參數
高階函數的參數是用來“配置”返回函數的行為的。對于makeAdder而言,它的參數配置了其返回函數每次添加數值的大小
function makeAdder(CAPTURED) { return function(free) { return free + CAPTURED; }; } var add10 = makeAdder(10); add10(32); //=> 42
捕獲變量的好處
用閉包來捕獲增加值,并用作后綴。(但這樣并不具有引用透明)
function makeUniqueStringFunction(start) { var COUNTER = start; return function(prefix) { return [prefix, COUNTER++].join(""); } }; var uniqueString = makeUniqueStringFunction(0); uniqueString("dari"); //=> "dari0" uniqueString("dari"); //=> "dari1"由函數構建函數 函數式組合的精華
精華:使用現有的零部件來建立新的行為,這些新行為同樣也成為了已有的零部件。
// 接收一個或多個函數,然后不斷嘗試依次調用這些函數的方法,直到返回一個非`undefined`的值 function dispatch(/* funs */) { var funs = _.toArray(arguments); var size = funs.length; return function(target /*, args */) { var ret = undefined; var args = _.rest(arguments); for (var funIndex = 0; funIndex < size; funIndex++) { var fun = funs[funIndex]; ret = fun.apply(fun, construct(target, args)); if (existy(ret)) return ret; } return ret; }; } var str = dispatch(invoker("toString", Array.prototype.toString), invoker("toString", String.prototype.toString)); str("a"); //=> "a" str(_.range(10)); //=> "0,1,2,3,4,5,6,7,8,9"
在這里,我們想做的只是返回一個遍歷函數數組,并apply給一個目標對象的函數,返回第一個存在的值。dispatch滿足了多態JavaScript
函數的定義。這樣簡化了委托具體方法的任務。例如,在underscore的實現中,你經常會看到許多不同的函數重復這樣的模式。
確保目標的存在
檢查是否有原生版本,如果是則使用它
如果沒有,那么做一些實現這些行為的具體任務。
做特定類型的任務(如適用)
做特定參數的任務(如適用)
做特定個參數的任務(如適用)
同樣的模式也體現在Underscore的函數_.map()的實現中:
_.map = _.collect = function(obj, iteratee, context) { iteratee = cb(iteratee, context); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, results = Array(length); for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; results[index] = iteratee(obj[currentKey], currentKey, obj); } return results; };
使用dispatch可以簡化一些這方面的代碼,并且更容易擴展。想象一下,你正在寫一個可以為數組和字符串類型生成字符描述的
函數。使用dispatch則可以優雅的實現:
var str = dispatch(invoker("toString", Array.prototype.toString), invoker("toString", String.prototype.toString)); str("a"); //=> "a" str(_.range(10)); //=> "0,1,2,3,4,5,6,7,8,9"柯里化 Curring
柯里化函數為每一個邏輯參數返回一個新函數。
例如:
// 除法 function divide(n,d){ return n/d; } // 手動柯里化 function curryDivide(n) { return function(d) { return n/d; }; }
curryDivide是手動柯里化函數,也就是說,我顯示地返回對應參數數量的函數。
自動柯里化參數
// 接收一個函數,并返回一個只接受一個參數的函數。 function curry(fun) { // 柯里化一個參數,雖然似乎沒什么用 return function(arg) { return fun(arg); }; } function curry2(fun) { // 柯里化兩個參數 return function(secondArg) { return function(firstArg) { return fun(firstArg, secondArg); }; }; } function curry3(fun) { // 柯里化三個參數 return function(last) { return function(middle) { return function(first) { return fun(first, middle, last); }; }; }; };
curry2函數接受一個函數并將其柯里化成兩個深層參數的函數。可以用它來實現先前定義的除法函數。
var divide10 = curry2(div)(10) divide10(50) // => 5
柯里化函數有利于指定JavaScript函數行為,并將現有函數“組合”為新函數。并且使用柯里化比較容易產生流利的函數式API。
部分應用柯里化函數逐漸返回消耗參數的函數,直到所有參數都耗盡。然而,部分應用函數是一個“部分“執行,等待接收剩余的參數立即執行的函數。
// 部分應用一個或兩個已知的參數 function partial1(fun, arg1) { return function(/* args */) { var args = construct(arg1, arguments); // construct為拼接數組,在此代碼略去 return fun.apply(fun, args); }; } function partial2(fun, arg1, arg2) { return function(/* args */) { var args = cat([arg1, arg2], arguments); // cat也為拼接數組,在此代碼略去 return fun.apply(fun, args); }; } // 部分應用任意數量的參數 function partial(fun /*, pargs */) { var pargs = _.rest(arguments); return function(/* arguments */) { var args = cat(pargs, _.toArray(arguments)); return fun.apply(fun, args); }; }通過組合端至端的拼接函數
一種理想化的函數式程序是向函數流水線的一端輸送的一塊數據,從另一端輸出一個全新的數據塊。
!_.isString(name)
這個流水線由_.isString和!組成
_.isString接收一個對象,并返回一個布爾值
!接收一個布爾值,并返回一個布爾值
// 通過組合多個函數及其數據轉換建立新的函數 function isntString(str){ return !_.isString(str) } isntString(1) // => true // 還可以使用Underscore的_.compose函數實現同樣的功能 // _.compose函數從右往左執行。即最右邊函數的結果會被送入其左側的函數,一個接一個 var isntString = _.compose(function(x) { return !x }, _.isString); isntString([]); //=> true遞歸
理解遞歸對理解函數式編程來說非常重要,原因有三。
遞歸的解決方案包括使用對一個普通問題子集的單一抽象的使用
遞歸可以隱藏可變狀態
遞歸是一種實現懶惰和無限大結構的方法
自吸收函數在編寫自遞歸函數時,規則如下
知道什么時候停止
決定怎樣算一個步驟
把問題分解成一個步驟和一個較小的問題
function myLength(ary) { if (_.isEmpty(ary)) // _.isEmpty何時停止 return 0; else // 進行一個步驟 1+ ; return 1 + myLength(_.rest(ary)); // 小一些的問題 _.rest(ary) }
尾遞歸
尾遞歸與一般自遞歸的明顯區別是,”一個步驟“和”縮小的問題“中的元素都要進行遞歸調用。
function tcLength(ary, n) { var l = n ? n : 0; if (_.isEmpty(ary)) return l; else return tcLength(_.rest(ary), l + 1); } tcLength(_.range(10)); //=> 10相互關聯函數
兩個或多個函數相互調用被稱為相互遞歸。下面看一個例子,用謂詞函數來檢查偶數和奇數:
function evenSteven(n) { if (n === 0) return true; else return oddJohn(Math.abs(n) - 1); } function oddJohn(n) { if (n === 0) return false; else return evenSteven(Math.abs(n) - 1); } // 相互遞歸調用來回反彈彼此之間遞減某個絕對的值,知道一方或另一方達到0 evenSteven(4) // => true oddJohn(11) // =>true對遞歸的改進
盡管遞歸技術上是可行的,但是因為JavaScript引擎沒有優化遞歸調用,因此,在使用或寫遞歸函數時,可能會碰到如下錯誤
evenSteven(10000) // 棧溢出
遞歸應該被看作一個底層操作,應該盡可能地避免(很容易造成棧溢出)。普通的共識是,首先是要函數組合,僅當需要的時才使用遞歸和蹦床。
蹦床(tramponline):使用蹦床展平調用,而不是深度嵌套的遞歸調用。
首先,看看如何手動修復evenOline和oddOline使得遞歸調用不會溢出。一個辦法是返回一個函數,它包裝調用,而不是直接直接調用。
function evenOline(n) { if (n === 0) return true; else return partial1(oddOline, Math.abs(n) - 1); } function oddOline(n) { if (n === 0) return false; else return partial1(evenOline, Math.abs(n) - 1); } oddOline(3)()() // 返回的只是一個函數調用 // => function(){return evenOline(Math.abs(n) - 1)} oddOline(3)()()() // 將函數調用執行 // => true oddOline(10000)()()()... // 10000個()去執行返回的函數調用 // => true
當然,我們不能直接向用戶暴露這個API,可以提高另外一個函數trampoline,從程序執行來進行扁平化處理。
function trampoline(fun /*, args */) { // 不斷調用函數的返回值,知道它不是一個函數為止 var result = fun.apply(fun, _.rest(arguments)); while (_.isFunction(result)) { result = result(); } return result; } trampoline(oddOline, 10000) // false
由于調用鏈的間接性,使用蹦床增加了相互遞歸函數的一些開銷。然而滿總比溢出要好。同樣,你可能不希望強迫用戶使用trampoline,只是為了避免堆棧溢出。我們可以進一步隱藏其外觀。
function isEvenSafe(n) { if (n === 0) return true; else return trampoline(partial1(oddOline, Math.abs(n) - 1)); } function isOddSafe(n) { if (n === 0) return false; else return trampoline(partial1(evenOline, Math.abs(n) - 1)); }基于流的編程 鏈接
使用jQuery等庫經常會使用鏈接,鏈接可以讓我們的代碼更加簡潔,如下是鏈接的實現示例。
鏈接方法的原理在于。每個鏈接的方法都返回統一的宿主對象引用。
function createPerson() { var firstName = ""; var age = 0; return { setFirstName: function(fn) { firstName = fn; return this; }, setAge: function(a) { age = a; return this; }, toString: function() { return [firstName, lastName, age].join(" "); } }; } createPerson() .setFirstName("Mike") .setAge(108) .toString(); //=> "Mike 108"
惰性鏈
上述鏈接是直接執行,然而我們也可以實行惰性鏈,即使其先緩存待執行的函數,等到調用執行函數時一起執行。
封裝了一些行為的函數通常被稱為thunk,存儲在_calls中的thunk期待將作為接受force方法調用的對象的中間目標。
function LazyChain(obj) { this._calls = []; // 用于緩存待執行函數的數組 thunk this._target = obj; // 目標對象 } LazyChain.prototype.invoke = function(methodName /*, args */) { // 將函數壓入的方法 var args = _.rest(arguments); this._calls.push(function(target) { var meth = target[methodName]; return meth.apply(target, args); }); return this; }; LazyChain.prototype.force = function() { // 強制執行this._calls中的函數 return _.reduce(this._calls, function(target, thunk) { return thunk(target); }, this._target); }; // 使用,直到force方法被調用才將 concat, sort,join執行 new LazyChain([2,1,3]) .invoke("concat", [8,5,7,6]) .invoke("sort") .invoke("join"," ") .force(); // => "1 2 3 4 5 6 7 8"管道
鏈接模式有利于給對象的方法調用創建流程的API,但是對于函數式API則未必。
方法連接有各種各樣的缺點,包括緊耦合對象的set和get邏輯。主要問題是,函數鏈經常會做調用之間改變傳遞的共同引用。函數式API重點在操作值而不是引用。
一下是管道的具體實現
function pipeline(seed /*, args */) { return _.reduce(_.rest(arguments), function(l,r) { return r(l); }, seed); }; pipeline(42, function(n){return -n},function(n){return n+1}) // => -41寫在最后
本文更多的是對《JavaScript函數式編程》一書的摘要,并透過一段段代碼試圖闡述函數式編程的思想。
希望以后的工作中能夠吸取函數式編程的好,并慢慢對其加深理解。從書中獲取知識,最終還是要落于實踐中去的。
同時,希望能夠通過這篇文章幫助不了解函數式編程的小伙伴建立系統的認識。
WilsonLiu"s blog首發地址:http://blog.wilsonliu.cn
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/91224.html
摘要:函數式編程,一看這個詞,簡直就是學院派的典范。所以這期周刊,我們就重點引入的函數式編程,淺入淺出,一窺函數式編程的思想,可能讓你對編程語言的理解更加融會貫通一些。但從根本上來說,函數式編程就是關于如使用通用的可復用函數進行組合編程。 showImg(https://segmentfault.com/img/bVGQuc); 函數式編程(Functional Programming),一...
摘要:為了盡可能提升互通性,已經成為函數式編程庫遵循的實際標準。與輕量級函數式編程的概念相反,它以火力全開的姿態進軍的函數式編程世界。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關于譯者:這是一個流淌著滬江血液的純粹工程:認真,是 HTML 最堅實的梁柱;分享,是 CSS 里最閃耀的一瞥;總結,...
摘要:所支持的面向對象編程包括原型繼承。發明于年的就是首批支持函數式編程的語言之一,而演算則可以說是孕育了這門語言。即使在今天,這個家族的編程語言應用范圍依然很廣。 1. 能說出來兩種對于 JavaScript 工程師很重要的編程范式么? JavaScript 是一門多范式(multi-paradigm)的編程語言,它既支持命令式(imperative)/面向過程(procedural)編程...
摘要:所支持的面向對象編程包括原型繼承。發明于年的就是首批支持函數式編程的語言之一,而演算則可以說是孕育了這門語言。即使在今天,這個家族的編程語言應用范圍依然很廣。 1. 能說出來兩種對于 JavaScript 工程師很重要的編程范式么? JavaScript 是一門多范式(multi-paradigm)的編程語言,它既支持命令式(imperative)/面向過程(procedural)編程...
摘要:所支持的面向對象編程包括原型繼承。發明于年的就是首批支持函數式編程的語言之一,而演算則可以說是孕育了這門語言。即使在今天,這個家族的編程語言應用范圍依然很廣。 1. 能說出來兩種對于 JavaScript 工程師很重要的編程范式么? JavaScript 是一門多范式(multi-paradigm)的編程語言,它既支持命令式(imperative)/面向過程(procedural)編程...
閱讀 2784·2023-04-25 18:06
閱讀 2576·2021-11-22 09:34
閱讀 1684·2021-11-08 13:16
閱讀 1302·2021-09-24 09:47
閱讀 3049·2019-08-30 15:44
閱讀 2773·2019-08-29 17:24
閱讀 2584·2019-08-23 18:37
閱讀 2433·2019-08-23 16:55