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

資訊專欄INFORMATION COLUMN

Js-函數(shù)式編程

whinc / 1092人閱讀

摘要:組合組合的功能非常強大,也是函數(shù)式編程的一個核心概念,所謂的對過程進行封裝很大程度上就是依賴于組合。在理解之前,先認識一個東西概念容器容器為函數(shù)式編程里普通的變量對象函數(shù)提供了一層極其強大的外衣,賦予了它們一些很驚艷的特性。

前言

JavaScript是一門多范式語言,即可使用OOP(面向?qū)ο螅部梢允褂肍P(函數(shù)式),由于筆者最近在學習React相關(guān)的技術(shù)棧,想進一步深入了解其思想,所以學習了一些FP相關(guān)的知識點,本文純屬個人的讀書筆記,如果有錯誤,望輕噴且提點。

什么是函數(shù)式編程
函數(shù)式編程(英語:functional programming)或稱函數(shù)程序設(shè)計、泛函編程,是一種編程范式,它將計算機運算視為函數(shù)運算,并且避免使用程序狀態(tài)以及易變對象。即對過程進行抽象,將數(shù)據(jù)以輸入輸出流的方式封裝進過程內(nèi)部,從而也降低系統(tǒng)的耦合度。
為什么Js支持FP

Js支持FP的一個重要原因在于,在JS中,函數(shù)是一等公民。即你可以像對其他數(shù)據(jù)類型一樣對其進行操作,把他們存在數(shù)組里,當作參數(shù)傳遞,賦值給變量...等等。如下:

const func = () => {}

// 存儲
const a = [func]

// 參數(shù) 返回值
const x = (func) => {
    ......
    ......
    return func
}

x(func)

這個特性在編寫語言程序時帶來了極大的便利,下面的知識及例子都建立在此基礎(chǔ)上。

純函數(shù) 概念

純函數(shù)是這樣一種函數(shù),即相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用。
副作用包括但不限于:

打印/log

發(fā)送一個http請求

可變數(shù)據(jù)

DOM查詢

簡單一句話, 即只要是與函數(shù)外部環(huán)境發(fā)生交互的都是副作用。

像Js中, slice就是純函數(shù), 而splice則不是

var xs = [1,2,3,4,5];

// 純的
xs.slice(0,3);
//=> [1,2,3]

xs.slice(0,3);
//=> [1,2,3]

xs.slice(0,3);
//=> [1,2,3]


// 不純的
xs.splice(0,3);
//=> [1,2,3]

xs.splice(0,3);
//=> [4,5]

xs.splice(0,3);
//=> []
例子

在React生態(tài)中,使用純函數(shù)的例子很常見,如React Redner函數(shù),Redux的reducer,Redux-saga的聲明式effects等等。

React Render
在React中,Render返回了一個JSX表達式,只要輸入相同,即可以保證我們拿到同樣的輸出(最終結(jié)果渲染到DOM上),而內(nèi)部的封裝細節(jié)我們不需要關(guān)心,只要知道它是沒有副作用的,這在我們開發(fā)過程中帶來了極大的便利。當我們的程序出問題時(渲染出來與預期不符合),我們只要關(guān)心我們的入?yún)⑹欠裼袉栴}即可。

class Component extends React.Component {
    render() {
        return (
            
) } }

Redux的reducer
Redux的reducer函數(shù)要求我們每一次都要返回一個新的state, 并且在其中不能有任何副作用,只要傳入?yún)?shù)相同,返回計算得到的下一個 state 就一定相同。沒有特殊情況、沒有副作用,沒有 API 請求、沒有變量修改,單純執(zhí)行計算。這樣做可以使得我們很容易的保存了每一次state改變的情況,對于時間旅行這種需求更是天然的親近。特別是在調(diào)試的過程中,我們可以借助插件,任意達到每一個state狀態(tài),能夠輕松的捕捉到錯誤是在哪一個節(jié)點出現(xiàn)。

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    default:
      return state
  }
}

Redux-sage的聲明式effects
許多時候, 我們會寫這樣的函數(shù)

const sendRequest = () => {
    return axions.post(...)
}

這是一個不純的函數(shù),因為它包含了副作用,發(fā)起了http請求,我們可以這樣封裝一下:

const sendRequestReducer = () => {
    return () => {
        return axios.post(...)
    }
}

