成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

JavaScript 函數(shù)式編程(三)

whjin / 1836人閱讀

摘要:函數(shù)式編程一般約定,函子有一個方法,用來生成新的容器。是實現(xiàn)了函數(shù)并遵守一些特定規(guī)則的容器類型。定義二若為廣群,且運算還滿足結(jié)合律,即任意,有,則稱為半群。

slide 地址

四、Talk is cheap!Show me the ... MONEY!
以下內(nèi)容主要參考自 Professor Frisby Introduces Composable Functional JavaScript

4.1.容器(Box)

假設(shè)有個函數(shù),可以接收一個來自用戶輸入的數(shù)字字符串。我們需要對其預處理一下,去除多余空格,將其轉(zhuǎn)換為數(shù)字并加一,最后返回該值對應(yīng)的字母。代碼大概長這樣...

const nextCharForNumStr = (str) =>
  String.fromCharCode(parseInt(str.trim()) + 1)

nextCharForNumStr(" 64 ") // "A"

因缺思廳,這代碼嵌套的也太緊湊了,看多了“老闊疼”,趕緊重構(gòu)一把...

const nextCharForNumStr = (str) => {
  const trimmed = str.trim()
  const number = parseInt(trimmed)
  const nextNumber = number + 1
  return String.fromCharCode(nextNumber)
}

nextCharForNumStr(" 64 ") // "A"

很顯然,經(jīng)過之前內(nèi)容的熏(xi)陶(nao),一眼就可以看出這個修訂版代碼很不 Pointfree...

為了這些只用一次的中間變量還要去想或者去查翻譯,也是容易“老闊疼”,再改再改~

const nextCharForNumStr = (str) => [str]
  .map(s => s.trim())
  .map(s => parseInt(s))
  .map(i => i + 1)
  .map(i => String.fromCharCode(i))

nextCharForNumStr(" 64 ") // ["A"]

這次借助數(shù)組的 map 方法,我們將必須的4個步驟拆分成了4個小函數(shù)。

這樣一來再也不用去想中間變量的名稱到底叫什么,而且每一步做的事情十分的清晰,一眼就可以看出這段代碼在干嘛。

我們將原本的字符串變量 str 放在數(shù)組中變成了 [str],這里就像放在一個容器里一樣。

代碼是不是感覺好 door~~ 了?

不過在這里我們可以更進一步,讓我們來創(chuàng)建一個新的類型 Box。我們將同樣定義 map 方法,讓其實現(xiàn)同樣的功能。

const Box = (x) => ({
  map: f => Box(f(x)),        // 返回容器為了鏈式調(diào)用
  fold: f => f(x),            // 將元素從容器中取出
  inspect: () => `Box(${x})`, // 看容器里有啥
})

const nextCharForNumStr = (str) => Box(str)
  .map(s => s.trim())
  .map(i => parseInt(i))
  .map(i => i + 1)
  .map(i => String.fromCharCode(i))
  .fold(c => c.toLowerCase()) // 可以輕易地繼續(xù)調(diào)用新的函數(shù)

nextCharForNumStr(" 64 ") // a

此外創(chuàng)建一個容器,除了像函數(shù)一樣直接傳遞參數(shù)以外,還可以使用靜態(tài)方法 of。

函數(shù)式編程一般約定,函子有一個 of 方法,用來生成新的容器。
Box(1) === Box.of(1)

其實這個 Box 就是一個函子(functor),因為它實現(xiàn)了 map 函數(shù)。當然你也可以叫它 Mappable 或者其他名稱。

不過為了保持與范疇學定義的名稱一致,我們就站在巨人的肩膀上不要再發(fā)明新名詞啦~(后面小節(jié)的各種奇怪名詞也是來源于數(shù)學名詞)。

functor 是實現(xiàn)了 map 函數(shù)并遵守一些特定規(guī)則的容器類型。

那么這些特定的規(guī)則具體是什么咧?

1. 規(guī)則一:

fx.map(f).map(g) === fx.map(x => g(f(x)))

這其實就是函數(shù)組合...

2. 規(guī)則二:

const id = x => x

fx.map(id) === id(fx)

4.2.Either / Maybe

假設(shè)現(xiàn)在有個需求:獲取對應(yīng)顏色的十六進制的 RGB 值,并返回去掉#后的大寫值。

const findColor = (name) => ({
  red: "#ff4444",
  blue: "#3b5998",
  yellow: "#fff68f",
})[name]

const redColor = findColor("red")
  .slice(1)
  .toUpperCase() // FF4444

const greenColor = findColor("green")
  .slice(1)
  .toUpperCase()
// Uncaught TypeError:
// Cannot read property "slice" of undefined

以上代碼在輸入已有顏色的 key 值時運行良好,不過一旦傳入其他顏色就會報錯。咋辦咧?

