優(yōu)雅代碼指北 -- 巧用 Ramda[轉(zhuǎn)]

不久前我在掘金發(fā)表了文章如何在 JS 代碼中消滅 for 循環(huán)。文中我寫了很多工具函數(shù),那些工具函數(shù)都能達(dá)到設(shè)計(jì)的目的复罐。但是,很重要一點(diǎn)我還沒告訴讀者雄家,其實(shí)我是幾乎不用自己寫的那些工具函數(shù)的?? 不是因?yàn)榕伦约簩戝e(cuò)了市栗,而是有更強(qiáng)大好用的替代方案。我今天就介紹一個(gè)我用的最多的工具函數(shù)庫 Ramda咳短,展示怎樣用 Ramda 寫出既簡潔易讀,又方便擴(kuò)展復(fù)用的代碼蛛淋。由于時(shí)間和精力有限咙好,我就不解釋我用到的每一個(gè) Ramda 函數(shù)的用法了,大家感興趣的話可以去查官方文檔褐荷。

Ramda 有兩個(gè)特性讓它從其它工具庫中脫穎而出:

  1. 所有 Ramda 函數(shù)都已經(jīng)被柯里化勾效。
  2. 所有 Ramda 函數(shù)都把數(shù)據(jù)作為最后一個(gè)參數(shù)傳入。

這兩個(gè)特征讓我們可以很輕松利用 Ramda 寫出 "point free" 風(fēng)格的代碼叛甫。所謂 point 就是指作為參數(shù)傳進(jìn)函數(shù)的數(shù)據(jù)层宫。point free 就是脫離數(shù)據(jù)的代碼風(fēng)格。通過做到 point free其监,我們做到了行為和數(shù)據(jù)的分離萌腿,這利于我們寫出更安全(組合行為時(shí)沒有副作用),更易擴(kuò)展(脫離數(shù)據(jù)的邏輯容易復(fù)用)抖苦,和更易理解(讀高階函數(shù)的組合就像讀普通英文一樣)的代碼毁菱。

一,第一個(gè) point free 例子

如果你寫過 React + Redux 項(xiàng)目锌历,你可能經(jīng)常寫這種代碼:

function mapStateToProps(state) {
  return {
    board: state.board,
    nextToken: state.nextToken
  }
}

我們可以用 Ramda 的 pick 函數(shù)改寫一下:

import { pick } from 'ramda';

function mapStateToProps(state) {
  return pick(['board', 'nextToken'], state)
}

繼續(xù)改寫成 point free 風(fēng)格贮庞,可以寫成這樣:

const mapStateToProps = pick(['board', 'nextToken']);

是不是簡潔了很多?

二究西,函數(shù)組合

問題: 有這樣一個(gè)數(shù)組:

const teams = [
  {name: 'Lions', score: 5},
  {name: 'Tigers', score: 4},
  {name: 'Bears', score: 6},
  {name: 'Monkeys', score: 2},
]
復(fù)制代碼

要求找出分?jǐn)?shù)最高的小組窗慎,并取到名字。

答案:

import { compose, head, sort, descend, prop } from "ramda";

const teams = [
    {name: 'Lions', score: 5},
    {name: 'Tigers', score: 4},
    {name: 'Bears', score: 6},
    {name: 'Monkeys', score: 2},
  ];

const sortByScoreDesc = sort(descend(prop("score")));
const getName = prop("name");
const findTheBestTeam = compose(
  getName,
  head,
  sortByScoreDesc
);

findTheBestTeam(teams) // => Bears
復(fù)制代碼

稍微感受一下卤材。數(shù)據(jù)是在最后一步才提供的遮斥,提供目標(biāo)數(shù)據(jù)之前一直在組合行為,沒有改變數(shù)據(jù)商膊,沒有任何副作用伏伐。注意 compose 是從后往前組合函數(shù),如果習(xí)慣從前往后組合函數(shù)晕拆,用 pipe岛杀。

再來一個(gè):

問題: 把下面這個(gè)查詢字符串轉(zhuǎn)成對象:

const queryString = "?page=2&pageSize=10&total=203";
復(fù)制代碼

答案:

import { compose, fromPairs, map, split, tail } from "ramda";

const queryString = "?page=2&pageSize=10&total=203";

const parseQs = compose(
  fromPairs,
  map(split("=")),
  split("&"),
  tail
);

const result = parseQs(queryString); // => { page: '2', pageSize: '10', total: '203' }
復(fù)制代碼