ok, 現(xiàn)在是一個純函數(shù)了,正如Redux-saga中的effects一樣:

import { call } from "redux-saga/effects"

function* fetchProducts() {
  const products = yield call(Api.fetch, "/products")
  // ...
}

實際上call不立即執(zhí)行異步調(diào)用,相反,call 創(chuàng)建了一條描述結(jié)果的信息。那么這樣做除了增加代碼的復雜度,還可以給我們帶來什么?參考saga的官方文檔就知道了, 答案是測試:

這些 聲明式調(diào)用(declarative calls) 的優(yōu)勢是,我們可以通過簡單地遍歷 Generator 并在 yield 后的成功的值上面做一個 deepEqual 測試, 就能測試 Saga 中所有的邏輯。這是一個真正的好處,因為復雜的異步操作都不再是黑盒,你可以詳細地測試操作邏輯,不管它有多么復雜。
import { call } from "redux-saga/effects"
import Api from "..."

const iterator = fetchProducts()

// expects a call instruction
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, "/products"),
  "fetchProducts should yield an Effect call(Api.fetch, "./products")"
)
總結(jié)

純函數(shù)有著以下的優(yōu)點

可緩存性
首先,純函數(shù)總能夠根據(jù)輸入來做緩存。實現(xiàn)緩存的一種典型方式是 memoize 技術(shù):

var memoize = function(f) {
  var cache = {};

  return function() {
    var arg_str = JSON.stringify(arguments);
    cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
    return cache[arg_str];
  };
};

var squareNumber  = memoize(function(x){ return x*x; });

squareNumber(4);
//=> 16

squareNumber(4); // 從緩存中讀取輸入值為 4 的結(jié)果
//=> 16

squareNumber(5);
//=> 25

squareNumber(5); // 從緩存中讀取輸入值為 5 的結(jié)果
//=> 25

可移植性
純函數(shù)因為不依賴外部環(huán)境,所以非常便于移植,你可以在任何地方使用它而不需要附帶著引入其他不需要的屬性。

可測試性
如上面提到的Redux reducer和Redux-saga一樣, 它對于測試天然親近。

并行代碼
我們可以并行運行任意純函數(shù)。因為純函數(shù)根本不需要訪問共享的內(nèi)存,而且根據(jù)其定義,純函數(shù)也不會因副作用而進入競爭態(tài)(race condition)。

柯里化 概念
在計算機科學中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。
var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12
例子

在Lodash類庫中,就有這么一個curry函數(shù)來幫助我們處理科里化,關(guān)于如何實現(xiàn)一個curry函數(shù),推薦大家參考這篇文章

var abc = function(a, b, c) {
  return [a, b, c];
};
 
var curried = _.curry(abc);
 
curried(1)(2)(3);
// => [1, 2, 3]
 
curried(1, 2)(3);
// => [1, 2, 3]
 
curried(1, 2, 3);
// => [1, 2, 3]
 
// Curried with placeholders.
curried(1)(_, 3)(2);
// => [1, 2, 3]
偏函數(shù)應用

偏函數(shù)本身與科里化并不相關(guān), 但在日常的編寫程序中,或許我們使用更多的是偏函數(shù),所以在這里簡單的介紹一下偏函數(shù)

偏函數(shù)應用是找一個函數(shù),固定其中的幾個參數(shù)值,從而得到一個新的函數(shù)。

有時候,我們會寫一個專門發(fā)送http請求的函數(shù)

const sendRequest = (host, fixPath, path) => {
    axios.post(`${host}${fixPath}{path}`)
}

但是大多數(shù)時候, host和fixPath是固定的, 我們不想每次都寫一次host和fixPath,但我們又不能寫死,因為我們需要sendRequest這個函數(shù)是可以移植的,不受環(huán)境的約束,那么我們可以這樣

const sendRequestPart = (path) => {
    const host = "..."
    const fixPath = "..."
    return sendRequest(host, fixPath, path)
}
總結(jié)

科里化和偏函數(shù)的主要用途是在組合中,這一小節(jié)主要介紹了他們的使用方法和行為。

組合 compose

組合的功能非常強大, 也是函數(shù)式編程的一個核心概念, 所謂的對過程進行封裝很大程度上就是依賴于組合。那么什么是組合?

var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + "!"; };
var shout = compose(exclaim, toUpperCase);

shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"