暫且不提條件判斷和各種奇技淫巧的錯誤處理。咱們來先看看函數(shù)式的解決方案~

函數(shù)式將錯誤處理抽象成一個 Either 容器,而這個容器由兩個子容器 RightLeft 組成。

// Either 由 Right 和 Left 組成

const Left = (x) => ({
  map: f => Left(x),            // 忽略傳入的 f 函數(shù)
  fold: (f, g) => f(x),         // 使用左邊的函數(shù)
  inspect: () => `Left(${x})`,  // 看容器里有啥
})

const Right = (x) => ({
  map: f => Right(f(x)),        // 返回容器為了鏈式調(diào)用
  fold: (f, g) => g(x),         // 使用右邊的函數(shù)
  inspect: () => `Right(${x})`, // 看容器里有啥
})

// 來測試看看~
const right = Right(4)
  .map(x => x * 7 + 1)
  .map(x => x / 2)

right.inspect() // Right(14.5)
right.fold(e => "error", x => x) // 14.5

const left = Left(4)
  .map(x => x * 7 + 1)
  .map(x => x / 2)

left.inspect() // Left(4)
left.fold(e => "error", x => x) // error

可以看出 RightLeft 相似于 Box

最大的不同就是 fold 函數(shù),這里需要傳兩個回調(diào)函數(shù),左邊的給 Left 使用,右邊的給 Right 使用。

其次就是 Leftmap 函數(shù)忽略了傳入的函數(shù)(因為出錯了嘛,當然不能繼續(xù)執(zhí)行啦)。

現(xiàn)在讓我們回到之前的問題來~

const fromNullable = (x) => x == null
  ? Left(null)
  : Right(x)

const findColor = (name) => fromNullable(({
  red: "#ff4444",
  blue: "#3b5998",
  yellow: "#fff68f",
})[name])

findColor("green")
  .map(c => c.slice(1))
  .fold(
    e => "no color",
    c => c.toUpperCase()
  ) // no color

從以上代碼不知道各位讀者老爺們有沒有看出使用 Either 的好處,那就是可以放心地對于這種類型的數(shù)據(jù)進行任何操作,而不是在每個函數(shù)里面小心翼翼地進行參數(shù)檢查。

4.3.Chain / FlatMap / bind / >>=

假設(shè)現(xiàn)在有個 json 文件里面保存了端口,我們要讀取這個文件獲取端口,要是出錯了返回默認值 3000。

// config.json
{ "port": 8888 }

// chain.js
const fs = require("fs")

const getPort = () => {
  try {
    const str = fs.readFileSync("config.json")
    const { port } = JSON.parse(str)
    return port
  } catch(e) {
    return 3000
  }
}

const result = getPort()

so easy~,下面讓我們來用 Either 來重構(gòu)下看看效果。

const fs = require("fs")

const Left = (x) => ({ ... })
const Right = (x) => ({ ... })

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, c => c.port)
啊,常規(guī)操作,看起來不錯喲~

不錯你個蛇頭...!

以上代碼有個 bug,當 json 文件寫的有問題時,在 JSON.parse 時會出錯,所以這步也要用 tryCatch 包起來。

但是,問題來了...

返回值這時候可能是 Right(Right("")) 或者 Right(Left(e))(想想為什么不是 Left(Right("")) 或者 Left(Left(e)))。

也就是說我們現(xiàn)在得到的是兩層容器,就像俄羅斯套娃一樣...

要取出容器中的容器中的值,我們就需要 fold 兩次...!(若是再多幾層...)

因缺思廳,所以聰明機智的函數(shù)式又想出一個新方法 chain~,其實很簡單,就是我知道這里要返回容器了,那就不要再用容器包了唄。

...

const Left = (x) => ({
  ...
  chain: f => Left(x) // 和 map 一樣,直接返回 Left
})

const Right = (x) => ({
  ...
  chain: f => f(x),   // 直接返回,不使用容器再包一層了
})

const tryCatch = (f) => { ... }

const getPort = () => tryCatch(
    () => fs.readFileSync("config.json")
  )
  .chain(c => tryCatch(() => JSON.parse(c))) // 使用 chain 和 tryCatch
  .fold(
    e => 3000,
    c => c.port
  )

其實這里的 LeftRight 就是單子(Monad),因為它實現(xiàn)了 chain 函數(shù)。

monad 是實現(xiàn)了 chain 函數(shù)并遵守一些特定規(guī)則的容器類型。

在繼續(xù)介紹這些特定規(guī)則前,我們先定義一個 join 函數(shù):

// 這里的 m 指的是一種 Monad 實例
const join = m => m.chain(x => x)

規(guī)則一:

join(m.map(join)) === join(join(m))

規(guī)則二:

// 這里的 M 指的是一種 Monad 類型
join(M.of(m)) === join(m.map(M.of))

