1. 程式人生 > >實戰函數語言程式設計:使用Ramda.js

實戰函數語言程式設計:使用Ramda.js

對我來說,使得JavaScript如此有趣的一個原因是它函數語言程式設計方面的特性。從一開始函式就是JavaScript世界中的一等公民。這使得通過多種方式的組合編寫優雅,富有表現力的程式碼成為可能。

然而,僅僅是擁有一些函數語言程式設計的能力並不代表你的程式碼就是函式式的。Ramda.js是一個很流行的庫(GitHub上有超過4K的star),我們可以利用它來幫助我們學習使用JavaScript進行函數語言程式設計。

入門

為了能充分利用Ramda.js,讓我們通過一個Node.js專案來了解它的好處。我們可以很容易的使用Node包管理器(npm)安裝它:

npm install ramda

通常我們會把這個庫的功能引入到名稱空間R下。這樣所有對Ramda方法的呼叫都有一個R.的字首。

var R = require('ramda');

當然我們也可以在前端程式碼裡使用Ramda.js。在瀏覽器中我們只需要引入該庫某個拷貝的正確路徑。就像下面的HTML片段一樣簡單:

<script src="ramda.min.js"></script>

Ramda.js沒有使用任何DOM或Node.js的專屬特性。它只是一個建立在JavaScript語言(ECMAScript 5標準)現有功能和演算法之上的的擴充套件。

準備好了嗎?讓我們看看它具體的功能吧!

概念

函數語言程式設計中最重要的概念是純粹函式。一個純函式是冪等的並且不會改變任何狀態。在數學領域這對諸如sin(x)這樣的函式來說是很自然的並且它們也不依賴任何外部狀態。

除了純粹函式以外,我們還想使用只有一個引數的函式。它們是最原始的形式。沒有引數的函式通常意味著會改變外部狀態,因此也就談不上純粹了。但是在像JavaScript這樣的語言裡我們通常會遇到接收多個引數的函式。

柯里化

擁有高階函式(例如,函式可以接收一個函式作為引數並返回一個新函式)的能力以及閉包(快取本地變數)聯合起來給了我們一個很好的方式:柯里化。柯里化是一個把多個引數的函式轉變為只接受一個引數並返回另一個只接收一個引數的函式的過程。這個過程持續多次直到收集到所有所需引數。

比如我們想用Ramda.js的輔助方法is實現一個單引數函式用於檢測其引數是否是string。以下的程式碼可以實現功能。

function isString (test) {
    return R.is(String, test);
}

var result = isString('foo'); //=> true

同樣的功能使用柯里化實現就會簡單很多。由於R.is是Ramda.js的一部分,當我們傳入較少的引數時,它就會自動返回一個柯里化過的函式:

var isString = R.is(String);
var result = isString('foo'); //=> true

這樣的程式碼更富表現力。由於我們使用R.is時只傳入了一個引數,因此它返回了一個函式。在第二次呼叫時(這裡要注意,原始的函式需要傳入2個引數)我們得到了最終的結果。

但是如果一開始我們沒有使用Ramda.js提供的輔助方法呢?假設在我們的程式碼中有以下函式:

var quadratic = (a, b, c, x) => x * x * a + x * b + c;
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> TypeError: quadratic(..) is not a function

這是一個完全二階多項式。它有4個可以是任何值的引數。但是通常我們會固定abc的值而只改變x。讓我們看看如果利用Ramda.js進行這種轉變:

var quadratic = R.curry((a, b, c, x) => x * x * a + x * b + c);
quadratic(1, 0, 0, 2); //=> 4
quadratic(1, 0, 0)(2); //=> 4

再者,我們可以通過改變引數來實現特定子集的別名。比如,我們可以這樣實現方程式x - 1

var xOffset = quadratic(0, 1, -1);
xOffset(0); //=> -1
xOffset(1); //=> 0

當函式沒有明確的引數數量時,我們需要通過curryN方法明確的指明引數數量。

柯里化是Ramda.js的核心,但如果除此之外沒有更多功能的話這個庫就顯得不那麼有意思了。在函數語言程式設計中另一個重要概念就是不可變性。

