Pointfree 編程風格指南
本文要回答一個很重要的問題: 函數式編程 有什么用?
目前,主流的編程語言都不是函數式的,已經能夠滿足需求。為何還要學函數式編程呢,只為了多理解一些新奇的概念?
一個網友說:
"函數式編程有什么優勢呢?"
"我感覺,這種寫法可能會令人頭痛吧。"
很長一段時間,我根本不知道從何入手,如何將它用于實際項目?直到有一天,我學到了 Pointfree 這個概念,頓時豁然開朗,原來應該這樣用!
我現在覺得,Pointfree 就是如何使用函數式編程的答案。
一、程序的本質
為了理解 Pointfree,請大家先想一想,什么是程序?
上圖是一個編程任務,左側是數據輸入(input),中間是一系列的運算步驟,對數據進行加工,右側是最后的數據輸出(output)。 一個或多個這樣的任務,就組成了程序。
輸入和輸出(統稱為 I/O)與鍵盤、屏幕、文件、數據庫等相關,這些跟本文無關。 這里的關鍵是,中間的運算部分不能有 I/O 操作,應該是純運算,即通過純粹的數學運算來求值。 否則,就應該拆分出另一個任務。
I/O 操作往往有現成命令,大多數時候,編程主要就是寫中間的那部分運算邏輯。現在,主流寫法是過程式編程和面向對象編程,但是我覺得,最合適純運算的是函數式編程。
二、函數的拆分與合成
上面那張圖中,運算過程可以用一個函數fn表示。
fn的類型如下。
fn :: a -> b
上面的式子表示,函數fn的輸入是數據a,輸出是數據b。
如果運算比較復雜,通常需要將fn拆分成多個函數。
f1、f2、f3的類型如下。
f1 :: a -> m
f2 :: m -> n
f3 :: n -> b
上面的式子中,輸入的數據還是a,輸出的數據還是b,但是多了兩個中間值m和n。
我們可以把整個運算過程,想象成一根水管(pipe),數據從這頭進去,那頭出來。
函數的拆分,無非就是將一根水管拆成了三根。
進去的數據還是a,出來的數據還是b。fn與f1、f2、f3的關系如下。
fn = R.pipe(f1, f2, f3);
上面代碼中,我用到了 Ramda 函數庫的 pipe 方法,將三個函數合成為一個。Ramda 是一個非常有用的庫,后面的例子都會使用它,如果你還不了解,可以先讀一下 教程 。
三、Pointfree 的概念
fn = R.pipe(f1, f2, f3);
這個公式說明,如果先定義f1、f2、f3,就可以算出fn。整個過程,根本不需要知道a或b。
也就是說,我們完全可以把數據處理的過程,定義成一種與參數無關的合成運算。不需要用到代表數據的那個參數,只要把一些簡單的運算步驟合成在一起即可。
這就叫做 Pointfree:不使用所要處理的值,只合成運算過程。中文可以譯作"無值"風格。
請看下面的例子。
var addOne = x => x + 1;
var square = x => x * x;
上面是兩個簡單函數addOne和square。
把它們合成一個運算。
var addOneThenSquare = R.pipe(addOne, square);
addOneThenSquare(2) // 9
上面代碼中,addOneThenSquare是一個合成函數。定義它的時候,根本不需要提到要處理的值,這就是 Pointfree。
四、Pointfree 的本質
Pointfree 的本質就是使用一些通用的函數,組合出各種復雜運算。上層運算不要直接操作數據,而是通過底層函數去處理。這就要求,將一些常用的操作封裝成函數。
比如,讀取對象的role屬性,不要直接寫成obj.role,而是要把這個操作封裝成函數。
var prop = (p, obj) => obj[p];
var propRole = R.curry(prop)('role');
上面代碼中,prop函數封裝了讀取操作。它需要兩個參數p(屬性名)和obj(對象)。這時,要把數據obj要放在最后一個參數,這是為了方便柯里化。函數propRole則是指定讀取role屬性,下面是它的用法(查看 完整代碼 )。
var isWorker = s => s === 'worker';
var getWorkers = R.filter(R.pipe(propRole, isWorker));
var data = [
{name: '張三', role: 'worker'},
{name: '李四', role: 'worker'},
{name: '王五', role: 'manager'},
];
getWorkers(data)
// [
// {"name": "張三", "role": "worker"},
// {"name": "李四", "role": "worker"}
// ]
上面代碼中,data是傳入的值,getWorkers是處理這個值的函數。定義getWorkers的時候,完全沒有提到data,這就是 Pointfree。
簡單說,Pointfree 就是運算過程抽象化,處理一個值,但是不提到這個值。這樣做有很多好處,它能夠讓代碼更清晰和簡練,更符合語義,更容易復用,測試也變得輕而易舉。
五、Pointfree 的示例一
下面,我們來看一個示例。
var str = 'Lorem ipsum dolor sit amet consectetur adipiscing elit';
上面是一個字符串,請問其中最長的單詞有多少個字符?
我們先定義一些基本運算。
// 以空格分割單詞
var splitBySpace = s => s.split(' ');
// 每個單詞的長度
var getLength = w => w.length;
// 詞的數組轉換成長度的數組
var getLengthArr = arr => R.map(getLength, arr);
// 返回較大的數字
var getBiggerNumber = (a, b) => a > b ? a : b;
// 返回最大的一個數字
var findBiggestNumber =
arr => R.reduce(getBiggerNumber, 0, arr);
然后,把基本運算合成為一個函數(查看 完整代碼 )。
var getLongestWordLength = R.pipe(
splitBySpace,
getLengthArr,
findBiggestNumber
);
getLongestWordLength(str) // 11
可以看到,整個運算由三個步驟構成,每個步驟都有語義化的名稱,非常的清晰。這就是 Pointfree 風格的優勢。
Ramda 提供了很多現成的方法,可以直接使用這些方法,省得自己定義一些常用函數(查看 完整代碼 )。
// 上面代碼的另一種寫法
var getLongestWordLength = R.pipe(
R.split(' '),
R.map(R.length),
R.reduce(R.max, 0)
);
六、Pointfree 示例二
最后,看一個實戰的例子,拷貝自 Scott Sauyet 的文章 《Favoring Curry》 。那篇文章能幫助你深入理解柯里化,強烈推薦閱讀。
下面是一段服務器返回的 JSON 數據。
現在要求是,找到用戶 Scott 的所有未完成任務,并按到期日期升序排列。
過程式編程的代碼如下(查看 完整代碼 )。
上面代碼不易讀,出錯的可能性很大。
現在使用 Pointfree 風格改寫(查看 完整代碼 )。
var getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(R.prop('tasks'))
.then(R.filter(R.propEq('username', membername)))
.then(R.reject(R.propEq('complete', true)))
.then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
.then(R.sortBy(R.prop('dueDate')));
};
上面代碼已經清晰很多了。
另一種寫法是,把各個then里面的函數合成起來(查看 完整代碼 )。
// 提取 tasks 屬性
var SelectTasks = R.prop('tasks');
// 過濾出指定的用戶
var filterMember = member => R.filter(
R.propEq('username', member)
);
// 排除已經完成的任務
var excludeCompletedTasks = R.reject(R.propEq('complete', true));
// 選取指定屬性
var selectFields = R.map(
R.pick(['id', 'dueDate', 'title', 'priority'])
);
// 按照到期日期排序
var sortByDueDate = R.sortBy(R.prop('dueDate'));
// 合成函數
var getIncompleteTaskSummaries = function(membername) {
return fetchData().then(
R.pipe(
SelectTasks,
filterMember(membername),
excludeCompletedTasks,
selectFields,
sortByDueDate,
)
);
};
上面的代碼跟過程式的寫法一比較,孰優孰劣一目了然。
七、參考鏈接
(完)
來自:http://www.udpwork.com/item/16173.html