上面的compose就是一個最簡單的組合函數(shù), 當然組合函數(shù)并不限制于傳入多少個函數(shù)參數(shù),它最后只返回一個函數(shù),我個人更喜歡將它認為像管道一樣,將數(shù)據(jù)經(jīng)過不同函數(shù)的逐漸加工,最后得到我們想要的結(jié)果

const testFunc = compose(func1, func2, func3, func4)  
testFunc(...args) 

在js中, 實現(xiàn)compose函數(shù)比較容易

const compose = (...fns) => {
    return (...args) => {
        let res = args
        for (let i = fns.length - 1; i > -1; i--) {
            res = fns[i](res)
        }
        return res
    }
}
例子

React官方推崇組合優(yōu)于繼承這個概念,這里選擇兩個比較典型的例子來看

React中的高階組件
在React中,有許多使用高階組件的地方,如React-router的withRouter函數(shù),React-redux的connect函數(shù)返回的函數(shù),

// Navbar 和 Comment都是組件
const NavbarWithRouter = withRouter(Navbar);
const ConnectedComment = connect(commentSelector, commentActions)(Comment);

而由于高階函數(shù)的簽名是Component => Component。所以我們可以很容易的將他們組合到一起,這也是官方推薦的做法

// 不要這樣做……
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// ……你可以使用一個函數(shù)組合工具
// compose(f, g, h) 和 (...args) => f(g(h(...args)))是一樣的
const enhance = compose(
  // 這些都是多帶帶一個參數(shù)的高階組件
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)

Redux的compose函數(shù)
Redux的compose函數(shù)實現(xiàn)要比上面提到的簡潔的多

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

這個實現(xiàn)咋看之下有點懵逼, 所以可以拆開來看一下

composeFn = compose(fn1, fn2, fn3, fn4)

那么reduce循環(huán)運行時, 第一次a就是fn1, b是fn2, 第二次a是(...args) => fn1(fn2(...args)), b是fn3, 第三次運行的時候則是a是(...args) => fn1(fn2(fn3(...args))), b是fn4, 最后返回了fn1(fn2(fn3(fn4(...args))))

pointfree
它的意思是說,函數(shù)無須提及將要操作的數(shù)據(jù)是什么樣的。
// 非 pointfree,因為提到了數(shù)據(jù):word
var snakeCase = function (word) {
  return word.toLowerCase().replace(/s+/ig, "_");
};

// pointfree
var snakeCase = compose(replace(/s+/ig, "_"), toLowerCase);
pointfree 模式能夠幫助我們減少不必要的命名,讓代碼保持簡潔和通用。對函數(shù)式代碼來說,pointfree 是非常好的石蕊試驗,因為它能告訴我們一個函數(shù)是否是接受輸入返回輸出的小函數(shù)。比如,while 循環(huán)是不能組合的。不過你也要警惕,pointfree 就像是一把雙刃劍,有時候也能混淆視聽。并非所有的函數(shù)式代碼都是 pointfree 的,不過這沒關(guān)系??梢允褂盟臅r候就使用,不能使用的時候就用普通函數(shù)。
總結(jié)

有了組合, 配合上面提到的科里化和偏函數(shù)應用, 可以將程序拆成一個個小函數(shù)然后組合起來, 優(yōu)點已經(jīng)很明顯的呈現(xiàn)出來,也很直觀的表達出了函數(shù)式編程的封裝過程的核心概念。

范疇學

函數(shù)式編程建立在范疇學上,很多時候討論起來難免有點理論化,所以這里簡單的介紹一下范疇。

有著以下這些組件(component)的搜集(collection)就構(gòu)成了一個范疇:

對象的搜集

態(tài)射的搜集

態(tài)射的組合

identity 這個獨特的態(tài)射

對象的搜集
對象就是數(shù)據(jù)類型,例如 String、Boolean、Number 和 Object 等等。通常我們把數(shù)據(jù)類型視作所有可能的值的一個集合(set)。像 Boolean 就可以看作是 [true, false] 的集合,Number 可以是所有實數(shù)的一個集合。把類型當作集合對待是有好處的,因為我們可以利用集合論(set theory)處理類型。

態(tài)射的搜集
態(tài)射是標準的、普通的純函數(shù)。

態(tài)射的組合
即上面提到的compose

identity 這個獨特的態(tài)射
讓我們介紹一個名為 id 的實用函數(shù)。這個函數(shù)接受隨便什么輸入然后原封不動地返回它:

var id = function(x){ return x; };
functor

在學習函數(shù)式編程的時候,第一次看到functor的時候一臉懵逼, 確實不理解這個東西是什么, 可以做什么,加上一堆術(shù)語,頭都大了。在理解functor之前,先認識一個東西

概念

容器

容器為函數(shù)式編程里普通的變量、對象、函數(shù)提供了一層極其強大的外衣,賦予了它們一些很驚艷的特性。
var Container = function(x) {
  this.__value = x;
}
Container.of = x => new Container(x);

//試試看
Container.of(1);
//=> Container(1)

Container.of("abcd");
//=> Container("abcd")

Container.of 把東西裝進容器里之后,由于這一層外殼的阻擋,普通的函數(shù)就對他們不再起作用了,所以我們需要加一個接口來讓外部的函數(shù)也能作用到容器里面的值(像Array也是一個容器):

Container.prototype.fmap = function(f){
  return Container.of(f(this.__value))
}

我們可以這樣使用它:

Container.of(3)
    .fmap(x => x + 1)                //=> Container(4)
    .fmap(x => "Result is " + x);    //=> Container("Result is 4")

我們通過簡單的代碼就實現(xiàn)了一個鏈式調(diào)用,并且這也是一個functor。

Functor(函子)是實現(xiàn)了 fmap 并遵守一些特定規(guī)則的容器類型。

這樣子看還是有點不好理解, 那么參考下面這句話可能會好一點:

a functor is nothing more than a data structure you can map functions over with the purpose of lifting values from a container, modifying them, and then putting them back into a container.   都是些簡單的單詞,意會比起本人翻譯會更容易理解。

加上一張圖:

ok, 現(xiàn)在大概知道functor是一個什么樣的東西了。

作用

那么functor有什么作用呢?

鏈式調(diào)用
首先它可以鏈式調(diào)用,正如上面提到的一樣。

Immutable
可以看到, 我們每次都是返回了一個新的Container.of, 所以數(shù)據(jù)是Immutable的, 而Immutable的作用就不在這里贅述了。

將控制權(quán)交給Container
將控制權(quán)交給Container, 這樣他就可以決定何時何地怎么去調(diào)用我們傳給fmap的function,這個作用非常強大,可以為我們做空值判斷、異步處理、惰性求值等一系列麻煩的事。

例子

上面作用的第三點可能直觀上有點難以理解, 下面舉三個簡單的例子

Maybe Container
定義一個Maybe Container來幫我們處理空值的判斷

var Maybe = function(x) {
  this.__value = x;
}

Maybe.of = function(x) {
  return new Maybe(x);
}

Maybe.prototype.fmap = function(f) {
  return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}

Maybe.prototype.isNothing = function() {
  return (this.__value === null || this.__value === undefined);
}

//試試看
import _ from "lodash";
var add = _.curry(_.add);

Maybe.of({name: "Stark"})
    .fmap(_.prop("age"))
    .fmap(add(10));
//=> Maybe(null)

Maybe.of({name: "Stark", age: 21})
    .fmap(_.prop("age"))
    .fmap(add(10));
//=> Maybe(31)

當然, 這里可以利用上面提到的科里化函數(shù)來簡化掉一堆fmap的情況

import _ from "lodash";
var compose = _.flowRight;
var add = _.curry(_.add);

// 創(chuàng)造一個柯里化的 map
var map = _.curry((f, functor) => functor.fmap(f));

var doEverything = map(compose(add(10), _.property("age")));

var functor = Maybe.of({name: "Stark", age: 21});
doEverything(functor);
//=> Maybe(31)

Task Container
我們可以編寫一個Task Container來幫我們處理異步的情況

var fs = require("fs");

//  readFile :: String -> Task(Error, JSON)
var readFile = function(filename) {
  return new Task(function(reject, result) {
    fs.readFile(filename, "utf-8", function(err, data) {
      err ? reject(err) : result(data);
    });
  });
};