不可變的結構

最簡單的防止函式改變程式狀態的方法就是隻操作不能被修改的資料結構。對於簡單物件來說我們需要只讀訪問器,如此一來像下邊的行為是不被允許的了。

var position = {
    x: 5,
    y: 9
};
position.x = 10; // works!

除了把屬性宣告為只讀以外,我們還可以把他們改成getter函式:

var position = (function (x, y) {
    return {
        getX: () => { return x; },
        getY: () => { return y; }
    };
})(5, 9);
position.getX() = 10; // does not work!

現在看起來好一些了,但是這個物件還是可能被修改。某人可以直接覆蓋getX函式的宣告:

position.getX = function () {
  return 10;
};

實現不可變性最好的途徑是使用Object.freeze。配合const關鍵字我們可以建立一個不能被修改的不可變變數。

const position = Object.freeze({ x: 5, y: 9 });

另一個例子涉及到列表。向一個不可變列表新增一個元素需要你為原始列表建立一份拷貝並將新元素新增到拷貝的末尾。當然我們也可以在原始物件上使用不可變性的知識來進行優化。這樣我們就可以用一個簡單的引用替換掉拷貝。本質上,它會變成某種形式的連結串列。需要我們注意的是標準的JavaScript陣列是可變的,因此需要通過拷貝保證正確性。

append()這樣的方法可以操作JavaScript陣列並返回這種拷貝後的陣列。這個操作是冪等的;如果使用相同的引數呼叫函式多次,我們將得到相同的結果:

R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']
R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']
R.append('tests', ['write', 'more']); //=> ['write', 'more', 'tests']

(譯註:這裡作者舉例並不恰當,因為例子中的每次呼叫都是在一個全新的陣列字面量上執行的。並不能證明R.append的冪等性)

還有一個remove方法用於刪除陣列中的指定元素。它像下面這樣工作:

R.remove('write', 'tests', ['write', 'more', 'tests']); //=> ['more']

由於該方法的引數個數是不固定的,但我們要對它進行柯里化時我們需要使用前面提到的curryN函式。此外,Ramda.js還提供了一套通用的輔助方法。

工具方法

所有輔助方法最重要的一點就是引數被以方便進行柯里化的方式排序。一個引數變化的越頻繁,那麼它在引數列表中的位置就越靠後。

sum() 和 range()

常見的操作如sumrange當然可以在Ramda.js中找到:

R.sum(R.range(1, 5)); //=> 10

針對range()方法,我們可以利用柯里化建立一個包裝器:

var from10ToExclusive = R.range(10);
from10ToExclusive(15); //=> [10, 11, 12, 13, 14]

如果我們想把它包裝成一個固定了最大值的函式呢?Ramda.js為我們提供了一個表示為R.__的特殊引數:

var to14FromInclusive = R.range(R.__, 15);
to14FromInclusive(10); //=> [10, 11, 12, 13, 14]

map()

此外,Ramda.js還嘗試為JavaScript內建方法,例如Array.prototype.map,提供更好的替代方案。這些替代方案使用了不同的引數順序並且自帶柯里化功能。

對於map方法,它看起來像下面這樣:

R.map(x => 2 * x, [1, 2, 3]); //=> [2, 4, 6]

prop()

另一個有用的工具是prop方法,它用來獲取某個特定屬性的值。如果指定屬性不存在,它就返回undefined。當這個屬性的值真的是undefined的時候,這可能會造成混淆。但在實踐中我們並不會區分這兩種情況。

R.prop('x', { x: 100 }); //=> 100
R.prop('x', { y: 50 }); //=> undefined

zipWith()

如果前面介紹的幾個方法還不能說服你Ramda.js真的有用,那麼接下來的例子對你來說可能會更有趣。這次我們不會在看某個具體的例子而是會看幾個隨意選擇的情景。