這條規(guī)則說明了 map 可被 chainof 所定義。

m.map(f) === m.chain(x => M.of(f(x)))

也就是說 Monad 一定是 Functor

Monad 十分強大,之后我們將利用它處理各種副作用。但別對其感到困惑,chain 的主要作用不過將兩種不同的類型連接(join)在一起罷了。

4.4.半群(Semigroup)
定義一:對于非空集合 S,若在 S 上定義了二元運算 ○,使得對于任意的 a, b ∈ S,有 a ○ b ∈ S,則稱 {S, ○} 為廣群。

定義二:若 {S, ○} 為廣群,且運算 ○ 還滿足結(jié)合律,即:任意 a, b, c ∈ S,有 (a ○ b) ○ c = a ○ (b ○ c),則稱 {S, ○} 為半群。

舉例來說,JavaScript 中有 concat 方法的對象都是半群。

// 字符串和 concat 是半群
"1".concat("2").concat("3") === "1".concat("2".concat("3"))

// 數(shù)組和 concat 是半群
[1].concat([2]).concat([3]) === [1].concat([2].concat([3]))

雖然理論上對于 來說它符合半群的定義:

數(shù)字相加返回的仍然是數(shù)字(廣群)

加法滿足結(jié)合律(半群)

但是數(shù)字并沒有 concat 方法

沒事兒,讓我們來實現(xiàn)這個由 組成的半群 Sum。

const Sum = (x) => ({
  x,
  concat: ({ x: y }) => Sum(x + y), // 采用解構(gòu)獲取值
  inspect: () => `Sum(${x})`,
})

Sum(1)
  .concat(Sum(2))
  .inspect() // Sum(3)

除此之外, 也滿足半群的定義~

const All = (x) => ({
  x,
  concat: ({ x: y }) => All(x && y), // 采用解構(gòu)獲取值
  inspect: () => `All(${x})`,
})

All(true)
  .concat(All(false))
  .inspect() // All(false)

最后,讓我們對于字符串創(chuàng)建一個新的半群 First,顧名思義,它會忽略除了第一個參數(shù)以外的內(nèi)容。

const First = (x) => ({
  x,
  concat: () => First(x), // 忽略后續(xù)的值
  inspect: () => `First(${x})`,
})

First("blah")
  .concat(First("yoyoyo"))
  .inspect() // First("blah")
咿呀喲?是不是感覺這個半群和其他半群好像有點兒不太一樣,不過具體是啥又說不上來...?

這個問題留給下個小節(jié)。在此先說下這玩意兒有啥用。

const data1 = {
  name: "steve",
  isPaid: true,
  points: 10,
  friends: ["jame"],
}
const data2 = {
  name: "steve",
  isPaid: false,
  points: 2,
  friends: ["young"],
}

假設(shè)有兩個數(shù)據(jù),需要將其合并,那么利用半群,我們可以對 name 應(yīng)用 First,對于 isPaid 應(yīng)用 All,對于 points 應(yīng)用 Sum,最后的 friends 已經(jīng)是半群了...

const Sum = (x) => ({ ... })
const All = (x) => ({ ... })
const First = (x) => ({ ... })

const data1 = {
  name: First("steve"),
  isPaid: All(true),
  points: Sum(10),
  friends: ["jame"],
}
const data2 = {
  name: First("steve"),
  isPaid: All(false),
  points: Sum(2),
  friends: ["young"],
}

const concatObj = (obj1, obj2) => Object.entries(obj1)
  .map(([ key, val ]) => ({
    // concat 兩個對象的值
    [key]: val.concat(obj2[key]),
  }))
  .reduce((acc, cur) => ({ ...acc, ...cur }))

concatObj(data1, data2)
/*
  {
    name: First("steve"),
    isPaid: All(false),
    points: Sum(12),
    friends: ["jame", "young"],
  }
*/
4.5.幺半群(Monoid)
幺半群是一個存在單位元(幺元)的半群。

半群我們都懂,不過啥是單位元?

單位元:對于半群 ,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a

舉例來說,對于數(shù)字加法這個半群來說,0就是它的單位元,所以 就構(gòu)成一個幺半群。同理:

對于 來說單位元就是 1

對于 來說單位元就是 true

對于 來說單位元就是 false

對于 來說單位元就是 Infinity

對于 來說單位元就是 -Infinity

那么 是幺半群么?

顯然我們并不能找到這樣一個單位元 e 滿足

First(e).concat(First("steve")) === First("steve").concat(First(e))

這就是上一節(jié)留的小懸念,為何會感覺 First 與 Sum 和 All 不太一樣的原因。

格嘰格嘰,這兩者有啥具體的差別么?

其實看到幺半群的第一反應(yīng)應(yīng)該是默認值或初始值,例如 reduce 函數(shù)的第二個參數(shù)就是傳入一個初始值或者說是默認值。

