亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

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

whjin / 1898人閱讀

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

slide 地址

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

4.1.容器(Box)

假設(shè)有個(gè)函數(shù),可以接收一個(gè)來(lái)自用戶輸入的數(shù)字字符串。我們需要對(duì)其預(yù)處理一下,去除多余空格,將其轉(zhuǎn)換為數(shù)字并加一,最后返回該值對(duì)應(yīng)的字母。代碼大概長(zhǎ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)過(guò)之前內(nèi)容的熏(xi)陶(nao),一眼就可以看出這個(gè)修訂版代碼很不 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個(gè)步驟拆分成了4個(gè)小函數(shù)。

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

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

代碼是不是感覺(jué)好 door~~ 了?

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

const Box = (x) => ({
  map: f => Box(f(x)),        // 返回容器為了鏈?zhǔn)秸{(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)建一個(gè)容器,除了像函數(shù)一樣直接傳遞參數(shù)以外,還可以使用靜態(tài)方法 of

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

其實(shí)這個(gè) Box 就是一個(gè)函子(functor),因?yàn)樗鼘?shí)現(xiàn)了 map 函數(shù)。當(dāng)然你也可以叫它 Mappable 或者其他名稱。

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

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

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

1. 規(guī)則一:

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

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

2. 規(guī)則二:

const id = x => x

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

4.2.Either / Maybe

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

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í)運(yùn)行良好,不過(guò)一旦傳入其他顏色就會(huì)報(bào)錯(cuò)。咋辦咧?

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

函數(shù)式將錯(cuò)誤處理抽象成一個(gè) Either 容器,而這個(gè)容器由兩個(gè)子容器 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)),        // 返回容器為了鏈?zhǔn)秸{(diào)用
  fold: (f, g) => g(x),         // 使用右邊的函數(shù)
  inspect: () => `Right(${x})`, // 看容器里有啥
})

// 來(lái)測(cè)試看看~
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ù),這里需要傳兩個(gè)回調(diào)函數(shù),左邊的給 Left 使用,右邊的給 Right 使用。

其次就是 Leftmap 函數(shù)忽略了傳入的函數(shù)(因?yàn)槌鲥e(cuò)了嘛,當(dāng)然不能繼續(xù)執(zhí)行啦)。

現(xiàn)在讓我們回到之前的問(wèn)題來(lái)~

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

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

4.3.Chain / FlatMap / bind / >>=

假設(shè)現(xiàn)在有個(gè) json 文件里面保存了端口,我們要讀取這個(gè)文件獲取端口,要是出錯(cuò)了返回默認(rèn)值 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~,下面讓我們來(lái)用 Either 來(lái)重構(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ī)操作,看起來(lái)不錯(cuò)喲~

不錯(cuò)你個(gè)蛇頭...!

以上代碼有個(gè) bug,當(dāng) json 文件寫(xiě)的有問(wèn)題時(shí),在 JSON.parse 時(shí)會(huì)出錯(cuò),所以這步也要用 tryCatch 包起來(lái)。

但是,問(wèn)題來(lái)了...

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

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

要取出容器中的容器中的值,我們就需要 fold 兩次...?。ㄈ羰窃俣鄮讓?..)

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

...

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
  )

其實(shí)這里的 LeftRight 就是單子(Monad),因?yàn)樗鼘?shí)現(xiàn)了 chain 函數(shù)。

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

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

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

規(guī)則一:

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

規(guī)則二:

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

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

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

也就是說(shuō) Monad 一定是 Functor

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

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

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

舉例來(lái)說(shuō),JavaScript 中有 concat 方法的對(duì)象都是半群。

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

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

雖然理論上對(duì)于 來(lái)說(shuō)它符合半群的定義:

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

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

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

沒(méi)事兒,讓我們來(lái)實(shí)現(xiàn)這個(gè)由 組成的半群 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)

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

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

First("blah")
  .concat(First("yoyoyo"))
  .inspect() // First("blah")
咿呀喲?是不是感覺(jué)這個(gè)半群和其他半群好像有點(diǎn)兒不太一樣,不過(guò)具體是啥又說(shuō)不上來(lái)...?

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

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

假設(shè)有兩個(gè)數(shù)據(jù),需要將其合并,那么利用半群,我們可以對(duì) name 應(yīng)用 First,對(duì)于 isPaid 應(yīng)用 All,對(duì)于 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 兩個(gè)對(duì)象的值
    [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)
幺半群是一個(gè)存在單位元(幺元)的半群。

半群我們都懂,不過(guò)啥是單位元?

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

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

對(duì)于 來(lái)說(shuō)單位元就是 1

對(duì)于 來(lái)說(shuō)單位元就是 true

對(duì)于 來(lái)說(shuō)單位元就是 false

對(duì)于 來(lái)說(shuō)單位元就是 Infinity

對(duì)于 來(lái)說(shuō)單位元就是 -Infinity

