你們都知道JavaScript
能夠做爲面向對象
或者函數式
編程語言來使用,通常狀況下你們理解的函數式編程
無非包括反作用
、函數組合
、柯里化
這些概念,其實並否則,若是往深瞭解學習會發現函數式編程
還包括很是多的高級特性,好比functor
、monad
等。國外課程網站egghead
上有個教授(名字叫Frisby)基於JavaScript
講解的函數式編程
很是棒,主要介紹了box
、semigroup
、monoid
、functor
、applicative functor
、monad
、isomorphism
等函數式編程相關的高級主題內容。整個課程大概30節左右,本篇文章主要是對該課程的翻譯與總結,有精力的強烈推薦你們觀看原課程 Professor Frisby Introduces Composable Functional JavaScript 。課程最後有個小實踐項目你們能夠練練手,體會下這種不一樣的編程方式。 這裏提早聲明下,本個課程裏面介紹的monad
等高級特性
不見得你們都在項目中能用到,不過能夠拓寬下知識面,另外也有助於學習haskell
這類純函數式編程javascript
Box
)建立線性數據流普通函數是這樣的:java
function nextCharForNumberString (str) {
const trimmed = str.trim();
const number = parseInt(trimmed);
const nextNumber = number + 1;
return String.fromCharCode(nextNumber);
}
const result = nextCharForNumberString(' 64');
console.log(result); // "A"
複製代碼
若是藉助Array,能夠這樣實現:node
const nextCharForNumberString = str =>
[str]
.map(s => s.trim())
.map(s => parseInt(s))
.map(i => i + 1)
.map(i => String.fromCharCode(i));
const result = nextCharForNumberString(' 64');
console.log(result); // ["A"]
複製代碼
這裏咱們把數據str
裝進了一個箱子(數組),而後連續屢次調用箱子的map
方法來處理箱子內部的數據。這種實現已經能夠感覺到一些奇妙之處了。再看一種基本思想相同的實現方式,只不過此次咱們不借助數組,而是本身實現箱子:git
const Box = x => ({
map: f => Box(f(x)),
fold: f => f(x),
toString: () => `Box(${x})`
});
const nextCharForNumberString = str =>
Box(str)
.map(s => s.trim())
.map(s => parseInt(s))
.map(i => i + 1)
.map(i => String.fromCharCode(i));
const result = nextCharForNumberString(' 64');
console.log(String(result)); // "Box(A)"
複製代碼
至此咱們本身動手實現了一個箱子。連續使用map
能夠組合一組操做,以建立線性的數據流。箱子中不只能夠放數據,還能夠放函數,別忘了函數也是一等公民:github
const Box = x => ({
map: f => Box(f(x)),
fold: f => f(x),
toString: () => `Box(${x})`
});
const f0 = x => x * 100; // think fo as a data
const add1 = f => x => f(x) + 1; // think add1 as a function
const add2 = f => x => f(x) + 2; // think add2 as a function
const g = Box(f0)
.map(f => add1(f))
.map(f => add2(f))
.fold(f => f);
const res = g(1);
console.log(res); // 103
複製代碼
這裏當你對一個函數容器調用map
時,實際上是在作函數組合。數據庫
Box
重構命令式代碼這裏使用的Box
跟上一節同樣:npm
const Box = x => ({
map: f => Box(f(x)),
fold: f => f(x),
toString: () => `Box(${x})`
});
複製代碼
命令式moneyToFloat
:編程
const moneyToFloat = str =>
parseFloat(str.replace(/\$/g, ''));
複製代碼
Box
式moneyToFloat
:json
const moneyToFloat = str =>
Box(str)
.map(s => s.replace(/\$/g, ''))
.fold(r => parseFloat(r));
複製代碼
咱們這裏使用Box
重構了moneyToFloat
,Box
擅長的地方就在於將嵌套表達式轉成一個一個的map
,這裏雖然不是很複雜,但倒是一種好的實踐方式。後端
命令式percentToFloat
:
const percentToFloat = str => {
const replaced = str.replace(/\%/g, '');
const number = parseFloat(replaced);
return number * 0.01;
};
複製代碼
Box
式percentToFloat
:
const percentToFloat = str =>
Box(str)
.map(str => str.replace(/\%/g, ''))
.map(replaced => parseFloat(replaced))
.fold(number => number * 0.01);
複製代碼
咱們這裏又使用Box
重構了percentToFloat
,顯然這種實現方式的數據流更加清晰。
命令式applyDiscount
:
const applyDiscount = (price, discount) => {
const cost = moneyToFloat(price);
const savings = percentToFloat(discount);
return cost - cost * savings;
};
複製代碼
重構applyDiscount
稍微麻煩點,由於該函數有兩條數據流,不過咱們能夠藉助閉包:
Box
式applyDiscount
:
const applyDiscount = (price, discount) =>
Box(price)
.map(price => moneyToFloat(price))
.fold(cost =>
Box(discount)
.map(discount => percentToFloat(discount))
.fold(savings => cost - cost * savings));
複製代碼
如今能夠看一下這組代碼的輸出了:
const result = applyDiscount('$5.00', '20%');
console.log(String(result)); // "4"
複製代碼
若是咱們在moneyToFloat
和percentToFloat
中不進行拆箱(即fold
),那麼applyDiscount
就不必在數據轉換以前先裝箱(即Box
)了:
const moneyToFloat = str =>
Box(str)
.map(s => s.replace(/\$/g, ''))
.map(r => parseFloat(r)); // here we don't fold the result out
const percentToFloat = str =>
Box(str)
.map(str => str.replace(/\%/g, ''))
.map(replaced => parseFloat(replaced))
.map(number => number * 0.01); // here we don't fold the result out
const applyDiscount = (price, discount) =>
moneyToFloat(price)
.fold(cost =>
percentToFloat(discount)
.fold(savings => cost - cost * savings));
const result = applyDiscount('$5.00', '20%');
console.log(String(result)); // "4"
複製代碼
Either
進行分支控制Either
的意思是二者之一,不是Right
就是Left
。咱們先實現Right
:
const Right = x => ({
map: f => Right(f(x)),
toString: () => `Right(${x})`
});
const result = Right(3).map(x => x + 1).map(x => x / 2);
console.log(String(result)); // "Right(2)"
複製代碼
這裏咱們暫且不實現Right
的fold
,而是先來實現Left
:
const Left = x => ({
map: f => Left(x),
toString: () => `Left(${x})`
});
const result = Left(3).map(x => x + 1).map(x => x / 2);
console.log(String(result)); // "Left(3)"
複製代碼
Left
容器跟Right
是不一樣的,由於Left
徹底忽略了傳入的數據轉換函數,保持容器內部數據原樣。有了Right
和Left
,咱們能夠對程序數據流進行分支控制。考慮到程序中常常會存在異常,所以容器一般都是未知類型RightOrLeft
。
接下來咱們實現Right
和Left
容器的fold
方法,若是未知容器是Right
,則使用第二個函數參數g
進行拆箱:
const Right = x => ({
map: f => Right(f(x)),
fold: (f, g) => g(x),
toString: () => `Right(${x})`
});
複製代碼
若是未知容器是Left
,則使用第一個函數參數f
進行拆箱:
const Left = x => ({
map: f => Left(x),
fold: (f, g) => f(x),
toString: () => `Left(${x})`
});
複製代碼
測試一下Right
和Left
的fold
方法:
const result = Right(2).map(x => x + 1).map(x => x / 2).fold(x => 'error', x => x);
console.log(result); // 1.5
複製代碼
const result = Left(2).map(x => x + 1).map(x => x / 2).fold(x => 'error', x => x);
console.log(result); // 'error'
複製代碼
藉助Either
咱們能夠進行程序流程分支控制,例如進行異常處理、null
檢查等。
下面看一個例子:
const findColor = name =>
({red: '#ff4444', blue: '#3b5998', yellow: '#fff68f'})[name];
const result = findColor('red').slice(1).toUpperCase();
console.log(result); // "FF4444"
複製代碼
這裏若是咱們給函數findColor
傳入green
,則會報錯。所以能夠藉助Either
進行錯誤處理:
const findColor = name => {
const found = {red: '#ff4444', blue: '#3b5998', yellow: '#fff68f'}[name];
return found ? Right(found) : Left(null);
};
const result = findColor('green')
.map(c => c.slice(1))
.fold(e => 'no color',
c => c.toUpperCase());
console.log(result); // "no color"
複製代碼
更進一步,咱們能夠提煉出一個專門用於null
檢測的Either
容器,同時簡化findColor
代碼:
const fromNullable = x =>
x != null ? Right(x) : Left(null); // [!=] will test both null and undefined
const findColor = name =>
fromNullable({red: '#ff4444', blue: '#3b5998', yellow: '#fff68f'}[name]);
複製代碼
chain
解決Either
的嵌套問題看一個讀取配置文件config.json
的例子,若是位置文件讀取失敗則提供一個默認端口3000
,命令式代碼實現以下:
const fs = require('fs');
const getPort = () => {
try {
const str = fs.readFileSync('config.json');
const config = JSON.parse(str);
return config.port;
} catch (e) {
return 3000;
}
};
const result = getPort();
console.log(result); // 8888 or 3000
複製代碼
咱們使用Either
重構:
const fs = require('fs');
const tryCatch = f => {
try {
return Right(f());
} catch (e) {
return Left(e);
}
};
const getPort = () =>
tryCatch(() => fs.readFileSync('config.json'))
.map(c => JSON.parse(c))
.fold(
e => 3000,
obj => obj.port
);
const result = getPort();
console.log(result); // 8888 or 3000
複製代碼
重構後就完美了嗎?咱們用到了JSON.parse
,若是config.json
文件格式有問題,程序就會報錯:
SyntaxError: Unexpected end of JSON input
所以須要針對JSON
解析失敗作異常處理,咱們能夠繼續使用tryCatch
來解決這個問題:
const getPort = () =>
tryCatch(() => fs.readFileSync('config.json'))
.map(c => tryCatch(() => JSON.parse(c)))
.fold(
left => 3000, // 第一個tryCatch失敗
right => right.fold( // 第一個tryCatch成功
e => 3000, // JSON.parse失敗
c => c.port
)
);
複製代碼
此次重構咱們使用了兩次tryCatch
,所以致使箱子套了兩層,最後須要進行兩次拆箱。爲了解決這種箱子套箱子的問題,咱們能夠給Right
和Left
增長一個方法chain
:
const Right = x => ({
chain: f => f(x),
map: f => Right(f(x)),
fold: (f, g) => g(x),
toString: () => `Right(${x})`
});
const Left = x => ({
chain: f => Left(x),
map: f => Left(x),
fold: (f, g) => f(x),
toString: () => `Left(${x})`
});
複製代碼
當咱們使用map
,又不想在數據轉換以後又增長一層箱子時,咱們應該使用chain
:
const getPort = () =>
tryCatch(() => fs.readFileSync('config.json'))
.chain(c => tryCatch(() => JSON.parse(c)))
.fold(
e => 3000,
c => c.port
);
複製代碼
Either
實現舉例const openSite = () => {
if (current_user) {
return renderPage(current_user);
}
else {
return showLogin();
}
};
const openSite = () =>
fromNullable(current_user)
.fold(showLogin, renderPage);
複製代碼
const streetName = user => {
const address = user.address;
if (address) {
const street = address.street;
if (street) {
return street.name;
}
}
return 'no street';
};
const streetName = user =>
fromNullable(user.address)
.chain(a => fromNullable(a.street))
.map(s => s.name)
.fold(
e => 'no street',
n => n
);
複製代碼
const concatUniq = (x, ys) => {
const found = ys.filter(y => y ===x)[0];
return found ? ys : ys.concat(x);
};
const cancatUniq = (x, ys) =>
fromNullable(ys.filter(y => y ===x)[0])
.fold(null => ys.concat(x), y => ys);
複製代碼
const wrapExamples = example => {
if (example.previewPath) {
try {
example.preview = fs.readFileSync(example.previewPath);
}
catch (e) {}
}
return example;
};
const wrapExamples = example =>
fromNullable(example.previewPath)
.chain(path => tryCatch(() => fs.readFileSync(path)))
.fold(
() => example,
preview => Object.assign({preview}, example)
);
複製代碼
半羣是一種具備concat
方法的類型,而且該concat
方法知足結合律。好比Array
和String
:
const res = "a".concat("b").concat("c");
const res = [1, 2].concat([3, 4].concat([5, 6])); // law of association
複製代碼
咱們自定義Sum
半羣,Sum
類型用來求和:
const Sum = x => ({
x,
concat: o => Sum(x + o.x),
toString: () => `Sum(${x})`
});
const res = Sum(1).concat(Sum(2));
console.log(String(res)); // "Sum(3)"
複製代碼
繼續自定義All
半羣,All
類型用來級聯布爾類型:
const All = x => ({
x,
concat: o => All(x && o.x),
toString: () => `All(${x})`
});
const res = All(true).concat(All(false));
console.log(String(res)); // "All(false)"
複製代碼
繼續定義First
半羣,First
類型鏈式調用concat
方法不改變其初始值:
const First = x => ({
x,
concat: o => First(x),
toString: () => `First(${x})`
});
const res = First('blah').concat(First('ice cream'));
console.log(String(res)); // "First(blah)"
複製代碼
這裏先佔位,回頭再補充。
const acct1 = Map({
name: First('Nico'),
isPaid: All(true),
points: Sum(10),
friends: ['Franklin']
});
const acct2 = Map({
name: First('Nico'),
isPaid: All(false),
points: Sum(2),
friends: ['Gatsby']
});
const res = acct1.concat(acct2);
console.log(res);
複製代碼
半羣知足結合律,若是半羣還具備幺元(單位元),那麼就是monoid。幺元與其餘元素結合時不會改變那些元素,能夠用公式表示以下:
e・a = a・e = a
咱們將半羣Sum
升級實現爲monoid只需實現一個empty
方法,調用改方法便可獲得該monoid的幺元:
const Sum = x => ({
x,
concat: o => Sum(x + o.x),
toString: () => `Sum(${x})`
});
Sum.empty = () => Sum(0);
const res = Sum.empty().concat(Sum(1).concat(Sum(2)));
// const res = Sum(1).concat(Sum(2)).concat(Sum.empty());
console.log(String(res)); // "Sum(3)"
複製代碼
接着咱們繼續將All
升級實現爲monoid:
const All = x => ({
x,
concat: o => All(x && o.x),
toString: () => `All(${x})`
});
All.empty = () => All(true);
const res = All(true).concat(All(true)).concat(All.empty());
console.log(String(res)); // "All(true)"
複製代碼
若是咱們嘗試着將半羣First
也升級爲monoid就會發現不可行,好比First('hello').concat(…)
的結果恆爲hello
,可是First.empty().concat(First('hello'))
的結果就不必定是hello
了,所以咱們沒法將半羣First
升級爲monoid。這也說明monoid必定是半羣,可是半羣不必定是monoid。半羣須要知足結合律,monoid不只須要知足結合律,還必須存在幺元。
Sum(求和):
const Sum = x => ({
x,
concat: o => Sum(x + o.x),
toString: () => `Sum(${x})`
});
Sum.empty = () => Sum(0);
複製代碼
Product(求積):
const Product = x => ({
x,
concat: o => Product(x * o.x),
toString: () => `Product(${x})`
});
Product.empty = () => Product(1);
const res = Product.empty().concat(Product(2)).concat(Product(3));
console.log(String(res)); // "Product(6)"
複製代碼
Any(只要有一個爲true
即返回true
,不然返回false
):
const Any = x => ({
x,
concat: o => Any(x || o.x),
toString: () => `Any(${x})`
});
Any.empty = () => Any(false);
const res = Any.empty().concat(Any(false)).concat(Any(false));
console.log(String(res)); // "Any(false)"
複製代碼
All(全部均爲true
才返回true
,不然返回false
):
const All = x => ({
x,
concat: o => All(x && o.x),
toString: () => `All(${x})`
});
All.empty = () => All(true);
const res = All(true).concat(All(true)).concat(All.empty());
console.log(String(res)); // "All(true)"
複製代碼
Max(求最大值):
const Max = x => ({
x,
concat: o => Max(x > o.x ? x : o.x),
toString: () => `Max(${x})`
});
Max.empty = () => Max(-Infinity);
const res = Max.empty().concat(Max(100)).concat(Max(200));
console.log(String(res)); // "Max(200)"
複製代碼
Min(求最小值):
const Min = x => ({
x,
concat: o => Min(x < o.x ? x : o.x),
toString: () => `Min(${x})`
});
Min.empty = () => Min(Infinity);
const res = Min.empty().concat(Min(100)).concat(Min(200));
console.log(String(res)); // "Min(100)"
複製代碼
foldMap
對集合彙總假設咱們須要對一個Sum
集合進行彙總,能夠這樣實現:
const res = [Sum(1), Sum(2), Sum(3)]
.reduce((acc, x) => acc.concat(x), Sum.empty());
console.log(res); // Sum(6)
複製代碼
考慮到這個操做的通常性,能夠抽成一個函數fold
。用node
安裝immutable
和immutable-ext
。immutable-ext
提供了fold
方法:
const {Map, List} = require('immutable-ext');
const {Sum} = require('./monoid');
const res = List.of(Sum(1), Sum(2), Sum(3))
.fold(Sum.empty());
console.log(res); // Sum(6)
複製代碼
也許你會以爲fold
接受的參數應該是一個函數,由於前面幾節介紹的fold
就是這樣的,好比Box
和Right
:
Box(3).fold(x => x); // 3
Right(3).fold(e => e, x => x); // 3
複製代碼
沒錯,不過fold
的本質就是拆箱。前面對Box
和Right
類型拆箱是將其值取出來;而如今對集合拆箱則是爲了將集合的彙總結果取出來。而將一個集合中的多個值彙總成一個值就須要傳入初始值Sum.empty()
。所以當你看到fold
時,應該當作是爲了從一個類型中取值出來,而這個類型多是一個僅含一個值的類型(好比Box
,Right
),也多是一個monoid集合。
咱們繼續看另一種集合Map
:
const res = Map({brian: Sum(3), sara: Sum(5)})
.fold(Sum.empty());
console.log(res); // Sum(8)
複製代碼
這裏的Map
是monoid集合,若是是普通數據集合能夠先使用集合的map
方法將該集合轉換成monoid集合:
const res = Map({brian: 3, sara: 5})
.map(Sum)
.fold(Sum.empty());
console.log(res); // Sum(8)
複製代碼
const res = List.of(1, 2, 3)
.map(Sum)
.fold(Sum.empty());
console.log(res); // Sum(6)
複製代碼
咱們能夠把這種對普通數據類型集合調用map
轉換成monoid類型集合,而後再調用fold
進行數據彙總的操做抽出來,即爲foldMap
:
const res = List.of(1, 2, 3)
.foldMap(Sum, Sum.empty());
console.log(res); // Sum(6)
複製代碼
LazyBox
延遲求值首先回顧一下前面Box
的例子:
const Box = x => ({
map: f => Box(f(x)),
fold: f => f(x),
toString: () => `Box(${x})`
});
const res = Box(' 64')
.map(s => s.trim())
.map(s => parseInt(s))
.map(i => i + 1)
.map(i => String.fromCharCode(i))
.fold(x => x.toLowerCase());
console.log(String(res)); // a
複製代碼
這裏進行了一系列的數據轉換,最後轉換成了a
。如今咱們能夠定義一個LazyBox
,延遲執行這一系列數據轉換函數,直到最後扣動扳機:
const LazyBox = g => ({
map: f => LazyBox(() => f(g())),
fold: f => f(g())
});
const res = LazyBox(() => ' 64')
.map(s => s.trim())
.map(s => parseInt(s))
.map(i => i + 1)
.map(i => String.fromCharCode(i))
.fold(x => x.toLowerCase());
console.log(res); // a
複製代碼
LazyBox
的參數是一個參數爲空的函數。在LazyBox
上調用map
並不會當即執行傳入的數據轉換函數,每調用一次map
待執行函數隊列中就會多一個函數,直到最後調用fold
扣動扳機,前面全部的數據轉換函數一觸一發,一個接一個的執行。這種模式有助於實現純函數。
Task
中捕獲反作用本節依然是討論Lazy特性,只不過基於data.task
庫,該庫能夠經過npm安裝。假設咱們要實現一個發射火箭的函數,若是咱們這樣實現,那麼該函數顯然不是純函數:
const launchMissiles = () =>
console.log('launch missiles!'); // 使用console.log模仿發射火箭
複製代碼
若是使用data.task
能夠藉助其Lazy特性,延遲執行:
const Task = require('data.task');
const launchMissiles = () =>
new Task((rej, res) => {
console.log('launch missiles!');
res('missile');
});
複製代碼
顯然這樣實現launchMissiles
即爲純函數。咱們能夠繼續在其基礎上組合其餘邏輯:
const app = launchMissiles().map(x => x + '!');
app
.map(x => x + '!')
.fork(
e => console.log('err', e),
x => console.log('success', x)
);
// launch missiles!
// success missile!!
複製代碼
調用fork
方法纔會扣動扳機,執行前面定義的Task
以及一系列數據轉換函數,若是不調用fork
,Task
中的console.log
操做就不會執行。
Task
處理異步任務假設咱們要實現讀文件,替換文件內容,而後寫文件的操做,命令式代碼以下:
const fs = require('fs');
const app = () =>
fs.readFile('config.json', 'utf-8', (err, contents) => {
if (err) throw err;
const newContents = contents.replace(/8/g, '6');
fs.writeFile('config1.json', newContents,
(err, success) => {
if (err) throw err;
console.log('success');
})
});
app();
複製代碼
這裏實現的app
內部會拋出異常,不是純函數。咱們能夠藉助Task
重構以下:
const Task = require('data.task');
const fs = require('fs');
const readFile = (filename, enc) =>
new Task((rej, res) =>
fs.readFile(filename, enc, (err, contents) =>
err ? rej(err) : res(contents)));
const writeFile = (filename, contents) =>
new Task((rej, res) =>
fs.writeFile(filename, contents, (err, success) =>
err ? rej(err) : res(success)));
const app = () =>
readFile('config.json', 'utf-8')
.map(contents => contents.replace(/8/g, '6'))
.chain(contents => writeFile('config1.json', contents));
app().fork(
e => console.log(e),
x => console.log('success')
);
複製代碼
這裏實現的app
是純函數,調用app().fork
纔會執行一系列動做。再看看data.task
官網的順序讀兩個文件的例子:
const fs = require('fs');
const Task = require('data.task');
const readFile = path =>
new Task((rej, res) =>
fs.readFile(path, 'utf-8', (error, contents) =>
error ? rej(error) : res(contents)));
const concatenated = readFile('Task_test_file1.txt')
.chain(a =>
readFile('Task_test_file2.txt')
.map(b => a + b));
concatenated.fork(console.error, console.log);
複製代碼
Functor是具備map
方法的類型,而且須要知足下面兩個條件:
fx.map(f).map(g) == fx.map(x => g(f(x)))
fx.map(id) == id(fx), where const id = x => x
以Box
類型爲例說明:
const Box = x => ({
map: f => Box(f(x)),
fold: f => f(x),
inspect: () => `Box(${x})`
});
const res1 = Box('squirrels')
.map(s => s.substr(5))
.map(s => s.toUpperCase());
const res2 = Box('squirrels')
.map(s => s.substr(5).toUpperCase());
console.log(res1, res2); // Box(RELS) Box(RELS)
複製代碼
顯然Box
知足第一個條件。注意這裏的s = > s.substr(5).toUpperCase()
其實本質上跟g(f(x))
是同樣的,咱們徹底從新定義成下面這種形式,不要被形式迷惑:
const f = s => s.substr(5);
const g = s => s.toUpperCase();
const h = s => g(f(s));
const res = Box('squirrels')
.map(h);
console.log(res); // Box(RELS)
複製代碼
接下來咱們看是否知足第二個條件:
const id = x => x;
const res1 = Box('crayons').map(id);
const res2 = id(Box('crayons'));
console.log(res1, res2); // Box(crayons) Box(crayons)
複製代碼
顯然也知足第二個條件。
of
方法將值放入Pointed Functorpointed functor是具備of
方法的functor,of
能夠理解成使用一個初始值來填充functor。以Box
爲例說明:
const Box = x => ({
map: f => Box(f(x)),
fold: f => f(x),
inspect: () => `Box(${x})`
});
Box.of = x => Box(x);
const res = Box.of(100);
console.log(res); // Box(100)
複製代碼
這裏再舉個functor的例子,IO functor:
const R = require('ramda');
const IO = x => ({
x, // here x is a function
map: f => IO(R.compose(f, x)),
fold: f => f(x) // get out x
});
IO.of = x => IO(x);
複製代碼
IO是一個值爲函數的容器,細心的話你會發現這就是前面的值爲函數的Box
容器。藉助IO functor,咱們能夠純函數式的處理一些IO操做了,由於讀寫操做就好像所有放入了隊列同樣,直到最後調用IO內部的函數時纔會扣動扳機執行一系列操做,試一下:
const R = require('ramda');
const {IO} = require('./IO');
const fake_window = {
innerWidth: '1000px',
location: {
href: "http://www.baidu.com/cpd/fe"
}
};
const io_window = IO(() => fake_window);
const getWindowInnerWidth = io_window
.map(window => window.innerWidth)
.fold(x => x);
const split = x => s => s.split(x);
const getUrl = io_window
.map(R.prop('location'))
.map(R.prop('href'))
.map(split('/'))
.fold(x => x);
console.log(getWindowInnerWidth()); // 1000px
console.log(getUrl()); // [ 'http:', '', 'www.baidu.com', 'cpd', 'fe' ]
複製代碼
functor能夠將一個函數做用到一個包着的(這裏「包着」意思是值存在於箱子內,下同)值上面:
Box(1).map(x => x + 1); // Box(2)
複製代碼
applicative functor能夠將一個包着的函數做用到一個包着的值上面:
const add = x => x + 1;
Box(add).ap(Box(1)); // Box(2)
複製代碼
而monod能夠將一個返回箱子類型的函數做用到一個包着的值上面,重點是做用以後包裝層數不增長:
先看個Box
functor的例子:
const Box = x => ({
map: f => Box(f(x)),
fold: f => f(x),
inspect: () => `Box(${x})`
});
const res = Box(1)
.map(x => Box(x))
.map(x => Box(x)); // Box(Box(Box(1)))
console.log(res); // Box([object Object])
複製代碼
這裏咱們連續調用map
而且map
時傳入的函數的返回值是箱子類型,顯然這樣會致使箱子的包裝層數不斷累加,咱們能夠給Box
增長join
方法來拆包裝:
const Box = x => ({
map: f => Box(f(x)),
join: () => x,
fold: f => f(x),
inspect: () => `Box(${x})`
});
const res = Box(1)
.map(x => Box(x))
.join()
.map(x => Box(x))
.join();
console.log(res); // Box(1)
複製代碼
這裏定義join
僅僅是爲了說明拆包裝這個操做,咱們固然可使用fold
完成相同的功能:
const Box = x => ({
map: f => Box(f(x)),
join: () => x,
fold: f => f(x),
inspect: () => `Box(${x})`
});
const res = Box(1)
.map(x => Box(x))
.fold(x => x)
.map(x => Box(x))
.fold(x => x);
console.log(res); // Box(1)
複製代碼
考慮到.map(...).join()
的通常性,咱們能夠爲Box
增長一個方法chain
完成這兩步操做:
const Box = x => ({
map: f => Box(f(x)),
join: () => x,
chain: f => Box(x).map(f).join(),
fold: f => f(x),
inspect: () => `Box(${x})`
});
const res = Box(1)
.chain(x => Box(x))
.chain(x => Box(x));
console.log(res); // Box(1)
複製代碼
這個很是簡單,直接舉例,能看懂這些例子就明白柯里化了:
const modulo = dvr => dvd => dvd % dvr;
const isOdd = modulo(2); // 求奇數
const filter = pred => xs => xs.filter(pred);
const getAllOdds = filter(isOdd);
const res1 = getAllOdds([1, 2, 3, 4]);
console.log(res1); // [1, 3]
const map = f => xs => xs.map(f);
const add = x => y => x + y;
const add1 = add(1);
const allAdd1 = map(add1);
const res2 = allAdd1([1, 2, 3]);
console.log(res2); // [2, 3, 4]
複製代碼
前面介紹的Box
是一個functor,咱們爲其添加ap
方法,將其升級成applicative functor:
const Box = x => ({
ap: b2 => b2.map(x), // here x is a function
map: f => Box(f(x)),
fold: f => f(x),
inspect: () => `Box(${x})`
});
const res = Box(x => x + 1).ap(Box(2));
console.log(res); // Box(3)
複製代碼
這裏Box
內部是一個一元函數,咱們也可使用柯里化後的多元函數:
const add = x => y => x + y;
const res = Box(add).ap(Box(2));
console.log(res); // Box([Function])
複製代碼
顯然咱們applicative functor上調用一次ap
便可消掉一個參數,這裏res
內部存的是仍然是一個函數:y => 2 + y
,只不過消掉了參數x
。咱們能夠連續調用ap
方法:
const res = Box(add).ap(Box(2)).ap(Box(3));
console.log(res); // Box(5)
複製代碼
稍加思考咱們會發現對於applicative functor,存在下面這個恆等式:
F(x).map(f) = F(f).ap(F(x))
即在一個保存值x
的functor上調用map(f)
,恆等於在保存函數f
的functor上調用ap(F(x))
。
接着咱們實現一個處理applicative functor的工具函數liftA2
:
const liftA2 = (f, fx, fy) =>
F(f).ap(fx).ap(fy);
複製代碼
可是這裏須要知道具體的functor類型F
,所以藉助於前面的恆等式,咱們繼續定義下面的通常形式liftA2
:
const liftA2 = (f, fx, fy) =>
fx.map(f).ap(fy);
複製代碼
試一下:
const res1 = Box(add).ap(Box(2)).ap(Box(4));
const res2 = liftA2(add, Box(2), Box(4)); // utilize helper function liftA2
console.log(res1); // Box(6)
console.log(res2); // Box(6)
複製代碼
固然咱們也能夠定義相似的liftA3
,liftA4
等工具函數:
const liftA3 = (f, fx, fy, fz) =>
fx.map(f).ap(fy).ap(fz);
複製代碼
首先來定義either
:
const Right = x => ({
ap: e2 => e2.map(x), // declare as a applicative, here x is a function
chain: f => f(x), // declare as a monad
map: f => Right(f(x)),
fold: (f, g) => g(x),
inspect: () => `Right(${x})`
});
const Left = x => ({
ap: e2 => e2.map(x), // declare as a applicative, here x is a function
chain: f => Left(x), // declare as a monad
map: f => Left(x),
fold: (f, g) => f(x),
inspect: () => `Left(${x})`
});
const fromNullable = x =>
x != null ? Right(x) : Left(null); // [!=] will test both null and undefined
const either = {
Right,
Left,
of: x => Right(x),
fromNullable
};
複製代碼
能夠看出either
既是monad又是applicative functor。
假設咱們要計算頁面上除了header
和footer
以外的高度:
const $ = selector =>
either.of({selector, height: 10}); // fake DOM selector
const getScreenSize = (screen, header, footer) =>
screen - (header.height + footer.height);
複製代碼
若是使用monod
的chain
方法,能夠這樣實現:
const res = $('header')
.chain(header =>
$('footer').map(footer =>
getScreenSize(800, header, footer)));
console.log(res); // Right(780)
複製代碼
也可使用applicative
實現,不過首先須要柯里化getScreenSize
:
const getScreenSize = screen => header => footer =>
screen - (header.height + footer.height);
const res1 = either.of(getScreenSize(800))
.ap($('header'))
.ap($('footer'));
const res2 = $('header')
.map(getScreenSize(800))
.ap($('footer'));
const res3 = liftA2(getScreenSize(800), $('header'), $('footer'));
console.log(res1, res2, res3); // Right(780) Right(780) Right(780)
複製代碼
本節介紹使用applicative functor實現下面這種模式:
for (x in xs) {
for (y in ys) {
for (z in zs) {
// your code here
}
}
}
複製代碼
使用applicative functor重構以下:
const {List} = require('immutable-ext');
const merch = () =>
List.of(x => y => z => `${x}-${y}-${z}`)
.ap(List(['teeshirt', 'sweater']))
.ap(List(['large', 'medium', 'small']))
.ap(List(['black', 'white']));
const res = merch();
console.log(res);
複製代碼
假設咱們要發起兩次讀數據庫的請求:
const Task = require('data.task');
const Db = ({
find: id =>
new Task((rej, res) =>
setTimeOut(() => {
console.log(res);
res({id: id, title: `Project ${id}`})
}, 5000))
});
const report = (p1, p2) =>
`Report: ${p1.title} compared to ${p2.title}`;
複製代碼
若是使用monad
的chain
實現,那麼兩個異步事件只能順序執行:
Db.find(20).chain(p1 =>
Db.find(8).map(p2 =>
report(p1, p2)))
.fork(console.error, console.log);
複製代碼
使用applicatives重構:
Task.of(p1 => p2 => report(p1, p2))
.ap(Db.find(20))
.ap(Db.find(8))
.fork(console.error, console.log);
複製代碼
假設咱們準備讀取一組文件:
const fs = require('fs');
const Task = require('data.task');
const futurize = require('futurize').futurize(Task);
const {List} = require('immutable-ext');
const readFile = futurize(fs.readFile);
const files = ['box.js', 'config.json'];
const res = files.map(fn => readFile(fn, 'utf-8'));
console.log(res);
// [ Task { fork: [Function], cleanup: [Function] },
// Task { fork: [Function], cleanup: [Function] } ]
複製代碼
這裏res
是一個Task
數組,而咱們想要的是Task([])
這種類型,相似promise.all()
的功能。咱們能夠藉助traverse
方法使Task
類型從數組裏跳到外面:
[Task] => Task([])
實現以下:
const files = List(['box.js', 'config.json']);
files.traverse(Task.of, fn => readFile(fn, 'utf-8'))
.fork(console.error, console.log);
複製代碼
假設咱們準備發起一組http請求:
const fs = require('fs');
const Task = require('data.task');
const {List, Map} = require('immutable-ext');
const httpGet = (path, params) =>
Task.of(`${path}: result`);
const res = Map({home: '/', about: '/about', blog: '/blod'})
.map(route => httpGet(route, {}));
console.log(res);
// Map { "home": Task, "about": Task, "blog": Task }
複製代碼
這裏res
是一個值爲Task
的Map
,而咱們想要的是Task({})
這種類型,相似promise.all()
的功能。咱們能夠藉助traverse
方法使Task
類型從Map
裏跳到外面:
{Task} => Task({})
實現以下:
Map({home: '/', about: '/about', blog: '/blod'})
.traverse(Task.of, route => httpGet(route, {}))
.fork(console.error, console.log);
// Map { "home": "/: result", "about": "/about: result", "blog": "/blod: result" }
複製代碼
本節介紹一種functor如何轉換成另一種functor。例如將either
轉換成Task
:
const {Right, Left, fromNullable} = require('./either');
const Task = require('data.task');
const eitherToTask = e =>
e.fold(Task.rejected, Task.of);
eitherToTask(Right('nightingale'))
.fork(
e => console.error('err', e),
r => console.log('res', r)
); // res nightingale
eitherToTask(Left('nightingale'))
.fork(
e => console.error('err', e),
r => console.log('res', r)
); // err nightingale
複製代碼
將Box
轉換成Either
:
const {Right, Left, fromNullable} = require('./either');
const Box = require('./box');
const boxToEither = b =>
b.fold(Right);
const res = boxToEither(Box(100));
console.log(res); // Right(100)
複製代碼
你可能會疑惑爲何boxToEither
要轉換成Right
,而不是Left
,緣由就是本節討論的類型轉換須要知足該條件:
nt(fx).map(f) == nt(fx.map(f))
其中nt
是natural transform的縮寫,即天然類型轉換,全部知足該公式的函數均爲天然類型轉換。接着討論boxToEither
,若是前面轉換成Left
,咱們看下是否還能知足該公式:
const boxToEither = b =>
b.fold(Left);
const res1 = boxToEither(Box(100)).map(x => x * 2);
const res2 = boxToEither(Box(100).map(x => x * 2));
console.log(res1, res2); // Left(100) Left(200)
複製代碼
顯然不知足上面的條件。
再看一個天然類型轉換函數first
:
const first = xs =>
fromNullable(xs[0]);
const res1 = first([1, 2, 3]).map(x => x + 1);
const res2 = first([1, 2, 3].map(x => x + 1));
console.log(res1, res2); // Right(2) Right(2)
複製代碼
前面的公式代表,對於一個functor
,先進行天然類型轉換再map
等價於先map
再進行天然類型轉換。
先看下first
的一個用例:
const {fromNullable} = require('./either');
const first = xs =>
fromNullable(xs[0]);
const largeNumbers = xs =>
xs.filter(x => x > 100);
const res = first(largeNumbers([2, 400, 5, 1000]).map(x => x * 2));
console.log(res); // Right(800)
複製代碼
這種實現沒什麼問題,不過這裏將large numbers的每一個值都進行了乘2的map
,而我麼最後的結果僅僅須要第一個值,所以借用天然類型轉換公式咱們能夠改爲下面這種形式:
const res = first(largeNumbers([2, 400, 5, 1000])).map(x => x * 2);
console.log(res); // Right(800)
複製代碼
再看一個稍微複雜點的例子:
const {Right, Left} = require('./either');
const Task = require('data.task');
const fake = id => ({
id,
name: 'user1',
best_friend_id: id + 1
}); // fake user infomation
const Db = ({
find: id =>
new Task((rej, res) =>
res(id > 2 ? Right(fake(id)) : Left('not found')))
}); // fake database
const eitherToTask = e =>
e.fold(Task.rejected, Task.of);
複製代碼
這裏咱們模擬了一個數據庫以及一些用戶信息,並假設數據庫中只可以查到id
大於2的用戶。
如今咱們要查找某個用戶的好朋友的信息:
Db.find(3) // Task(Right(user))
.map(either =>
either.map(user => Db.find(user.best_friend_id))) // Task(Either(Task(Either)))
複製代碼
若是這裏使用chain
,看一下效果如何:
Db.find(3) // Task(Right(user))
.chain(either =>
either.map(user => Db.find(user.best_friend_id))) // Either(Task(Either))
複製代碼
這樣調用完以後也有有問題:容器的類型從Task
變成了Either
,這也不是咱們想看到的。下面咱們藉助天然類型轉換重構一下:
Db.find(3) // Task(Right(user))
.map(eitherToTask) // Task(Task(user))
複製代碼
爲了去掉一層包裝,咱們改用chain
:
Db.find(3) // Task(Right(user))
.chain(eitherToTask) // Task(user)
.chain(user =>
Db.find(user.best_friend_id)) // Task(Right(user))
.chain(eitherToTask)
.fork(
console.error,
console.log
); // { id: 4, name: 'user1', best_friend_id: 5 }
複製代碼
這裏討論的同構不是「先後端同構」的同構,而是一對知足以下要求的函數:
from(to(x)) == x
to(from(y)) == y
若是可以找到一對函數知足上述要求,則說明一個數據類型x
具備與另外一個數據類型y
相同的信息或結構,此時咱們說數據類型x
和數據類型y
是同構的。好比String
和[char]
就是同構的:
const Iso = (to, from) =>({
to,
from
});
// String ~ [char]
const chars = Iso(s => s.split(''), arr => arr.join(''));
const res1 = chars.from(chars.to('hello world'));
const res2 = chars.to(chars.from(['a', 'b', 'c']));
console.log(res1, res2); // hello world [ 'a', 'b', 'c' ]
複製代碼
這有什麼用呢?咱們舉個例子:
const filterString = (str1, str2, pred) =>
chars.from(chars.to(str1 + str2).filter(pred));
const res1 = filterString('hello', 'HELLO', x => x.match(/[aeiou]/ig));
console.log(res1); // eoEO
const toUpperCase = (arr1, arr2) =>
chars.to(chars.from(arr1.concat(arr2)).toUpperCase());
const res2 = toUpperCase(['h', 'e', 'l', 'l', 'o'], ['w', 'o', 'r', 'l', 'd']);
console.log(res2); // [ 'H', 'E', 'L', 'L', 'O', 'W', 'O', 'R', 'L', 'D' ]
複製代碼
這裏咱們藉助Array
的filter
方法來過濾String
中的字符;藉助String
的toUpperCase
方法來處理字符數組的大小寫轉換。可見有了同構,咱們能夠在兩種不一樣的數據類型之間互相轉換並調用其方法。
課程最後三節的實戰例子見:實戰。