// sum
const Sum = (x) => ({ ... })
Sum.empty = () => Sum(0) // 單位元

const sum = xs => xs.reduce((acc, cur) => acc + cur, 0)

sum([1, 2, 3])  // 6
sum([])         // 0,而不是報錯!

// all
const All = (x) => ({ ... })
All.empty = () => All(true) // 單位元

const all = xs => xs.reduce((acc, cur) => acc && cur, true)

all([true, false, true]) // false
all([])                  // true,而不是報錯!

// first
const First = (x) => ({ ... })

const first = xs => xs.reduce(acc, cur) => acc)

first(["steve", "jame", "young"]) // steve
first([])                         // boom!!!

從以上代碼可以看出幺半群比半群要安全得多,

4.6.foldMap 1.套路

在上一節(jié)中幺半群的使用代碼中,如果傳入的都是幺半群實例而不是原始類型的話,你會發(fā)現(xiàn)其實都是一個套路...

const Monoid = (x) => ({ ... })

const monoid = xs => xs.reduce(
    (acc, cur) => acc.concat(cur),  // 使用 concat 結(jié)合
    Monoid.empty()                  // 傳入幺元
)

monoid([Monoid(a), Monoid(b), Monoid(c)]) // 傳入幺半群實例

所以對于思維高度抽象的函數(shù)式來說,這樣的代碼肯定是需要繼續(xù)重構(gòu)精簡的~

2.List、Map

在講解如何重構(gòu)之前,先介紹兩個炒雞常用的不可變數(shù)據(jù)結(jié)構(gòu):ListMap。

顧名思義,正好對應(yīng)原生的 ArrayObject

3.利用 List、Map 重構(gòu)

因為 immutable 庫中的 ListMap 并沒有 empty 屬性和 fold 方法,所以我們首先擴展 List 和 Map~

import { List, Map } from "immutable"

const derived = {
  fold (empty) {
    return this.reduce((acc, cur) => acc.concat(cur), empty)
  },
}

List.prototype.empty = List()
List.prototype.fold = derived.fold

Map.prototype.empty = Map({})
Map.prototype.fold = derived.fold

// from https://github.com/DrBoolean/immutable-ext

這樣一來上一節(jié)的代碼就可以精簡成這樣:

List.of(1, 2, 3)
  .map(Sum)
  .fold(Sum.empty())     // Sum(6)

List().fold(Sum.empty()) // Sum(0)

Map({ steve: 1, young: 3 })
  .map(Sum)
  .fold(Sum.empty())     // Sum(4)

Map().fold(Sum.empty())  // Sum(0)
4.利用 foldMap 重構(gòu)

注意到 mapfold 這兩步操作,從邏輯上來說是一個操作,所以我們可以新增 foldMap 方法來結(jié)合兩者。

import { List, Map } from "immutable"

const derived = {
  fold (empty) {
    return this.foldMap(x => x, empty)
  },
  foldMap (f, empty) {
    return empty != null
      // 幺半群中將 f 的調(diào)用放在 reduce 中,提高效率
      ? this.reduce(
          (acc, cur, idx) =>
            acc.concat(f(cur, idx)),
          empty
      )
      : this
        // 在 map 中調(diào)用 f 是因為考慮到空的情況
        .map(f)
        .reduce((acc, cur) => acc.concat(cur))
  },
}

List.prototype.empty = List()
List.prototype.fold = derived.fold
List.prototype.foldMap = derived.foldMap

Map.prototype.empty = Map({})
Map.prototype.fold = derived.fold
Map.prototype.foldMap = derived.foldMap

// from https://github.com/DrBoolean/immutable-ext

所以最終版長這樣:

List.of(1, 2, 3)
  .foldMap(Sum, Sum.empty()) // Sum(6)
List()
  .foldMap(Sum, Sum.empty()) // Sum(0)

Map({ a: 1, b: 3 })
  .foldMap(Sum, Sum.empty()) // Sum(4)
Map()
  .foldMap(Sum, Sum.empty()) // Sum(0)
4.7.LazyBox

下面我們要來實現(xiàn)一個新容器 LazyBox。

顧名思義,這個容器很懶...

雖然你可以不停地用 map 給它分配任務(wù),但是只要你不調(diào)用 fold 方法催它執(zhí)行(就像 deadline 一樣),它就死活不執(zhí)行...

const LazyBox = (g) => ({
  map: f => LazyBox(() => f(g())),
  fold: f => f(g()),
})

const result = LazyBox(() => " 64 ")
  .map(s => s.trim())
  .map(i => parseInt(i))
  .map(i => i + 1)
  .map(i => String.fromCharCode(i))
  // 沒有 fold 死活不執(zhí)行

result.fold(c => c.toLowerCase()) // a
4.8.Task 1.基本介紹