readFile("metamorphosis").fmap(split("
")).fmap(head);

例子中的 reject 和 result 函數(shù)分別是失敗和成功的回調(diào)。正如你看到的,我們只是簡單地調(diào)用 Task 的 map 函數(shù),就能操作將來的值,好像這個值就在那兒似的。(這看起來有點像Promise)

Io Container
我們可以利用Io Container來做惰性求值

import _ from "lodash";
var compose = _.flowRight;

var IO = function(f) {
    this.__value = f;
}

IO.of = x => new IO(_ => x);

IO.prototype.map = function(f) {
    return new IO(compose(f, this.__value))
};

var io_document = new IO(_ => window.document);

io_document.map(function(doc){ return doc.title });
//=> IO(document.title)

注意我們這里雖然感覺上返回了一個實際的值 IO(document.title),但事實上只是一個對象:{ __value: [Function] },它并沒有執(zhí)行,而是簡單地把我們想要的操作存了起來,只有當我們在真的需要這個值得時候,IO 才會真的開始求值,

functor 范疇

functor 的概念來自于范疇學,并滿足一些定律。 即functor 接受一個范疇的對象和態(tài)射(morphism),然后把它們映射(map)到另一個范疇里去。

Js中的functor

Js中也有一些實現(xiàn)了functor, 如map、filter

map    :: (A -> B)   -> Array(A) -> Array(B)
filter :: (A -> Boolean) -> Array(A) -> Array(A)
Monad 普通functor的問題

我們來寫一個函數(shù) cat,這個函數(shù)的作用和 Linux 命令行下的 cat 一樣,讀取一個文件,然后打出這個文件的內(nèi)容

import fs from "fs";
import _ from "lodash";

var map = _.curry((f, x) => x.map(f));
var compose = _.flowRight;

var readFile = function(filename) {
    return new IO(_ => fs.readFileSync(filename, "utf-8"));
};

var print = function(x) {
    return new IO(_ => {
        console.log(x);
        return x;
    });
}

var cat = compose(map(print), readFile);

cat("file")
//=> IO(IO("file的內(nèi)容"))

ok, 我們最后得到的是兩層嵌套的IO, 要獲取其中的值

cat("file").__value().__value()

問題很明顯的出來了, 我們需要連續(xù)調(diào)用兩次_value才能獲取, 那么假如我們嵌套了更多呢, 難道每次都要調(diào)用一大堆__value嗎, 那當然是不可能的。

概念

我們可以使用一個join函數(shù), 來將Container里面的東西拿出來, 像這樣

var join = x => x.join();
IO.prototype.join = function() {
  return this.__value ? IO.of(null) : this.__value();
}

// 試試看
var foo = IO.of(IO.of("123"));

foo.join();

似乎這樣也有點麻煩, 每次都要使用一個join來剖析

var doSomething = compose(join, map(f), join, map(g), join, map(h));

我們可以使用一個chain函數(shù), 來幫助我們做這些事

var chain = _.curry((f, functor) => functor.chain(f));
IO.prototype.chain = function(f) {
  return this.map(f).join();
}

// 現(xiàn)在可以這樣調(diào)用了
var doSomething = compose(chain(f), chain(g), chain(h));

// 當然,也可以這樣
someMonad.chain(f).chain(g).chain(h)

// 寫成這樣是不是很熟悉呢?
readFile("file")
    .chain(x => new IO(_ => {
        console.log(x);
        return x;
    }))
    .chain(x => new IO(_ => {
        // 對x做一些事情,然后返回
    }))

ok, 事實上這就是一個Monad, 而且你也會很熟悉, 這就像一個Promise的then, 那么什么是Monad呢?
Monad有一個bind方法, 就是上面講到的chain(同一個東西不同叫法),

function bind(instance: M, transform: (value: T) => M): M {
    // ...
}

其實,Monad 的作用跟 Functor 類似,也是應用一個函數(shù)到一個上下文中的值。不同之處在于,F(xiàn)unctor 應用的是一個接收一個普通值并且返回一個普通值的函數(shù),而 Monad 應用的是一個接收一個普通值但是返回一個在上下文中的值的函數(shù)。上下文即一個Container。

Promise是Monad

需要被認為是Monad需要具備以下三個條件

擁有容器, 即Maybe、IO之類。

一個可以將普通類型轉(zhuǎn)換為具有上下文的值的函數(shù), 即Contanier.of

擁有bind函數(shù)(即上面提到的bind, 而不是ES5的bind)

那么Promise具備了什么條件?

擁有容器 Promise, 即上面第一點

Promise.resolve(value)將值轉(zhuǎn)換為一個具有上下文的值, 即上面第二點。

Promise.prototype.then(onFullfill: value => Promise) 擁有一個bind(then)函數(shù), 接受一個函數(shù)作為參數(shù), 該函數(shù)接受一個普通值并返回一個含有上下文的值。 即上面第三點

不過Promise比Monad擁有更多的功能。

如果then返回了一個正常的value, Promise會調(diào)用Promise.resolve將其轉(zhuǎn)換為Promise

普通的Monad只能提供在計算的時候傳遞一個值, 而Promise有兩個不同的值 - 一個用于成功值,一個用于錯誤(類似于Either monad)??梢允褂胻hen方法的第二個回調(diào)或使用特殊的.catch方法捕獲錯誤

Applicative Functor

提到了Functor和Monad而不提Applicative Functor就不完整了。

概念

Applicative Functor就是讓不同 functor 可以相互應用(apply)的能力。
舉一個簡單的例子, 假設(shè)有兩個同類型的 functor,我們想把這兩者作為一個函數(shù)的兩個參數(shù)傳遞過去來調(diào)用這個函數(shù)。

// 這樣是行不通的,因為 2 和 3 都藏在瓶子里。
add(Container.of(2), Container.of(3));
//NaN

// 使用可靠的 map 函數(shù)試試
var container_of_add_2 = map(add, Container.of(2));
// Container(add(2))

這時候我們創(chuàng)建了一個 Container,它內(nèi)部的值是一個局部調(diào)用的(partially applied)的函數(shù)。確切點講就是,我們想讓 Container(add(2)) 中的 add(2) 應用到 Container(3) 中的 3 上來完成調(diào)用。也就是說,我們想把一個 functor 應用到另一個上。
巧的是,完成這種任務的工具已經(jīng)存在了,即 chain 函數(shù)。我們可以先 chain 然后再 map 那個局部調(diào)用的 add(2),就像這樣:

Container.of(2).chain(function(two) {
  return Container.of(3).map(add(two));
});

然而這樣我們需要延遲Container.of(3)的建立, 這對我們來說是很不方便的也是沒有必要的, 我們可以通過建立一個ap函數(shù)來達成我們想要的效果

Container.prototype.ap = function(other_container) {
  return other_container.map(this.__value);
}

Container.of(2).map(add).ap(Container.of(3));
// Container(5)

注意上面的add是科里化函數(shù), this.__value是一個純函數(shù)。

由于這種先 map 再 ap 的操作很普遍,我們可以抽象出一個工具函數(shù) liftA2:

const liftA2 = (f, m1, m2) => m1.map(f).ap(m2)
liftA2(add, Container.of(2), Container.of(3))
應用

正如我們上面所說, 我們可以獨立創(chuàng)建兩個Container, 那么在Task中也可以同時發(fā)起兩個http請求,而不必等到第一個返回再執(zhí)行第二個

// Http.get :: String -> Task Error HTML

var renderPage = curry(function(destinations, events) { /* render page */  });

Task.of(renderPage).ap(Http.get("/destinations")).ap(Http.get("/events"))
// Task("
some page with dest and events
")
FunctorMonadApplicative Functor的數(shù)學規(guī)律

Functor

// identity
map(id) === id;

// composition
compose(map(f), map(g)) === map(compose(f, g));

Monad

bind(unit(x), f) ≡ f(x)
bind(m, unit) ≡ m
bind(bind(m, f), g) ≡ bind(m, x ? bind(f(x), g))

Applicative Functor

Identity: A.of(x => x).ap(v) === v
Homomorphism: A.of(f).ap(A.of(x)) === A.of(f(x))
Interchange: u.ap(A.of(y)) === A.of(f => f(y)).ap(u)
js 與 函數(shù)式和面向?qū)ο?/b>

以下引用自文章漫談 JS 函數(shù)式編程(一)

面向?qū)ο髮?shù)據(jù)進行抽象,將行為以對象方法的方式封裝到數(shù)據(jù)實體內(nèi)部,從而降低系統(tǒng)的耦合度。而函數(shù)式編程,選擇對過程進行抽象,將數(shù)據(jù)以輸入輸出流的方式封裝進過程內(nèi)部,從而也降低系統(tǒng)的耦合度。兩者雖是截然不同,然而在系統(tǒng)設(shè)計的目標上可以說是殊途同歸的。