假設我們要合併兩個列表。這個功能使用zip方法實現很簡單。然而,該方法的返回值(一個新陣列,其中每個元素又是一個包含兩個待合併陣列相同位置值的陣列)有可能並不是你想要的。這時就該zipWith方法登場了。它可以使用任意函式將來自兩個列表的值對映為一個。

var letters = ["A", "B", "C", "D", "E"];
var numbers = [1, 2, 3];
var zipper = R.zipWith((x, y) => x + y);
zipper(letters, numbers); // ["A1", "B2", "C3"]

類似的,我們可以建立一個計算向量點積的函式:

var dot = R.pipe(R.zipWith((x, y) => x * y), R.sum);
dot([1, 2, 3], [1, 2, 3]) // 14

我們先是使用乘法將兩個數組合並(得到[1, 4, 9])然後將結果交給sum函式。

不管怎麼說,操作可列舉的資料型別是個很大的話題。不出乎意料的,Ramda.js帶來了很多有用的工具。我們已經介紹過的R.map可以在每個元素上執行同一方法。類似的,還有一些工具用於減少元素的數量。無論是使用最常見的filter方法(返回一個新陣列)還是使用reduce函式獲取一個單一的值。

chain()

Ramda.js自帶了一些運算元組的有用方法。例如,使用chain可以很容易的合併陣列。假設我們有一個primeFactorization函式接受一個數字作為引數並輸出該數字的質因子。我們可以像下面這樣將該函式作用於一組數字並把結果合併在一起:

R.chain(primeFactorization, [4, 7, 21]); //=> [2, 2, 7, 3, 7]

一個實際的例子

到目前為止一切順利。現在最大的問題是:通過在日常工作中使用Ramda.js引入的概念,我們能得到什麼好處呢?假設我們有如下(看起來已經不錯了)的程式碼片段:

fetchFromServer()
  .then(JSON.parse)
  .then(function (data){ return data.posts })
  .then(function (posts){
    return posts.map(function (post){ return post.title })
  });

如何利用Ramda.js使這段程式碼變得更加可讀呢?事實上,第一行其實已經很好了。第二行就已經變得雜亂了。我們要做的不過是提取傳入引數的posts屬性。

最後,我們有一個凌亂的第三行。這裡我們想要迴圈所有post。同樣的,這裡的目的只是想抽取一個特定的屬性。下面的解決方案怎麼樣?

fetchFromServer()
  .then(JSON.parse)
  .then(R.prop('posts'))
  .then(R.map(R.prop('title')));

得益於Ramda.js提供的函數語言程式設計能力,這可能是最具可讀性的方案了。然而,要知道ECMAScript 6引入的‘胖箭頭’語法也提供了一種簡單,易讀的方案:

fetchFromServer()
  .then(JSON.parse)
  .then(json => json.posts)
  .then(posts => posts.map(p => p.title));

在不需要任何Ramda.js知識的情況下,這幾乎擁有同樣的可讀性。此外,我們還減少了抽象 —— 這對效能和可維護性是有益的。

Lenses

最後,我們也應該討論一下有用的物件輔助方法。這裡我們要提到的是lens方法。

lens是一個特殊的物件,它可以和一個數組或物件一起作為引數傳遞給Ramda.js函式。它允許這些函式讀取或操作對應物件或陣列上的特定屬性或索引的值。

假設我們有一個帶有兩個鍵xy的物件 —— 就像在本文開始提到的不可變性中的例子那樣。我們可以建立一個lenses來‘聚焦’到我們感興趣的屬性上,而不是使用getter和setter建立一個新的包裹物件。

要建立一個訪問x屬性的lens,我們可以這麼寫:

var x = R.lens(R.prop('x'), R.assoc('x'));

prop是一個標準的getter方法(這一點已經介紹過了), assoc是一個setter方法(語法包括3個值: key, value, object)。

現在就可以使用Ramda.js中的函式訪問該lens定義的熟悉了。

var xCoordinate = R.view(x, position);
var newPosition = R.set(x, 7, position);

要注意的是這裡的操作並不會修改position物件(不管我們是否forze過它)。

要知道的是set只是over的一種特殊形式。overset很類似只不過第二個引數接受一個函式。這個函式會被用來改變屬性的值。例如,下面的函式將會把x座標乘3:

var newPosition = R.over(x, R.multiply(3), position);

Ramda.js, lodash, 還是其他什麼東西?

一個合理的問題是為什麼要選擇Ramda.js?為什麼不是lodash或是其他的什麼東西?當然有人可能會爭辯說Ramda.js更新,因此一定更好。但這並不是事實。事實是Ramda.js的設計充分考慮了函數語言程式設計的原則 —— 在引數位置和選擇方面使用了新的方式。

舉例來說,Ramda.js中的列表迭代器預設只會傳入迴圈元素,而不包括列表。另一方面,其他庫(比如lodash)的標準做法是給回掉函式傳入元素和索引。這看起來是個微不足道的問題,但它阻礙了你使用像parseInt()這樣的內建函式(接受一個可選的第二個引數)。然而,Ramda.js卻可以完美的工作。

最後,選擇哪個庫需要由具體的需求或是團隊的經驗/能力來決定,但也有一些不錯的應該重視Ramda.js的理由

測試地址:

http://ramdajs.com/repl/?v=0.23.0

相關推薦

實戰語言程式設計使用Ramda.js

對我來說,使得JavaScript如此有趣的一個原因是它函數語言程式設計方面的特性。從一開始函式就是JavaScript世界中的一等公民。這使得通過多種方式的組合編寫優雅,富有表現力的程式碼成為可能。 然而,僅僅是擁有一些函數語言程式設計的能力並不代表你的程式碼就是函式式的。Ramda.js是一個很流行

Scala - 08 - 語言程式設計高階函式

函數語言程式設計的崛起 函數語言程式設計中的 “值不可變性”避免了對公共的可變狀態進行同步訪問控制的複雜問題,能夠較好滿足分散式並行程式設計的需求,適應大資料時代的到來。 函式是第一等公民 可以作為實參傳遞給另外一個函式 可以作為返回值 可以賦值給變數 可以儲存在

Scala - 09 - 語言程式設計一些操作

1- 集合類(collection) 系統地區分了可變的和不可變的集合。 scala.collection包中所有的集合類 可變集合(Mutable) 顧名思義,意味著可以修改,移除或者新增一個元素。 scala.collectio

C# 語言程式設計LINQ

一直以來,我以為 LINQ 是專門用來對不同資料來源進行查詢的工具,直到我看了這篇十多年前的文章,才發現 LINQ 的功能遠不止 Query。這篇文章的內容比較高階,主要寫了用 C# 3.0 推出的 LINQ 語法實現了一套“解析器組合子(Parser Combinator)”的過程。那麼這個組合子是用來幹什

語言程式設計如何高效簡潔地對資料查詢與變換

摘要:一提到程式設計正規化,很容易聯想到宗教的虔誠,每種宗教所表達信條都有一定合理性,但如果一直只遵循一種教條,可能也被讓自己痛苦不堪,程式設計正規化也是如此。 案例1 案例一,程式碼摘抄來自一企業培訓材料,主要程式碼邏輯是列印每課成績,並找出學生非F級別課程統計平均分數: class CourseGr

Python語言程式設計(一)高階函式

首先有一個高階函式的知識。 一個函式可以接收另一個函式作為引數,這種函式就稱之為高階函式。 def add(x, y, f): return f(x) + f(y) 當我們呼叫add(-5, 6, abs)時,引數x,y和f分別接收-5,6和abs,根據函式定義,我們可以推導計算過程為:

JS語言程式設計 - 函式組合與柯里化

我們都知道單一職責原則,其實面向物件的SOLID中的S(SRP, Single responsibility principle)。在函式式當中每一個函式就是一個單元,同樣應該只做一件事。但是現實世界總是複雜的,當把現實世界對映到程式設計時,單一的函式就沒有太大的意義。這個時候就需要函式組合和柯里化了。 鏈

Scala實戰高手****第14課Scala集合上的語言程式設計實戰及Spark原始碼鑑賞

