前言
函數語言程式設計與我們以往的程式設計習慣有許多不同。這篇文章舉了一些JavaScript的例子,介紹了函數語言程式設計中重要的概念。附加的文章會讓你更深入的瞭解JavaScript中的函數語言程式設計。
本文原始碼可以在GitHub上找到,放在jsFunctionalProgramming倉庫中。
我要感謝Csaba Hellinger的支援和投入,在他的幫助下我才完成這篇文章。
PART 1
函數語言程式設計由Lambda Calculus演化而來,它是一個抽象數學的函式表述,我們將思考怎麼把它運用在現實中。
函數語言程式設計是宣告式程式設計的正規化。
為什麼要使用函數語言程式設計?
函數語言程式設計有以下具體特性:
避免狀態改變(可變的資料) - 函數語言程式設計的特性之一就是:函式在應用中不會改變狀態,它們(functions)寧願從之前的狀態之上建立一個新的狀態。
函式宣告 vs 函式表示式 - 在函數語言程式設計中,我們定義和描述函式就像數學中的一個方法宣告。
冪等性 - 這意味著我們用相同的引數呼叫一個函式(不管任何時刻)它都會返回相同的結果,這也可以避免狀態的改變。
這三個特性咋一看似乎並沒有什麼意義,但如果我們更深入的分析,發現在以下三種情況下使用函數語言程式設計能充分發揮這三個特性:
並行的程式碼執行 - 因為函數語言程式設計有冪等性和避免狀態改變的特性,用函式方法編寫程式碼會讓並行更容易,因為不會出現同步問題。
簡明、簡潔的程式碼 - 因為函數語言程式設計使用方法宣告的方式,程式碼不會像程式導向程式設計一樣,有額外的演算法步驟。
不同的程式設計思想 - 一旦你真正使用了一門函數語言程式設計語言,你會擁有一種新的程式設計思想,當你構建應用時也會有新的點子。
f(x) === J(s)
javascript 是一門真正的(純粹的)函數語言程式設計語言嗎?
不!JavaScript並不是一門純粹的函數語言程式設計語言...
第一型物件 - 函式
它可以很好的運用在函數語言程式設計中,因為函式是第一性物件。如果在一門程式語言中,函式和其他型別一樣,那麼這門語言中的函式就是第一型物件。舉個例子,函式可以作為引數傳遞給其他函式,也可以賦值給變數。
我們將檢查一些函式是否是第一型物件,但是在這之前,我們先構建一個程式碼塊,我們將像真正的函式式語言一樣使用JavaScript。
在大部分純函數語言程式設計語言中(Haskell, Clean, Erlang),它們是沒有for
或者while
迴圈的,所以迴圈一個列表需要用到遞迴函式。純函數語言程式設計語言有語言支援和最好的列表推導式和列表串聯。
這裡有一個函式實現了for
迴圈,我們將在接下來的程式碼中用到它,但是你也將看到它在JS中的侷限性,因為尾部呼叫優化並沒有被廣泛的支援,但以後會好起來的。
function funcFor(first, last, step, callback) {
//
//遞迴inner函式
//
function inner(index) {
if((step > 0 && index >= last) || (step < 0 && index < last)) {
return;
}
callback(index);
//
//接下來進行尾部呼叫
//
inner(index + step);
}
//
//開始遞迴
//
inner(first);
}複製程式碼
inner
函式包含了對停止遞迴的管理,它傳入引數index
去呼叫callback
,再遞迴呼叫inner(index + step)
確保迴圈傳遞到下一步。
遞迴是函數語言程式設計的一個重要方面。
現在,讓我們看看真正的函數語言程式設計:
function applyIfAllNumbers(items, fn) {
if(areNumbers(items)) {
return funcMap(items, fn);
}
return [];
}複製程式碼
applyIfAllNumbers
函式的目的是呼叫fn
函式,並把items
中的每個數字作為引數傳入,但前提是隻有在items
陣列中都是數字的情況下才去呼叫。
下面是驗證器函式:
function areNumbers(numbers) {
if(numbers.length == 0) {
return true;
}
else {
return isNumber(number[0]) && areNumbers(numbers.slice(1));
}
}
function isNumber(n) {
return isFinite(n) && +n === n;
}複製程式碼
這段程式碼簡單明瞭,如果引數是一個數字,isNumber
函式返回true
,否則返回false
。areNumbers
函式使用isNumber
函式判斷numbers
陣列中是否全是數字(再提醒一次,遞迴常常被用來實現這種邏輯)。
另一個例子是applyForNumbersOnly
:
function applyForNumbersOnly(items, fn) {
let numbers = filter(items, isNumber);
return funcMap(numbers, fn);
}複製程式碼
這樣寫甚至更簡潔:
function applyForNumbersOnly(items, fn) {
return funcMap(filter(items, isNumber), fn);
}複製程式碼
applyForNumbersOnly
呼叫fn
方法僅僅是為了收集items
中的數字。
funcMap
函式在函數語言程式設計中重現了著名的map
函式,但是這裡我藉助了funcForEach
函式來建立它:
function funcForEach(items, fn) {
return funcFor(0, items.length, 1, function(idx) {
fn(items[idx]);
});
}
function funcMap(items, fn) {
let result = [];
funcForEach(items, function(item) {
result.push(fn(item));
});
return result;
}複製程式碼
最後還剩filter
函式,我們再一次使用遞迴來實現過濾的邏輯。
function filter(input, callback) {
function inner(input, callback, index, output) {
if (index === input.length) {
return output;
}
return inner(
input,
callback,
index + 1,
callback(input[index]) ? output.concat(input[index]) : output;
);
}
return inner(input, callback, 0, []);
}複製程式碼
JS中的尾呼叫優化(TCO)
在EcmaScript 2015 TCO文件中有一些用例的定義,這門語言不久就將支援尾呼叫優化了。最關鍵的一點就是在你的程式碼中使用use strict
模式,否則JS不能支援尾呼叫優化。
由於沒有內建方法來檢測瀏覽器是否支援尾調動優化,以下程式碼實現了這個功能:
"use static"
function isTCOSupported() {
const outerStackLen = new Error().stack.length;
//inner函式的name長度一定不能超過外部函式
return (function inner() {
const innerStackLen = new Error().stack.length;
return innerStackLen <= outerStackLen;
}());
}
console.log(isTCOSupported() ? "TCO Available" : "TCO N/A");複製程式碼
這裡有一個重現Math.pow
函式的例子,它能從EcmaScript 2015的TCO中獲益。
這個pow函式的實現使用了ES6預設引數,讓它看上去更簡潔。
function powES6(base, power, result=base) {
if (power === 0) {
return 1;
}
if(power === 1) {
return result;
}
return powES6(base, power - 1, result * base);
}複製程式碼
首先要提醒以下,powES6
函式有三個引數而不是兩個。第三個引數是計算後的值。我們隨身攜帶return
是為了實現讓我們的遞迴呼叫變成真正的尾呼叫,讓JS可以使用它的尾呼叫優化技術。
萬一我們不能使用ES6的特性,那麼我們不推薦使用遞迴去實現pow
函式,因為這門語言還沒有提出有關遞迴的優化,這樣實現起來就很複雜了:
function recursivePow(base, power, result) {
if (power === 0) {
return 1;
}
else if(power === 1) {
return result;
}
return recursivePow(base, power - 1, result * base);
}
function pow(base, power) {
return recursivePow(base, power, base);
}複製程式碼
我們把遞迴計算放在了另一個recursivePow
函式中,這個函式有三個引數,就像powES6
函式一樣。使用一個新函式並把base
作為引數傳遞給它,以此實現ES6中的預設引數邏輯。
在這個頁面你可以檢視TCO在不同瀏覽器和平臺的支援情況。
目前只有Safari 10是完全支援TCO的瀏覽器(在寫這篇文章時),我將進行一些對於pow
的測試,來看看它的表現。
測試遞迴呼叫
我使用了powES6
和pow
函式來進行測試:
"use strict";
function stressPow(n) {
var result = [];
for (var i=0; i<n; ++i) {
result.push(
pow(2, 0),
pow(2, 1),
pow(2, 2),
pow(2, 3),
pow(2, 4),
pow(2, 5),
pow(2, 10),
pow(2, 20),
pow(2, 30),
pow(1, 10000),
pow(2, 40),
pow(3, 10),
pow(4, 15),
pow(1, 11000),
pow(3.22, 125),
pow(3.1415, 89),
pow(7, 2500),
pow(2, 13000)
);
}
return result;
}
var start = performance.now();
var result_standard = stressPow(2500);
var duration = performance.now() - start;
console.log(result_standard);
console.log(`Duration: ${duration} ms.`);複製程式碼
我在Chrome v55, Firefox v50, Safari v9.2 和 Safari v10上測試了以上程式碼。
小結
根據上面的資料,我們得出Safari對遞迴函式的優化效率是最高的。Safari 10對尾呼叫的支援是最好的,速度比Chrome快了大約2.8倍。Firefox幾乎和Safari 9.2 一樣棒,這出乎了我的意料。
如果你很喜歡這篇文章,請點個贊哦。(譯者注:話說好長啊,好累啊。)
讓我們繼續函式式!
PART 2 也即將發出,關於高階函式和例子,講解如何編寫函式式風格的程式碼。
喜歡本文的朋友可以關注我的微信公眾號,不定期推送一些好文。
本文由Rockjins Blog翻譯,轉載請與譯者聯絡。否則將追究法律責任。