有了上一節(jié)中 LazyBox 的基礎(chǔ)之后,接下來我們來創(chuàng)建一個新的類型 Task。

首先 Task 的構(gòu)造函數(shù)可以接收一個函數(shù)以便延遲計算,當然也可以用 of 方法來創(chuàng)建實例,很自然的也有 mapchain、concatempty 等方法。

與眾不同的是它有個 fork 方法(類似于 LazyBox 中的 fold 方法,在 fork 執(zhí)行前其他函數(shù)并不會執(zhí)行),以及一個 rejected 方法,類似于 Left,忽略后續(xù)的操作。

import Task from "data.task"

const showErr = e => console.log(`err: ${e}`)
const showSuc = x => console.log(`suc: ${x}`)

Task
  .of(1)
  .fork(showErr, showSuc) // suc: 1

Task
  .of(1)
  .map(x => x + 1)
  .fork(showErr, showSuc) // suc: 2

// 類似 Left
Task
  .rejected(1)
  .map(x => x + 1)
  .fork(showErr, showSuc) // err: 1

Task
  .of(1)
  .chain(x => new Task.of(x + 1))
  .fork(showErr, showSuc) // suc: 2
2.使用示例

接下來讓我們做一個發(fā)射飛彈的程序~

const lauchMissiles = () => (
  // 和 promise 很像,不過 promise 會立即執(zhí)行
  // 而且參數(shù)的位置也相反
  new Task((rej, res) => {
    console.log("lauchMissiles")
    res("missile")
  })
)

// 繼續(xù)對之前的任務(wù)添加后續(xù)操作(duang~給飛彈加特技?。?const app = lauchMissiles()
  .map(x => x + "!")

// 這時才執(zhí)行(發(fā)射飛彈)
app.fork(showErr, showSuc)
3.原理意義

上面的代碼乍一看好像沒啥用,只不過是把待執(zhí)行的代碼用函數(shù)包起來了嘛,這還能吹上天?

還記得前面章節(jié)說到的副作用么?雖然說使用純函數(shù)是沒有副作用的,但是日常項目中有各種必須處理的副作用。

所以我們將有副作用的代碼給包起來之后,這些新函數(shù)就都變成了純函數(shù),這樣我們的整個應(yīng)用的代碼都是純的~,并且在代碼真正執(zhí)行前(fork 前)還可以不斷地 compose 別的函數(shù),為我們的應(yīng)用不斷添加各種功能,這樣一來整個應(yīng)用的代碼流程都會十分的簡潔漂亮。

4.異步嵌套示例

以下代碼做了 3 件事:

讀取 config1.json 中的數(shù)據(jù)

將內(nèi)容中的 8 替換成 6

將新內(nèi)容寫到 config2.json 中

import fs from "fs"

const app = () => (
  fs.readFile("config1.json", "utf-8", (err, contents) => {
    if (err) throw err

    const newContents = content.replace(/8/g, "6")

    fs.writeFile("config2.json", newContents, (err, _) => {
      if (err) throw err

      console.log("success!")
    })
  })
)

讓我們用 Task 來改寫一下~

import fs from "fs"
import Task from "data.task"

const cfg1 = "config1.json"
const cfg2 = "config2.json"

const readFile = (file, enc) => (
  new Task((rej, res) =>
    fs.readFile(file, enc, (err, str) =>
      err ? rej(err) : res(str)
    )
  )
)

const writeFile = (file, str) => (
  new Task((rej, res) =>
    fs.writeFile(file, str, (err, suc) =>
      err ? rej(err) : res(suc)
    )
  )
)

const app = readFile(cfg1, "utf-8")
  .map(str => str.replace(/8/g, "6"))
  .chain(str => writeFile(cfg2, str))

app.fork(
  e => console.log(`err: ${e}`),
  x => console.log(`suc: ${x}`)
)

代碼一目了然,按照線性的先后順序完成了任務(wù),并且在其中還可以隨意地插入或修改需求~

4.9.Applicative Functor 1.問題引入

Applicative Functor 提供了讓不同的函子(functor)互相應(yīng)用的能力。

為啥我們需要函子的互相應(yīng)用?什么是互相應(yīng)用?

先來看個簡單例子:

const add = x => y => x + y

add(Box.of(2))(Box.of(3)) // NaN

Box(2).map(add).inspect() // Box(y => 2 + y)

現(xiàn)在我們有了一個容器,它的內(nèi)部值為局部調(diào)用(partially applied)后的函數(shù)。接著我們想讓它應(yīng)用到 Box(3) 上,最后得到 Box(5) 的預期結(jié)果。

說到從容器中取值,那肯定第一個想到 chain 方法,讓我們來試一下:

Box(2)
  .chain(x => Box(3).map(add(x)))
  .inspect() // Box(5)

成功實現(xiàn)~,BUT,這種實現(xiàn)方法有個問題,那就是單子(Monad)的執(zhí)行順序問題。

