Pointfree 編程風格指南

xzmxml888 7年前發布 | 10K 次閱讀 軟件開發 前端技術

本文要回答一個很重要的問題: 函數式編程 有什么用?

目前,主流的編程語言都不是函數式的,已經能夠滿足需求。為何還要學函數式編程呢,只為了多理解一些新奇的概念?

一個網友說:

"函數式編程有什么優勢呢?"

"我感覺,這種寫法可能會令人頭痛吧。"

很長一段時間,我根本不知道從何入手,如何將它用于實際項目?直到有一天,我學到了 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

 

 本文由用戶 xzmxml888 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!