面向?qū)ο笏枷牒秃瘮?shù)式編程思想也是不矛盾的,因為一個龐大的系統(tǒng),可能既要對數(shù)據(jù)進行抽象,又要對過程進行抽象,或者一個局部適合進行數(shù)據(jù)抽象,另一個局部適合進行過程抽象,這都是可能的。數(shù)據(jù)抽象不一定以對象實體為形式,同樣過程抽象也不是說形式上必然是 functional 的,比如流式對象(InputStream、OutputStream)、Express 的 middleware,就帶有明顯的過程抽象的特征。但是在通常情況下,OOP更適合用來做數(shù)據(jù)抽象,F(xiàn)P更適合用來做過程抽象。

當然由于Javascript本身是多范式語言, 所以可以在合適的地方使用合適的編程方式??偠灾?, 兩者互不排斥,是可共存的。

尾遞歸優(yōu)化

由于函數(shù)式編程,如果尾遞歸不做優(yōu)化,很容易爆棧, 這個知識點有很多文章提出來了, 這里推薦一篇文章

聲明式編程

聲明式主要表現(xiàn)在于只關(guān)心結(jié)果而不關(guān)心過程, 這里推薦一篇輕松易懂的文章
或者舉個例子:
在JQ時代的時候, 假如我們需要渲染一個DOM, 并改變其文字顏色, 我們需要這樣的步驟:

找到DOM的class或者id

根據(jù)class或者id找到DOM

重新賦值DOM的style屬性的color屬性

而在React中, 我們可以直接告訴JSX我們想要DOM的顏色變成紅色即可。

const textColor = "red"
const comp = () => {
    return (
        
) }

而關(guān)于聲明式和函數(shù)式, 我個人認為函數(shù)式和聲明式一樣, 也是屬于關(guān)心結(jié)果, 但是函數(shù)式最重要的特點是“函數(shù)第一位”,即函數(shù)可以出現(xiàn)在任何地方。 兩者其實不應該做比較。

函數(shù)式編程在JS中的實踐

Undescore/Lodash/Ramda庫 特別是Lodash, 打開node_modules基本都能看到

Immutable-js 數(shù)據(jù)不可變

React

Redux

ES6 尾遞歸優(yōu)化

函數(shù)式編程在前端開發(fā)中的優(yōu)勢

以下引用自知乎答案

優(yōu)化綁定

說白了前端和后端不一樣的關(guān)鍵點是后端HTTP較多,前端渲染多,前端真正的剛需是數(shù)據(jù)綁定機制。后端一次對話,計算好Response發(fā)回就完成任務了,所以后端吃了二十年年MVC老本還是挺好用的。前端處理的是連續(xù)的時間軸,并非一次對話,像后端那樣賦值簡單傳遞就容易斷檔,導致狀態(tài)不一致,帶來大量額外復雜度和Bug。不管是標準FRP還是Mobx這種命令式API的TFRP,內(nèi)部都是基于函數(shù)式設(shè)計的。函數(shù)式重新發(fā)明的Return和分號是要比裸命令式好得多的(前端狀態(tài)可以同步,后端線程安全等等,想怎么封裝就怎么封裝)。

封裝作用

接上條,大幅簡化異步,IO,渲染等作用/副作用相關(guān)代碼。和很多人想象的不一樣,函數(shù)式很擅長處理作用,只是多一層抽象,如果應用稍微復雜一點,這點成本很快就能找回來(Redux Saga是個例子,特別是你寫測試的情況下)。渲染現(xiàn)在大家都可以理解冪等渲染地好處了,其實函數(shù)式編程各種作用和狀態(tài)也是冪等的,對于復雜應用非常有幫助。

復用

引用透明,無副作用,代數(shù)設(shè)計讓函數(shù)式代碼可以正確優(yōu)雅地復用。前端不像后端業(yè)務固定,做好業(yè)務分析和DDD就可以搭個靜態(tài)結(jié)構(gòu),高枕無憂了。前端的好代碼一定是活的,每處都可能亂改??山M合性其實很重要。通過高階函數(shù)來組合效果和效率都要高于繼承,試著多用ramda,你就可以發(fā)現(xiàn)絕大部分東西都能一行寫完,最后給個實參就變成一個UI,來需求改兩筆就變成另外一個。
總結(jié)

函數(shù)式編程在JS的未來是大放異彩還是泯然眾人,都不影響我們學習它的思想。本文里面有許多引用沒有特別指出,但都會在底部放上鏈接(如介意請留言), 望見諒。