我們這樣實現(xiàn)的話,就必須等 Box(2) 執(zhí)行完畢后,才能對 Box(3) 進行求值。假如這是兩個異步任務(wù),那么完全無法并行執(zhí)行。

別慌,吃口藥~
2.基本介紹

下面介紹下主角:ap~:

const Box = (x) => ({
  // 這里 box 是另一個 Box 的實例,x 是函數(shù)
  ap: box => box.map(x),
  ...
})

Box(add)
  // Box(y => 2 + y) ,咦?在哪兒見過?
  .ap(Box(2))
  .ap(Box(3)) // Box(5)

運算規(guī)則

F(x).map(f) === F(f).ap(F(x))

// 這就是為什么
Box(2).map(add) === Box(add).ap(Box(2))
3.Lift 家族

由于日常編寫代碼的時候直接用 ap 的話模板代碼太多,所以一般通過使用 Lift 家族系列函數(shù)來簡化。

// F 該從哪兒來?
const fakeLiftA2 = f => fx => fy => F(f).ap(fx).ap(fy)

// 應(yīng)用運算規(guī)則轉(zhuǎn)換一下~
const liftA2 = f => fx => fy => fx.map(f).ap(fy)

liftA2(add, Box(2), Box(4)) // Box(6)

// 同理
const liftA3 = f => fx => fy => fz => fx.map(f).ap(fy).ap(fz)
const liftA4 = ...
...
const liftAN = ...
4.Lift 應(yīng)用

例1

// 假裝是個 jQuery 接口~
const $ = selector =>
  Either.of({ selector, height: 10 })

const getScreenSize = screen => head => foot =>
  screen - (head.height + foot.height)

liftA2(getScreenSize(800))($("header"))($("footer")) // Right(780)

例2

// List 的笛卡爾乘積
List.of(x => y => z => [x, y, z].join("-"))
  .ap(List.of("tshirt", "sweater"))
  .ap(List.of("white", "black"))
  .ap(List.of("small", "medium", "large"))

例3

const Db = ({
  find: (id, cb) =>
    new Task((rej, res) =>
      setTimeout(() => res({ id, title: `${id}`}), 100)
    )
})

const reportHeader = (p1, p2) =>
  `Report: ${p1.title} compared to ${p2.title}`

Task.of(p1 => p2 => reportHeader(p1, p2))
  .ap(Db.find(20))
  .ap(Db.find(8))
  .fork(console.error, console.log) // Report: 20 compared to 8

liftA2
  (p1 => p2 => reportHeader(p1, p2))
  (Db.find(20))
  (Db.find(8))
  .fork(console.error, console.log) // Report: 20 compared to 8
4.10.Traversable 1.問題引入
import fs from "fs"

// 詳見 4.8.
const readFile = (file, enc) => (
  new Task((rej, res) => ...)
)

const files = ["a.js", "b.js"]

// [Task, Task],我們得到了一個 Task 的數(shù)組
files.map(file => readFile(file, "utf-8"))

然而我們想得到的是一個包含數(shù)組的 Task([file1, file2]),這樣就可以調(diào)用它的 fork 方法,查看執(zhí)行結(jié)果。

為了解決這個問題,函數(shù)式編程一般用一個叫做 traverse 的方法來實現(xiàn)。

files
  .traverse(Task.of, file => readFile(file, "utf-8"))
  .fork(console.error, console.log)

traverse 方法第一個參數(shù)是創(chuàng)建函子的函數(shù),第二個參數(shù)是要應(yīng)用在函子上的函數(shù)。

2.實現(xiàn)

其實以上代碼有 bug...,因為數(shù)組 Array 是沒有 traverse 方法的。沒事兒,讓我們來實現(xiàn)一下~

Array.prototype.empty = []

// traversable
Array.prototype.traverse = function (point, fn) {
  return this.reduce(
    (acc, cur) => acc
      .map(z => y => z.concat(y))
      .ap(fn(cur)),
    point(this.empty)
  )
}
看著有點兒暈?

不急,首先看代碼主體是一個 reduce,這個很熟了,就是從左到右遍歷元素,其中的第二個參數(shù)傳遞的就是幺半群(monoid)的單位元(empty)。

再看第一個參數(shù),主要就是通過 applicative functor 調(diào)用 ap 方法,再將其執(zhí)行結(jié)果使用 concat 方法合并到數(shù)組中。

所以最后返回的就是 Task([foo, bar]),因此我們可以調(diào)用 fork 方法執(zhí)行它。

4.11.自然變換(Natural Transformations) 1.基本概念

自然變換就是一個函數(shù),接受一個函子(functor),返回另一個函子。看看代碼熟悉下~

const boxToEither = b => b.fold(Right)

這個 boxToEither 函數(shù)就是一個自然變換(nt),它將函子 Box 轉(zhuǎn)換成了另一個函子 Either