你可能會問,JS 原生都提供 map 方法了尽爆,為什么還有用 Ramda 的兼贸? 原因是文章開頭提到的,Ramda 函數(shù)有兩個(gè)特厲害的屬性谨胞。這個(gè)例子里,給 map 傳一個(gè)回調(diào)函數(shù),它會返回一個(gè)新函數(shù)闸溃,等你給它傳數(shù)據(jù)。

三拱撵,一個(gè)數(shù)據(jù)用兩次辉川,怎么 point free?

有時(shí)候會遇到這種情景拴测,根據(jù)一個(gè)數(shù)據(jù)算出結(jié)果乓旗,再根據(jù)相同數(shù)據(jù)算出另一個(gè)結(jié)果,然后把兩個(gè)結(jié)果進(jìn)行某種運(yùn)算集索。比如這個(gè)簡單例子:

問題: 給定一個(gè)用戶對象屿愚,根據(jù)用戶 id 生成頭像地址,并把地址合并到用戶對象上务荆。

// 合并前
const user = {
  id: 1,
  name: 'Joe'
}

// 合并后

{
    id: 1,
    name: 'Joe',
    avatar: 'https://img.socialnetwork.com/avatar/1.png'
}

答案一:

const generateUrl = id => `https://img.socialnetwork.com/avatar/${id || 'default'}.png`;
const getUpdatedUser = user => ({ ...user, avatar: generateUrl(user.id) });

getUpdatedUser(user);

這個(gè)方案已經(jīng)足夠簡潔妆距,但是并沒有達(dá)到 point free 的要求。數(shù)據(jù) user 提前出現(xiàn)了函匕,而我們期待的是在函數(shù)組合時(shí)不關(guān)心數(shù)據(jù)娱据,哪怕是作為參數(shù)。但是盅惜,數(shù)據(jù)在計(jì)算過程中需要多次用到吸耿,怎樣在沒有數(shù)據(jù)(連代表數(shù)據(jù)的參數(shù)都沒有)的情況下表達(dá)對未來數(shù)據(jù)的多次操作?Ramda 提供的 converge 函數(shù)可以解決這個(gè)問題:

答案二:

import { compose, converge, propOr, assoc, identity } from "ramda";
const user = {
  id: 1,
  name: "Joe"
};
const generateUrl = id => `https://img.socialnetwork.com/avatar/${id}.png`;

const getUrlFromUser = compose(
  generateUrl,
  propOr("default", "id")
);
const getUpdatedUser = converge(assoc("avatar"), [
  getUrlFromUser,
  identity
]);

getUpdatedUser(user)

converge 函數(shù)接受兩個(gè)參數(shù)酷窥,第一個(gè)參數(shù)是最終執(zhí)行的函數(shù)咽安,第二個(gè)參數(shù)是由作用于傳入數(shù)據(jù)的變形函數(shù)組成的數(shù)組。在這個(gè)例子里蓬推,user 數(shù)組先分別傳給 identitygetUrlFromUser 函數(shù)妆棒,然后把這兩個(gè)函數(shù)的計(jì)算結(jié)果分別傳給 assoc("avatar")identity 可能是最無聊的函數(shù)沸伏,它長這樣:

const identity = x => x;

我們要保留 user 數(shù)據(jù)不動糕珊,然后傳給 assoc("avatar") 作為第二個(gè)參數(shù),所以用了 identity毅糟。

四红选,方法和數(shù)據(jù)耦合在一起,怎么 point free 姆另?

有些時(shí)候方法就在數(shù)據(jù)上喇肋。比如用 jQuery 選中 DOM 元素后坟乾,對 DOM 元素進(jìn)行操作的方法。假設(shè) DOM 上有個(gè) <div id = "el1"></div>蝶防,用 jQuery 選中元素后甚侣,執(zhí)行某個(gè)動畫效果:

 $('#el1')
   .animate({left:'250px'})
   .animate({left:'10px'})
   .slideUp()

jQuery 的方法全在選中 DOM 元素后生成的對象上,方法是沒法離開數(shù)據(jù)的间学。但這并不影響我們在數(shù)據(jù)還沒給到之前組合行為殷费。Ramda 提供了 invoker 函數(shù)解決類似問題:

import { invoker, compose, constructN } from "ramda";

const animate = invoker(1, "animate");
const slide = invoker(0, "slideUp");
const jq = constructN(1, $);

