javascript
【JS】446- 你不知道的 map
本文來自【前端早讀課】,內(nèi)容不錯(cuò),推薦給大家。
前言
今日早讀文章由酷家樂@Gloria投稿分享。
正文從這開始~~
作為前端工程師,你肯定用過Array.prototype.map方法。
如果你聽說過Ramda,它也提供了和Array.prototype.map方法類似的map方法。
但是這個(gè)map背后的東西可以讓你看到另外一個(gè)世界,我相信,如果你不想了解Ramda,也能從這篇文章中有所收獲。
下面我們進(jìn)入到例子。
簡(jiǎn)單的使用
像下面這樣使用這個(gè)函數(shù)。
R.map(x => x + 1, [1, 2, 3]); // [2, 3, 4]除了數(shù)組外它還可以作用于Object:
R.map(x => x + 1, {a: 1, b: 2, c: 3}); // {a: 2, b: 3, c: 4}你以為就完了嗎?它還能作用于函數(shù):
R.map(x => x + 1, a => a + 1); // a => (a+1)+1哇,作用于函數(shù)真的是沒想到,那還能作用于其它奇奇怪怪的東西嗎?
當(dāng)然可以,有很多東西從某種維度上講都是同一類東西,關(guān)鍵R.map的維度是什么呢?
先別講什么亂七八糟的,接下來咱們來看一看官方文檔上都有哪些描述.
文檔上都說了啥
接收一個(gè)函數(shù)和一個(gè) functor, 將該函數(shù)應(yīng)用到 functor 的每個(gè)值上,返回一個(gè)具有相同形態(tài)的 functor。
Ramda 為 Array 和 Object 提供了合適的 map 實(shí)現(xiàn),因此 R.map 適用于 [1, 2, 3] 或 {x: 1, y: 2, z: 3}。
若第二個(gè)參數(shù)自身存在 map 方法,則調(diào)用自身的 map 方法。
若在列表位置中給出 transfomer,則用作 transducer 。
函數(shù)也是 functors,map 會(huì)將它們組合起來(相當(dāng)于 R.compose)。
行了,除了2,3能看懂,其它都是啥??!!functor??transfomer??transducer??
我們找到Ramda的源碼,看看這個(gè)map究竟都有哪些魔法?
看看ramda源碼
隱去了一些不需要了解的邏輯,下面是代碼:
var map = _dispatchable(['fantasy-land/map', 'map'], _xmap, function map(fn, functor) { /*ramda默認(rèn)處理邏輯*/ switch(Object.prototype.toString.call(functor)) { case'[object Function]': returnfunction() { return fn.call(this, functor.apply(this, arguments)); }; case'[object Object]': return _reduce(function(acc, key) {acc[key] = fn(functor[key]); return acc; }, {}, keys(functor)); default: return _map(fn, functor); } });先說說_dispatchable的邏輯:
function _dispatchable(methodNames, xf, fn): Function_dispatchable返回的函數(shù)作為R.map的處理過程
接收 3 個(gè)參數(shù):methodNames(方法名數(shù)組),xf(transformer),fn(默認(rèn)的ramda實(shí)現(xiàn))
如果 methodNames 中的方法名存在于傳進(jìn) R.map方法的最后一個(gè)參數(shù)f上,則將該方法作為處理過程 (如 f 是數(shù)組,則使用默認(rèn)的處理過程)
如果最后一個(gè)參數(shù) f 是transformer,處理結(jié)果則是:一個(gè)新的transformer
如果以上3,4說的情況都沒有,則使用Ramda的默認(rèn)處理過程(第一個(gè)代碼塊注釋處)
總體看下來R.map有3種處理策略(按照優(yōu)先級(jí)從上到下):
最后一個(gè)參數(shù)f上出現(xiàn)在 methodNames 中的方法
根據(jù)最后一個(gè)參數(shù) f 返回新的 transformer
Ramda默認(rèn)處理邏輯
默認(rèn)的處理邏輯就不再展開了,比較容易明白,先說說2,1放在后面講。
transduce
進(jìn)入正題之前,拋開ramda,看一個(gè)簡(jiǎn)單的栗子:
const add = (a, b) => a + b; [1,2,3,4].reduce(add, 0); // 10計(jì)算出一個(gè)數(shù)組中所有數(shù)字的和。
現(xiàn)在如果要對(duì)每個(gè)數(shù)字+1,再求和:
const add = (a, b) => a + b; const plusOne = a => a + 1; [1,2,3,4].map(plusOne).reduce(add, 0); // 14上面的代碼會(huì)遍歷數(shù)組兩次,雖然代碼寫起來省事了,如果數(shù)據(jù)量比較大,這個(gè)做法看起來就有些笨拙了。但是又不能改寫add方法,萬一別的地方也用到了add。
想辦法只遍歷一次:結(jié)合add和plusOne生成一個(gè)新的函數(shù)addNPlusOne:
const addNPlusOne = (acc, value) => add(acc, plusOne(value)); [1,2,3,4].reduce(addNPlusOne, 0); // 14嗯,解決了。但是還不夠通用,將add視為reducer,plusOne視為對(duì)value的預(yù)處理函數(shù)fn,通過結(jié)合fn和reducer生成一個(gè)新的reducer提供給reduce
const makeMapReducer = fn => reducer => (acc, value) => reducer(acc, fn(value)); const addNPlusOne = makeMapReducer(plusOne)(add); [1,2,3,4].reduce(addNPlusOne); // 14transducer
makeMapReducer(plusOne)就是一個(gè)transducer。
在之前的基礎(chǔ)上:如果需要先篩選出小于等于2的數(shù)值,然后再給每一項(xiàng)+1,最后統(tǒng)計(jì)出數(shù)組中所有數(shù)的和。
需要再添加一個(gè)filterTransducer:
const makeFilterReducer = fn => reducer => (acc, value) => fn(value)? reducer(acc, value) : acc; const filterTransducer = makeFilterReducer(a => a <= 2); const addNPluslteTwo = filterTransducer(addNPlusOne); [1,2,3,4].reduce(addNPlusltTwo); // 5好了,也就是說如果你不使用任何第三方庫,這個(gè)生成transducer的函數(shù)需要你自己去實(shí)現(xiàn)。
在Ramda中
在Ramda中你可以這樣實(shí)現(xiàn)上面的栗子:
R.transduce(R.map(a => a+1), (acc, value) => acc + value, 0, [1,2,3,4]); // 14 R.transduce(R.pipe(R.map(a => a+1),R.filter(a => a <= 2), ), (acc, value) => acc+value, 0, [1,2,3,4]); // 5再簡(jiǎn)化一點(diǎn):
R.transduce(R.map(R.inc), R.add, 0, [1,2,3,4]); // 14 R.transduce(R.pipe( R.map(R.inc),R.filter(R.gte(2)), ), R.add, 0, [1,2,3,4]); // 5之前的例子,我們自己實(shí)現(xiàn)了transducer。
而對(duì)于ramda來說,很多作用于數(shù)組的api都會(huì)有默認(rèn)的生成transducer的實(shí)現(xiàn),比如map,filter,find等等api。
好了,好像扯遠(yuǎn)了,我們?cè)倩氐絉.map上,看一看這里的transformer是啥意思。
根據(jù)最后一個(gè)參數(shù)f返回新的transformer
回到開始的話題
當(dāng)你調(diào)用R.transduce的時(shí)候,它會(huì)把第二個(gè)參數(shù)R.add,轉(zhuǎn)化為一個(gè)對(duì)象,這個(gè)對(duì)象上存在方法@@transducer/step,這個(gè)方法返回的是R.add(acc, value)。存在方法@@transducer/step的對(duì)象就叫做transformer。
其實(shí)你可以這樣理解:transformer是一個(gè)函數(shù)的載體,transformer['@@transducer/step']就是這個(gè)函數(shù)。
好了,如果當(dāng)R.map的第二個(gè)參數(shù)是一個(gè)transformer的時(shí)候:
// _xwrap是ramda內(nèi)部函數(shù),用于將函數(shù)轉(zhuǎn)為transformer R.map(R.inc)(_xwrap(R.add)) // 跟下面是等價(jià)的 R.map(R.inc, _xwrap(R.add))R.map(R.inc)其實(shí)就是上面我們說的transducer(transducer還能組合起來,不再展開了,有興趣的同學(xué)可以加群討論)
transducer + transformer = transformer,所以上面兩行代碼返回的結(jié)果依然是一個(gè)transformer,這個(gè)transformer的@@transducer/step方法最終效果是下面這樣:
XMap.prototype['@@transducer/step'] = function(acc, value) { return R.add(acc, R.inc(value)); };這個(gè)transformer代表的就是最終的reducer函數(shù)的容器
R.transduce(R.map(R.inc), R.add, 0, [1,2,3,4]); // 與下面是等價(jià)的 const xf = R.map(R.inc)(_xwrap(R.add)); R.reduce(xf['@@transducer/step'], 0, [1,2,3,4]);總結(jié)一下
為了減少遍歷次數(shù),用transduce替代reduce,把之前reduce過程的前置操作比如map,filter,find等操作在一次遍歷中完成。
為了實(shí)現(xiàn)這個(gè)transduce,以及在其上map,filter,find這種操作的可組合性,引入了transducer+transformer的概念。
這個(gè)transducer的概念最早是在Clojure里出現(xiàn),有興趣的同學(xué)可以看看:https://video.tudou.com/v/XMjMxNTY2MDgzNg==.html?__fr=oldtd
fantasyland/map
最后一個(gè)參數(shù)?f上出現(xiàn)在?methodNames中的方法
根據(jù)最后一個(gè)參數(shù)?f返回新的?transformer
ramda默認(rèn)處理邏輯
既然第2點(diǎn)講完了,開始這篇文章的最后一部分,這一部分與上面講的transducer沒有任何關(guān)系,這一部分也是本文想著重介紹的。
var map = _dispatchable(['fantasy-land/map', 'map'],...)從上面R.map的實(shí)現(xiàn)中可以看到,傳入_dispatchable的methodsName中,第一個(gè)方法名是fantasyland/map。
如果R.map(fn, obj),obj上有fantasyland/map方法,則R.map(fn, obj)等價(jià)于 obj['fantasyland/map'](fn)。
那么methodsName中另一個(gè)map和這個(gè)fantasyland/map有啥區(qū)別?為啥還有這么長(zhǎng)的一個(gè)名字?
fantasyland規(guī)范
其實(shí)fantasyland/map這個(gè)名字是有特殊含義的,fantasyland/map沒有特定的實(shí)現(xiàn),不過,如果你要實(shí)現(xiàn)這么一個(gè)方法,你需要遵循fantasyland規(guī)范。
所謂的fantasyland規(guī)范,其實(shí)就是一個(gè)文檔,這個(gè)文檔里規(guī)定了一些代數(shù)結(jié)構(gòu)在javascript里實(shí)現(xiàn)的約束
Fantasy Land Specificationaka "Algebraic JavaScript Specification"
如果你在大學(xué)有接觸過《離散數(shù)學(xué)》的話,其中的一些概念會(huì)在這個(gè)規(guī)范中有具體的javascript定義,比如:二元關(guān)系(等價(jià)關(guān)系,全序關(guān)系),群,半群。當(dāng)然,除了這3類數(shù)據(jù)結(jié)構(gòu),還有范疇以及在基礎(chǔ)代數(shù)結(jié)構(gòu)上衍生出來的其它結(jié)構(gòu)。
類型簽名
接下去我們會(huì)著重看一下與fantasy-land/map相關(guān)的定義,不過,在此之前有一些簡(jiǎn)單的類型簽名,需要提前了解一下(下面的類型簽名解釋,是個(gè)人翻譯版本,如果你有興趣,可以直接看github上英文原版的解釋):
:: :“a屬于類型b”
e :: t:可以理解成:“e屬于類型t”
true :: Boolean:“ true 屬于 Boolean 類型”
42 :: Integer,Number :“42既屬于 Integer 也屬于 Number 類型”
通過類型構(gòu)造函數(shù)可以構(gòu)造一個(gè)新的類型
類型構(gòu)造函數(shù)接受0個(gè)或多個(gè)參數(shù)
Array 就是一個(gè)類型構(gòu)造函數(shù),它接受一個(gè)類型作為參數(shù)
Array String 是存放著字符串的數(shù)組,像這幾個(gè)數(shù)組都是屬于 Array String :[],['foo', 'bar', 'baz']
Array(Array String) 是存放著數(shù)組的數(shù)組,存放的數(shù)組里面又存放著字符串,像這幾個(gè)數(shù)組都是屬于 Array(Array String):[],[[], []],[[], ['foo'], ['bar`, 'baz']]
小寫字母是類型變量
類型變量可以代表任何類型,除非用胖箭頭(下面有介紹)對(duì)它做類型約束
->(箭頭)函數(shù)的類型構(gòu)造函數(shù)
-> 是一個(gè)中綴類型構(gòu)造函數(shù),這個(gè)類型構(gòu)造函數(shù)接受兩個(gè)參數(shù),箭頭左邊的參數(shù)是輸入類型,右邊的參數(shù)是輸出類型
-> 可以接受0個(gè)或多個(gè)輸入類型作為左邊的參數(shù)。語法:() ->,中的多個(gè)類型以“ , ”分隔。一元函數(shù)輸入?yún)?shù)旁邊的括號(hào)可以省略,比如:String -> Boolean,(String, String) -> Boolean
String -> Array String 對(duì)應(yīng)一類函數(shù):接受一個(gè) String 類型的參數(shù),然后返回一個(gè)類型為 Array String 的值
String -> Array String -> Array String 代表著一類函數(shù):接受一個(gè)類型為String的輸入,輸出一個(gè)類型為 Array String -> Array String 的函數(shù),這個(gè)輸出的函數(shù)接受一個(gè)類型為 Array String 的參數(shù),輸出類型為 Array String 的值
(String, Array String) -> Array String代表著一類函數(shù):接受兩個(gè)參數(shù),第一個(gè)是String 類型,第二個(gè)是 Array String 類型,輸出類型為 Array String 的值
() -> Number 代表著一類函數(shù):不接受輸入,返回一個(gè)類型為 Number 的值
~>(波浪箭頭)方法的類型構(gòu)造函數(shù)
當(dāng)一個(gè)函數(shù)是一個(gè)對(duì)象的屬性時(shí),它被叫做這個(gè)對(duì)象上的“方法”。所有的“方法”都擁有一個(gè)隱含的參數(shù)類型-所在對(duì)象的類型
a ~> a -> a 代表著一類方法:是類型為 a 的對(duì)象上的方法,且這個(gè)方法接受一個(gè)類型為a 的參數(shù),返回一個(gè)類型為 a 的值
=>(胖箭頭)胖箭頭用來對(duì)類型變量做類型約束
比如有這么一個(gè)方法 a ~> a -> a ,在這個(gè)方法的類型簽名中,a 可以代表任何類型。Semigroup a => a ~> a -> a,而這個(gè)類型簽名中就對(duì)類型變量 a 做了類型約束,使得類型 a 必須滿足類型類 Semigroup 。當(dāng)一個(gè)類型滿足一個(gè)類型類的意思是,這個(gè)類型實(shí)現(xiàn)了所有類型類指定的函數(shù)/方法。
就拿這次我們要說的fantasy-land/map舉例:
fantasy-land/mapfantasy-land/map解析
先不管下面這部分
Functoru'fantasy-land/map' is equivalent to u (identity)u'fantasy-land/map') is equivalent to u'fantasy-land/map''fantasy-land/map' (composition)
直接看規(guī)范中對(duì)fantasy-land/map的定義:
fantasy-land/map :: Functor f => f a ~> (a -> b) -> f bFunctor是一個(gè)類型類,f 必須滿足 Functor, f a 代表了以 f 作為類型構(gòu)造函數(shù),類型 a 作為構(gòu)造參數(shù)生成的類型,比如 Array String,代表字符串?dāng)?shù)組,Array 就是 f ,它滿足Functor類型類。
如果一個(gè)對(duì)象,是Functor實(shí)例(具體的值)。那么這個(gè)對(duì)象上需要存在一個(gè)名為 fantasy-land/map 的方法,這個(gè)方法必須接受一個(gè)函數(shù)作為參數(shù):
u['fantasy-land/map'](f) // 舉個(gè)例子 [1,2,3]['fantasy-land/map'](f)f 必須是一個(gè)函數(shù)
如果 f 不是一個(gè)函數(shù),fantasy-land/map 的行為是不確定的
f 可以返回任何類型的值
不應(yīng)該檢測(cè) f 的返回類型
fantasy-land/map 方法,必須返回一個(gè)相同的Functor(比如 [1,2,3]'fantasy-land/map'?必須返回也一個(gè)數(shù)組:Array)
其實(shí)可以類比 Array.prototype.map 方法,只是換了個(gè)名字而已。
那么說了這么多,Functor 是個(gè)什么東東?除了 Array 以外,還有什么是 Functor ?
其實(shí) Function 也是 Functor ,驚喜嗎?
不賣關(guān)子了,Functor 的中文名是“函子”,接下來講講“函子”。
啥是函子
“函子”是范疇論中的概念,所以,在準(zhǔn)備完全理解“函子”之前,你需要明白啥是“范疇”?
范疇
其實(shí),在生活中,無處不充斥著范疇,只不過范疇論把這些東西抽象成了數(shù)學(xué)結(jié)構(gòu)。
范疇此一概念代表著一堆數(shù)學(xué)實(shí)體和存在于這些實(shí)體間的關(guān)系。--維基百科
范疇的定義其實(shí)很簡(jiǎn)單,就是實(shí)體的集合+實(shí)體間的關(guān)系。
那么什么是“實(shí)體”?這取決于你怎么看。
從集合的角度來說,實(shí)體是 a set of values ,首先它得是一個(gè)集合(set),其次,這個(gè)集合是由有好多的值組成(value)。
還是比較抽象,再具體一點(diǎn),比如:一個(gè)類型可被看作為值的集合(a set of values),類型與類型之間的關(guān)系就是函數(shù),所以一堆類型+類型之間的函數(shù),就是范疇。
比如有下面這些函數(shù):
fn1 :: Number-> String const fn1 = (a: number) => `${a}1`; fn2 :: String-> Boolean const fn2 = (a: string) => a === '1'; ...這些函數(shù)都是定義在Number和String上的映射關(guān)系。Number,String和Boolean,以及它們之間的映射關(guān)系,構(gòu)成下面這個(gè)范疇
范疇在范疇論中,圖片中的 NUMBER , STRING 和 BOOLEAN 叫做“對(duì)象”(Object),fn1 和 fn2 叫做“態(tài)射”(Morphism), fn2 * fn1 叫做“態(tài)射復(fù)合”, NUMBER -> NUMBER 叫做單位態(tài)射。
明白什么是范疇之后,接下來說一說我們的主角:函子
函子
先來看看維基上的解釋:
在范疇論中,函子是范疇間的一類映射。函子也可以解釋為小范疇范疇內(nèi)的態(tài)射。--維基百科
范疇和范疇也會(huì)有映射關(guān)系,如果把范疇視作一個(gè)對(duì)象時(shí),函子就是范疇之間的態(tài)射。然后組成了一個(gè)范疇的范疇。
舉個(gè)例子:考慮一個(gè)基礎(chǔ)類型的范疇A,一個(gè)數(shù)組范疇B。
兩個(gè)范疇思考以下幾個(gè)問題:
Number 和 Array?之間的關(guān)系
String 和 Array?之間的關(guān)系
Number 到 String 的態(tài)射與 Array?到 Array?的態(tài)射的關(guān)系
之前介紹過 Array 是類型構(gòu)造函數(shù):
將 Number 傳進(jìn) Array ,構(gòu)造出 Array
將 String 傳進(jìn) Array ,構(gòu)造出 Array
可通過 Array 上的 map 方法會(huì)保持 Number -> String 映射到?Array<Number>->Array<String>
再回顧一下上文對(duì)函子的定義:
在范疇論中,函子是范疇間的一類映射。
上面例子中,范疇A到范疇B的映射其實(shí)就是類型構(gòu)造函數(shù) Array ,所以說, Array 就是函子。
函子這里省去了對(duì)公式上的定義的match,爭(zhēng)取大家對(duì)這個(gè)概念有感性的認(rèn)識(shí),如果想知道函子嚴(yán)謹(jǐn)?shù)亩x,可以看這里
回到fantasy-land/map
了解了函子的感性定義之后,回到嚴(yán)謹(jǐn)?shù)囊?guī)范上來。
之前解析 fantasy-land/map 的時(shí)候,有個(gè)定義一直沒有提及,就是 Functor , fantasy-land/map 在文檔中的位置其實(shí)是Functor的子標(biāo)題,現(xiàn)在再來回顧一下。
Functor 1. u['fantasy-land/map'](a => a) is equivalent to u (identity) 2. u['fantasy-land/map'](x => f(g(x))) is equivalent to u['fantasy-land/map'](g)['fantasy-land/map'](f) (composition)通過對(duì)比函子的公式定義,解析Functor需滿足的條件(F即函子):
保持著單位態(tài)射(id即單位態(tài)射,idX即對(duì)象X上的單位態(tài)射)
保持著態(tài)射的復(fù)合
總結(jié)一下fantasyland規(guī)范中對(duì)函子的定義
如果實(shí)現(xiàn)一個(gè)函子,你需要在函子上實(shí)現(xiàn) fantasy-land/map 方法,這個(gè)方法的類型簽名應(yīng)該是這樣的:
fantasy-land/map :: Functor f => f a ~> (a -> b) -> f b函子實(shí)例調(diào)用方法 fantasy-land/map 時(shí),需同時(shí)保持單位態(tài)射和態(tài)射的復(fù)合。
結(jié)尾
這篇文章不知不覺寫得有些長(zhǎng)了,從Ramda文檔->源碼->transducer->fantasyland規(guī)范->范疇論->函子,算是自己完整的探索過程,希望能夠帶給你一些不一樣的東西。
參考文章
JavaScript玩轉(zhuǎn)Clojure大法之Transducer
Wikipedia 范疇論
Wikipedia 函子
關(guān)于本文作者:@Gloria原文:https://zhuanlan.zhihu.com/p/96059965
▼
原創(chuàng)系列推薦
▼
1. JavaScript 重溫系列(22篇全)
2. ECMAScript 重溫系列(10篇全)
3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
4.?正則 / 框架 / 算法等 重溫系列(16篇全)
5.?Webpack4 入門(上)||?Webpack4 入門(下)
6.?MobX 入門(上)?||??MobX 入門(下)
7.?59篇原創(chuàng)系列匯總
回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
總結(jié)
以上是生活随笔為你收集整理的【JS】446- 你不知道的 map的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 张博涵清华大学_看了清华大学“神仙打架”
- 下一篇: 毅力就是下一番苦功