那么我們用 Left 行不行呢?

答案是不行!

因為自然變換不僅是將一個函子轉(zhuǎn)換成另一個函子,它還滿足以下規(guī)則:

nt(x).map(f) == nt(x.map(f))

舉例來說就是:

const res1 = boxToEither(Box(100))
  .map(x => x * 2)
const res2 = boxToEither(
  Box(100).map(x => x * 2)
)

res1 === res2 // Right(200)

即先對函子 a 做改變再將其轉(zhuǎn)換為函子 b,是等價于先將函子 a 轉(zhuǎn)換為函子 b 再做改變。

顯然,Left 并不滿足這個規(guī)則。所以任何滿足這個規(guī)則的函數(shù)都是自然變換。

2.應(yīng)用場景

1.例1:得到一個數(shù)組小于等于 100 的最后一個數(shù)的兩倍的值

const arr = [2, 400, 5, 1000]
const first = xs => fromNullable(xs[0])
const double = x => x * 2
const getLargeNums = xs => xs.filter(x => x > 100)

first(
  getLargeNums(arr).map(double)
)

根據(jù)自然變換,它顯然和 first(getLargeNums(arr)).map(double) 是等價的。但是后者顯然性能好得多。

再來看一個更復雜一點兒的例子:

2.例2:找到 id 為 3 的用戶的最好的朋友的 id

// 假 api
const fakeApi = (id) => ({
  id,
  name: "user1",
  bestFriendId: id + 1,
})

// 假 Db
const Db = {
  find: (id) => new Task(
    (rej, res) => (
      res(id > 2
        ? Right(fakeApi(id))
        : Left("not found")
      )
    )
  )
}
// Task(Either(user))
const zero = Db.find(3)

// 第一版
// Task(Either(Task(Either(user)))) ???
const one = zero
  .map(either => either
    .map(user => Db
      .find(user.bestFriendId)
    )
  )
  .fork(
    console.error,
    either => either // Either(Task(Either(user)))
      .map(t => t.fork( // Task(Either(user))
        console.error,
        either => either
            .map(console.log), // Either(user)
      ))
  )

這是什么鬼???

肯定不能這么干...

// Task(Either(user))
const zero = Db.find(3)

// 第二版
const two = zero
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
    .chain(user => Db
      .find(user.bestFriendId) // Task(Either(user))
    )
    .chain(either => either
      .fold(Task.rejected, Task.of) // Task(user)
    )
  )
  .fork(
    console.error,
    console.log,
  )

第二版的問題是多余的嵌套代碼。

// Task(Either(user))
const zero = Db.find(3)

// 第三版
const three = zero
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
  )
  .chain(user => Db
    .find(user.bestFriendId) // Task(Either(user))
  )
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
  )
  .fork(
    console.error,
    console.log,
  )

第三版的問題是多余的重復邏輯。

// Task(Either(user))
const zero = Db.find(3)

// 這其實就是自然變換
// 將 Either 變換成 Task
const eitherToTask = (e) => (
  e.fold(Task.rejected, Task.of)
)

// 第四版
const four = zero
  .chain(eitherToTask) // Task(user)
  .chain(user => Db
    .find(user.bestFriendId) // Task(Either(user))
  )
  .chain(eitherToTask) // Task(user)
  .fork(
    console.error,
    console.log,
  )

// 出錯版
const error = Db.find(2) // Task(Either(user))
  // Task.rejected("not found")
  .chain(eitherToTask)
  // 這里永遠不會被調(diào)用,被跳過了
  .chain(() => console.log("hey man"))
  ...
  .fork(
    console.error, // not found
    console.log,
  )
4.12.同構(gòu)(Isomorphism)
同構(gòu)是在數(shù)學對象之間定義的一類映射,它能揭示出在這些對象的屬性或者操作之間存在的關(guān)系。

簡單來說就是兩種不同類型的對象經(jīng)過變形,保持結(jié)構(gòu)并且不丟失數(shù)據(jù)。

具體怎么做到的呢?

其實同構(gòu)就是一對兒函數(shù):tofrom,遵守以下規(guī)則:

to(from(x)) === x
from(to(y)) === y

這其實說明了這兩個類型都能夠無損地保存同樣的信息。

1. 例如 String[Char] 就是同構(gòu)的。
// String ~ [Char]
const Iso = (to, from) => ({ to, from })

const chars = Iso(
  s => s.split(""),
  c => c.join("")
)

const str = "hello world"

chars.from(chars.to(str)) === str

這能有啥用呢?

const truncate = (str) => (
  chars.from(
    // 我們先用 to 方法將其轉(zhuǎn)成數(shù)組
    // 這樣就能使用數(shù)組的各類方法
    chars.to(str).slice(0, 3)
  ).concat("...")
)