package com.dt.spark.scala.bascis object Functional_Itearal {   def main(args: Array[String]): Unit = {        val range

Python學習筆記(十二)lambda表示式與語言程式設計

以Mark Lutz著的《Python學習手冊》為教程,每天花1個小時左右時間學習,爭取兩週完成。 --- 寫在前面的話 2013-7-22 21:00 學習筆記 1,lambda的一般形式是關鍵字lambda後面跟一個或多個引數,緊跟一個冒號,以後是一個表示

python3入門與實踐(六)語言程式設計

匿名函式 lambda # 1.parameter_list 引數列表 # 2.expression 函式體,只能是有一些簡單的,注意不是程式碼塊,比如不能寫賦值語句 # 3.不需要return lambda parameter_list: expression 複製程式碼 def add(x,y):

JavaScript兩大支柱-PART2語言程式設計

JavaScript是有史以來最重要的程式語言之一,不僅僅是因為它的流行,而且因為它推廣了兩個對程式設計發展極為重要的特性: 原型繼承(沒有類的物件,原型委託,又名OLOO - 連結到其他物件的物件),和 函數語言程式設計(由帶閉包的lambdas啟用) 總的來

Python進階語言程式設計例項(附程式碼)

上篇文章“幾個小例子告訴你, 一行Python程式碼能幹哪些事 -- 知乎專欄”中用到了一些列表解析、生成器、map、filter、lambda、zip等表達形式,這就涉及到了Python中關於函數語言程式設計(functional programming)的語法、函式等

Python學習筆記19語言程式設計

import shutil#copy()複製檔案#格式: shutil.copy(來源路徑,目標路徑)#返回值:返回目標路徑#拷貝的同時,可以給檔案重新命名rst = shutil.copy('/home/dadada/hahaha.huhu', '/home/dadada/hahaha.hoho')prin

Python學習筆記Python語言程式設計

Python學習筆記:Python函數語言程式設計 學自廖雪峰巨佬的Python3教程:https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014317848428125ae6a

第3講Scala語言程式設計徹底精通

簡介: 本篇博文主要是對Scala函數語言程式設計的徹底詳解,涉及高階函式,閉包,顆粒化等詳解。 1. Scala函數語言程式設計 Scala中的函式的可以不依賴類或則藉口,獨立

ScalaFunction(高階語言程式設計

一個可以進行高階函數語言程式設計的模組,我們來看看它都實現了哪些操作,並結合原始碼理解一下。 def chain[a](fs: Seq[(a) ⇒ a]): (a) ⇒ a 把一些列的方法串起來,挨個執行,每個方法的結果,回作為下一個方法的入參

程式語言的發展趨勢及未來方向(3)語言程式設計

這是Anders Hejlsberg(不用介紹這是誰了吧)在比利時TechDays 2010所做的開場演講。由於最近我在部落格上關於語言的討論比較多,出於應景,也打算將Anders的演講完整地聽寫出來。在上一部分中,Anders闡述了他眼中宣告式程式設計的理念及DSL,並演示C#中一種內部DSL的形式:LIN

Python語言程式設計指南(四)生成器

生成器是迭代器,同時也並不僅僅是迭代器,不過迭代器之外的用途實在是不多,所以我們可以大聲地說:生成器提供了非常方便的自定義迭代器的途徑。 這是函數語言程式設計指南的最後一篇,似乎拖了一個星期才寫好,嗯…… 轉載請註明原作者和原文地址:) 4.

js 語言程式設計 淺談

js 函數語言程式設計 函式式的思想, 就是不斷地用已有函式, 來組合出新的函式。 函數語言程式設計具有五個鮮明的特點: 1. 函式是"第一等公民" 指的是函式與其他資料型別一樣,處於平等地位 2. 只用"表示式",不用"語句" "表示式"(expression)是一個單純

(六)Python語言程式設計1高階函式 Higher-order-function

# 高階函式 Higher-order-function # 變數可以指向函式 # 以Python內建的求絕對值的函式abs()為例,呼叫該函式用以下程式碼: print(abs(-10)) # 10 print(abs) # <built-in f