參考&引用

聲明式編程和命令式編程有什么區(qū)別?
用 JS 代碼完整解釋 Monad
怎么理解“聲明式渲染”?
JavaScript函數(shù)式編程(二)
JavaScript Functors Explained
前端開發(fā)js函數(shù)式編程真實用途體現(xiàn)在哪里?
js 是更傾向于函數(shù)式編程了還是更傾向于面向?qū)ο螅炕蛘邲]有傾向?只是簡單的提供了更多的語法糖?
漫談 JS 函數(shù)式編程(一)
有哪些函數(shù)式編程在前端的實踐經(jīng)驗?
前端使用面向?qū)ο笫骄幊?還是 函數(shù)式編程 針對什么問題用什么方式 分別有什么具體案例?
什么是 Monad (Functional Programming)?
Monads In Javascript
Functor、Applicative 和 Monad
JavaScript 讓 Monad 更簡單
函數(shù)式編程

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

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

相關(guān)文章

  • 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
  • 翻譯連載 |《你不知道的JS》姊妹篇 |《JavaScript 輕量級函數(shù)編程》- 引言&前言

    摘要:我稱之為輕量級函數(shù)式編程。序眾所周知,我是一個函數(shù)式編程迷。函數(shù)式編程有很多種定義。本書是你開啟函數(shù)式編程旅途的絕佳起點。事實上,已經(jīng)有很多從頭到尾正確的方式介紹函數(shù)式編程的書了。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson - 《You-Dont-Know-JS》作者 譯者團隊(排名不分先后):阿希、blueken、brucecham、...

    2bdenny 評論0 收藏0
  • 翻譯連載 | 附錄 C:函數(shù)編程函數(shù)庫-《JavaScript輕量級函數(shù)編程》 |《你不知道的J

    摘要:為了盡可能提升互通性,已經(jīng)成為函數(shù)式編程庫遵循的實際標準。與輕量級函數(shù)式編程的概念相反,它以火力全開的姿態(tài)進軍的函數(shù)式編程世界。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個流淌著滬江血液的純粹工程:認真,是 HTML 最堅實的梁柱;分享,是 CSS 里最閃耀的一瞥;總結(jié),...

    Miracle 評論0 收藏0
  • 翻譯連載 |《你不知道的JS》姊妹篇 |《JavaScript 輕量級函數(shù)編程》- 第 1 章:

    摘要:所以我覺得函數(shù)式編程領(lǐng)域更像學者的領(lǐng)域。函數(shù)式編程的原則是完善的,經(jīng)過了深入的研究和審查,并且可以被驗證。函數(shù)式編程是編寫可讀代碼的最有效工具之一可能還有其他。我知道很多函數(shù)式編程編程者會認為形式主義本身有助于學習。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson?。 禮ou-Dont-Know-JS》作者 關(guān)于譯者:這是一個流淌著滬江血液...

    omgdog 評論0 收藏0
  • 全本 | iKcamp翻譯 | 《JavaScript 輕量級函數(shù)編程》|《你不知道的JS》姊妹篇

    摘要:本書主要探索函數(shù)式編程的核心思想。我們在中應用的僅僅是一套基本的函數(shù)式編程概念的子集。我稱之為輕量級函數(shù)式編程。通常來說,關(guān)于函數(shù)式編程的書籍都熱衷于拓展閱讀者的知識面,并企圖覆蓋更多的知識點。,本書統(tǒng)稱為函數(shù)式編程者。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson - 《You-Dont-Know-JS》作者 譯者團隊(排名不分先后)...

    paney129 評論0 收藏0
  • JS函數(shù)編程讀書筆記 - 1

    摘要:在近期看到了函數(shù)式編程這本書預售的時候就定了下來。主要目的是個人目前還是不理解什么是函數(shù)式編程。且和現(xiàn)在在學習函數(shù)式編程有莫大的關(guān)系。加速大概了解了函數(shù)式編程之后??偨Y(jié)看完了第一章也是可以小結(jié)一下的函數(shù)式編程。 本文章記錄本人在學習 函數(shù)式 中理解到的一些東西,加深記憶和并且整理記錄下來,方便之后的復習。 在近期看到了《JavaScript函數(shù)式編程》這本書預售的時候就定了下...

    G9YH 評論0 收藏0

發(fā)表評論

0條評論

whinc

|高級講師

TA的文章

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