truncate(str) // hel...
2. 再來看看最多有一個參數(shù)的數(shù)組 [a]Either 的同構(gòu)關(guān)系
// [a] ~ Either null a
const singleton = Iso(
  e => e.fold(() => [], x => [x]),
  ([ x ]) => x ? Right(x) : Left()
)

const filterEither = (e, pred) => singleton
  .from(
    singleton
      .to(e)
      .filter(pred)
  )

const getUCH = (str) => filterEither(
  Right(str),
  x => x.match(/h/ig)
).map(x => x.toUpperCase())

getUCH("hello") // Right(HELLO)

getUCH("ello") // Left(undefined)
參考資料

JS函數(shù)式編程指南

Pointfree 編程風格指南

Hey Underscore, You"re Doing It Wrong!

Functional Concepts with JavaScript: Part I

Professor Frisby Introduces Composable Functional JavaScript

函數(shù)式編程入門教程

What are Functional Programming, Monad, Monoid, Applicative, Functor ??

相關(guān)文章

JavaScript 函數(shù)式編程(一)

JavaScript 函數(shù)式編程(二)

JavaScript 函數(shù)式編程(三)-- 本文

JavaScript 函數(shù)式編程(四)正在醞釀...

以上 to be continued...

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/96872.html

相關(guān)文章

  • gitbook: 前端好書推薦

    摘要:它大致概述并討論了前端工程的實踐如何學習它,以及在年實踐時使用什么工具。目的是每年發(fā)布一次內(nèi)容更新。前端實踐第一部分廣泛描述了前端工程的實踐。對大多數(shù)人來說,函數(shù)式編程看起來更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 這是任何人都可以用來了解前端開發(fā)實踐的指南。它大致概述并...

    Ali_ 評論0 收藏0
  • gitbook: 前端好書推薦

    摘要:它大致概述并討論了前端工程的實踐如何學習它,以及在年實踐時使用什么工具。目的是每年發(fā)布一次內(nèi)容更新。前端實踐第一部分廣泛描述了前端工程的實踐。對大多數(shù)人來說,函數(shù)式編程看起來更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 這是任何人都可以用來了解前端開發(fā)實踐的指南。它大致概述并...

    CocoaChina 評論0 收藏0
  • gitbook: 前端好書推薦

    摘要:它大致概述并討論了前端工程的實踐如何學習它,以及在年實踐時使用什么工具。目的是每年發(fā)布一次內(nèi)容更新。前端實踐第一部分廣泛描述了前端工程的實踐。對大多數(shù)人來說,函數(shù)式編程看起來更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 這是任何人都可以用來了解前端開發(fā)實踐的指南。它大致概述并...

    Warren 評論0 收藏0
  • JavaScript面向?qū)ο?em>編程-繼承(

    摘要:子類不是父類實例的問題是由類式繼承引起的。所以寄生式繼承和構(gòu)造函數(shù)繼承的組合又稱為一種新的繼承方式。但是這里的寄生式繼承處理的不是對象,而是類的原型??瓷先ヂ晕碗s,還得好好研究。 寄生組合式繼承(終極繼承者) 前面學習了類式繼承和構(gòu)造函數(shù)繼承組合使用,也就是組合繼承,但是這種繼承方式有個問題,就是子類不是父類的實例,而子類的原型是父類的實例。子類不是父類實例的問題是由類式繼承引起的。...

    alaege 評論0 收藏0
  • SegmentFault 技術(shù)周刊 Vol.16 - 淺入淺出 JavaScript 函數(shù)編程

    摘要:函數(shù)式編程,一看這個詞,簡直就是學院派的典范。所以這期周刊,我們就重點引入的函數(shù)式編程,淺入淺出,一窺函數(shù)式編程的思想,可能讓你對編程語言的理解更加融會貫通一些。但從根本上來說,函數(shù)式編程就是關(guān)于如使用通用的可復用函數(shù)進行組合編程。 showImg(https://segmentfault.com/img/bVGQuc); 函數(shù)式編程(Functional Programming),一...

    csRyan 評論0 收藏0
  • JavaScript函數(shù)語言的特性

    摘要:在函數(shù)內(nèi)保存數(shù)據(jù)在命令式語言中,函數(shù)內(nèi)部的私有變量局部變量是不能被保存的。從程序的執(zhí)行方式上來講,局部變量在棧上分配,在函數(shù)執(zhí)行結(jié)束后,所占用的棧被釋放。這一點其實是破壞它的函數(shù)式特性的。 本文內(nèi)容是我閱讀《JavaScript語言精髓與編程實踐》時,做的讀書筆記,周愛民老師的書寫的太深刻了! 函數(shù)式語言中的函數(shù) 首先要有一個概念:并不是一個語言支持函數(shù),這個語言就可以叫做函數(shù)式語言。...

    BlackHole1 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<