const animateDiv = compose(
  slide,
  animate({ left: "10px" }),
  animate({ left: "250px" }),
  jq
);

animateDiv("#el1");
animateDiv("#el2");

invoker 函數(shù)接受3個(gè)參數(shù)。第一個(gè)參數(shù)表示要在對象上執(zhí)行的函數(shù)接受多少個(gè)參數(shù)低葫,第二個(gè)參數(shù)表示要在對象上執(zhí)行的函數(shù)的名字详羡,第三個(gè)參數(shù)是目標(biāo)對象。constructN 是用來實(shí)例化一個(gè)構(gòu)造函數(shù)或者類嘿悬。

五殷绍,強(qiáng)大的 lens

lens 是從函數(shù)式編程語言借來的一個(gè)概念,它相當(dāng)于對某個(gè)屬性的聚焦鹊漠。比如 lensProp('a'),就是對 a 屬性的聚焦茶行,不管這個(gè) a 屬性由哪個(gè)對象提供躯概。聚焦之后,我們可以很方便的讀取屬性(view)和改變屬性(over)注意畔师,Ramda 中所有改變值的操作都不是真的在原數(shù)據(jù)基礎(chǔ)上改娶靡,而是返回改了指定屬性的新值。

舉個(gè)很簡單的例子看锉。

import {lensProp, view, over, toUpper} from 'ramda';

const person = {
  firstName: 'Fred',
  lastName: 'Flintstone'
}

const fLens = lensProp('firstName')

const firstName = view(fLens, person) // => 'Fred'

const result = over(fLens, toUpper, person)
// => {firstName: 'FRED', lastName: 'Flintstone'}

上面例子還不能看出 lens 有什么用姿锭。來看下實(shí)際使用場景:

問題:

用 React 寫一個(gè)簡單 counter demo,點(diǎn)擊 + 和 — 按鈕時(shí)伯铣,計(jì)數(shù)器對應(yīng)加 1 和減 1呻此。

lens 用在 React 的 setState 里非常方便:

import {inc, dec, lensProp, over} from 'ramda'

const countL = lensProp('count')
const transformCount = over(countL)
const incCount = transformCount(inc)
const decCount = transformCount(dec)

// ... 其它細(xì)節(jié)

state = {
  count: 0
}

increase = () => {
  this.setState(incCount);
};

decrease = () => {
  this.setState(decCount);
};

// ... 其它細(xì)節(jié)

lens 與 React 的配合,最能發(fā)揮作用的情景是在寫函數(shù)式組件的時(shí)候腔寡。有興趣可以參考這個(gè) Demo

六焚鲜,更高階的函數(shù)組合

前面提到的內(nèi)容都是常規(guī)的函數(shù)組合,Ramda 還提供了 monad 的組合放前。抱歉要扔術(shù)語了忿磅,如果有時(shí)間,未來我可能會解釋什么是 monad凭语。大家常用的 Promise 就是個(gè) monad葱她,通過運(yùn)行 Promise 得到值之后,你并不能在 Promise 外面操作值似扔,而是必須在 then 方法里面處理吨些。這就是 monad 的最大特征(它里層會返回同樣的 Type搓谆,一層層嵌套,必須通過某種 flatMap 機(jī)制將里層的值取出)锤灿。

首先挽拔,最常見的是組合 Promise。來看例子但校。

假設(shè)我們先根據(jù)用戶 email 地址請求得到用戶信息螃诅,然后再根據(jù)用戶 ID 得到用戶粉絲數(shù)。

// 獲取用戶信息的異步函數(shù)
const ajaxForUserInfo = userEmail => fetch(/* post request */); 

// 獲取用戶粉絲的異步函數(shù)
const ajaxForUserFollowers = id => fetch(/* post request*/);

const fetchUserFollowers = async userEmail => {
  const userInfo = await ajaxForUserInfo(userEmail);
  const userFollowers = await ajaxForUserFollowers(userInfo.id);
  return userFollowers;
};

用 Ramda 提供的 composeP状囱,可以組合上面兩個(gè) Promise:

import {composeP} from 'ramda';

const ajaxForUserFollowers = composeP(
  ajaxForUserFollowers,
  ajaxForUserInfo
);

const fetchUserFollowers = async email => {
  const userFollowers = await ajaxUserFollowers(email);
  return userFollowers;
};

上面例子只有兩個(gè) Promise 需要組合术裸。如果是多個(gè)的話,組合的優(yōu)勢更明顯亭枷。

高能預(yù)警:

上面的內(nèi)容已經(jīng)足以覆蓋大部分函數(shù)組合的需求袭艺。接下來要講的一種函數(shù)組合算是比較硬核的函數(shù)式編程了,感興趣的可以接著看叨粘,沒興趣的可以跳過了猾编。

除了對 Promise 的組合,Ramda 還提供更泛的 monad 組合升敲,叫 Kleisli Composition答倡。暫時(shí)不用知道這玩意是什么,知道它是對各種 monad 的組合就行了驴党。我們以 Maybe Monad 為例來看 Kleisli Composition:

可能讀者在看到函數(shù)組合時(shí)會有疑問瘪撇,如果某個(gè)函數(shù)有可能返回空值,還怎么組合港庄?在每個(gè)后續(xù)函數(shù)前都做空值判斷倔既?那就真不優(yōu)雅了。函數(shù)式編程提供了 Maybe Monad 進(jìn)行空值處理鹏氧,Maybe 可以和其它 monad 正常組合渤涌。

來看例子:

假設(shè)我們有這樣一個(gè) JSON 字符串需要解析

'{"user": {"address": {"state": "in"}}}'

我們需要取到用戶的任何一個(gè)深度的地址,而每一層獲取地址都可能失敗把还。所以歼捏,每一次取值,我們都要做空值處理笨篷。

// 解析 JSON 的函數(shù)瞳秽,做了錯(cuò)誤處理
function parse(s) {
  try {
    return JSON.parse(s);
  } catch (e) {
    return null;
  }
}
import Maybe from "folktale/maybe";
import { compose, composeK, toUpper } from "ramda";

// 解析 JSON 可能返回 null,所以把結(jié)果放到 Maybe 里面
const maybeParseJson = json => Maybe.fromNullable(parse(json));

// 獲取屬性也可能返回空值率翅,把結(jié)果放到 Maybe 里面
const maybeProp = prop => obj => Maybe.fromNullable(obj[prop]);

// 傳給 toUpper 的值可能不是字符串练俐,也要把結(jié)果放到 Maybe
const maybeUpper = compose(
  Maybe.of,
  toUpper
);

// composeK 代表 Kleisli composition
const getStateCode = composeK(
  maybeUpper,
  maybeProp("city"),
  maybeProp("state"),
  maybeProp("address"),
  maybeProp("user"),
  maybeParseJson
);

const s = '{"user": {"address": {"state": "in"}}}';

getStateCode(s).getOrElse("Error ocurred"); 

// city 屬性不存在,程序會返回 'Error ocurred'

此篇文章轉(zhuǎn)自優(yōu)秀的leihuang同學(xué)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冕臭,一起剝皮案震驚了整個(gè)濱河市腺晾,隨后出現(xiàn)的幾起案子燕锥,更是在濱河造成了極大的恐慌,老刑警劉巖悯蝉,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件归形,死亡現(xiàn)場離奇詭異,居然都是意外死亡鼻由,警方通過查閱死者的電腦和手機(jī)暇榴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蕉世,“玉大人蔼紧,你說我怎么就攤上這事『萸幔” “怎么了奸例?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長向楼。 經(jīng)常有香客問我查吊,道長,這世上最難降的妖魔是什么湖蜕? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任逻卖,我火速辦了婚禮,結(jié)果婚禮上重荠,老公的妹妹穿的比我還像新娘。我一直安慰自己虚茶,他們只是感情好戈鲁,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嘹叫,像睡著了一般婆殿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上罩扇,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天婆芦,我揣著相機(jī)與錄音,去河邊找鬼喂饥。 笑死消约,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的员帮。 我是一名探鬼主播或粮,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼捞高!你這毒婦竟也來了氯材?” 一聲冷哼從身側(cè)響起渣锦,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎氢哮,沒想到半個(gè)月后袋毙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡冗尤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年听盖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片生闲。...
    茶點(diǎn)故事閱讀 38,673評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡媳溺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出碍讯,到底是詐尸還是另有隱情悬蔽,我是刑警寧澤,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布捉兴,位于F島的核電站蝎困,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏倍啥。R本人自食惡果不足惜禾乘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望虽缕。 院中可真熱鬧始藕,春花似錦、人聲如沸氮趋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剩胁。三九已至诉植,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間昵观,已是汗流浹背晾腔。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留啊犬,地道東北人灼擂。 一個(gè)月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像觉至,于是被迫代替她去往敵國和親缤至。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容