那么 是幺半群么?

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

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

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

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

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

// 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,而不是報(bào)錯(cuò)!

// 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,而不是報(bào)錯(cuò)!

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

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

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

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

4.6.foldMap 1.套路

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

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í)例

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

2.List、Map

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

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

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

因?yàn)?immutable 庫(kù)中的 ListMap 并沒(méi)有 empty 屬性和 fold 方法,所以我們首先擴(kuò)展 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

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

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 這兩步操作,從邏輯上來(lái)說(shuō)是一個(gè)操作,所以我們可以新增 foldMap 方法來(lái)結(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 是因?yàn)榭紤]到空的情況
        .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

所以最終版長(zhǎng)這樣:

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

下面我們要來(lái)實(shí)現(xiàn)一個(gè)新容器 LazyBox。

顧名思義,這個(gè)容器很懶...

雖然你可以不停地用 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))
  // 沒(méi)有 fold 死活不執(zhí)行

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

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

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

與眾不同的是它有個(gè) fork 方法(類(lèi)似于 LazyBox 中的 fold 方法,在 fork 執(zhí)行前其他函數(shù)并不會(huì)執(zhí)行),以及一個(gè) rejected 方法,類(lèi)似于 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

// 類(lèi)似 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.使用示例

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

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

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

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

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

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

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

4.異步嵌套示例

以下代碼做了 3 件事:

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

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

將新內(nèi)容寫(xiě)到 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 來(lái)改寫(xiě)一下~

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.問(wèn)題引入

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

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

先來(lái)看個(gè)簡(jiǎn)單例子:

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

說(shuō)到從容器中取值,那肯定第一個(gè)想到 chain 方法,讓我們來(lái)試一下:

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

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

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

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

下面介紹下主角:ap~:

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

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

運(yùn)算規(guī)則

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

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

由于日常編寫(xiě)代碼的時(shí)候直接用 ap 的話模板代碼太多,所以一般通過(guò)使用 Lift 家族系列函數(shù)來(lái)簡(jiǎn)化。

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

// 應(yīng)用運(yùn)算規(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

// 假裝是個(gè) 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.問(wèn)題引入
import fs from "fs"

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

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

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

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

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

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

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

2.實(shí)現(xiàn)

其實(shí)以上代碼有 bug...,因?yàn)閿?shù)組 Array 是沒(méi)有 traverse 方法的。沒(méi)事兒,讓我們來(lái)實(shí)現(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)
  )
}
看著有點(diǎn)兒暈?

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

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

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

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

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

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

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

那么我們用 Left 行不行呢?

答案是不行!

因?yàn)樽匀蛔儞Q不僅是將一個(gè)函子轉(zhuǎn)換成另一個(gè)函子,它還滿足以下規(guī)則:

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

舉例來(lái)說(shuō)就是:

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

res1 === res2 // Right(200)

即先對(duì)函子 a 做改變?cè)賹⑵滢D(zhuǎn)換為函子 b,是等價(jià)于先將函子 a 轉(zhuǎn)換為函子 b 再做改變。

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

2.應(yīng)用場(chǎng)景

1.例1:得到一個(gè)數(shù)組小于等于 100 的最后一個(gè)數(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) 是等價(jià)的。但是后者顯然性能好得多。

再來(lái)看一個(gè)更復(fù)雜一點(diǎn)兒的例子:

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,
  )

第二版的問(wèn)題是多余的嵌套代碼。

// 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,
  )

第三版的問(wèn)題是多余的重復(fù)邏輯。

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

// 這其實(shí)就是自然變換
// 將 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,
  )

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

簡(jiǎn)單來(lái)說(shuō)就是兩種不同類(lèi)型的對(duì)象經(jīng)過(guò)變形,保持結(jié)構(gòu)并且不丟失數(shù)據(jù)。

具體怎么做到的呢?

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

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

這其實(shí)說(shuō)明了這兩個(gè)類(lèi)型都能夠無(wú)損地保存同樣的信息。

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ù)組的各類(lèi)方法
    chars.to(str).slice(0, 3)
  ).concat("...")
)

truncate(str) // hel...
2. 再來(lái)看看最多有一個(gè)參數(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 編程風(fēng)格指南

Hey Underscore, You"re Doing It Wrong!

Functional Concepts with JavaScript: Part I

Professor Frisby Introduces Composable Functional JavaScript

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

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)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/96872.html

相關(guān)文章

  • gitbook: 前端好書(shū)推薦

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

    Ali_ 評(píng)論0 收藏0
  • gitbook: 前端好書(shū)推薦

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

    CocoaChina 評(píng)論0 收藏0
  • gitbook: 前端好書(shū)推薦

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

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

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

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

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

    csRyan 評(píng)論0 收藏0
  • JavaScript函數(shù)語(yǔ)言的特性

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

    BlackHole1 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<