函數式編程了解一下(下)
- 2019 年 10 月 10 日
- 筆記
回顧柯里化、偏應用
函數式編程了解一下(上)
對於上一篇文章,有朋友群里艾特說不是很明白柯里化的函數,這裡我們拿出來簡單說下
let curry = (fn) =>{ if(typeof fn !== 'function'){ throw Error('No Function'); } return function curriedFn(...args){ if(args.length < fn.length){ return function(){ return curriedFn.apply(null,args.concat([].slice.call(arguments))); } } return fn.apply(null,args); } } function add (a,b,c) { return a+b+c } curry(add)(1)(2)(3)
一步一步來理解,第一次調用curry函數的時候,返回一個curried函數,待調用狀態,當我們傳入1的時候,返回的依舊是一個函數,args是利用閉包,記錄你傳入的參數是否為函數定義時候的參數個數,如果不是,那我接着等待你在傳入。因為我們利用args來記錄每次傳入的值,所以我們每次拿curry函數後的傳入的參數就必須使用arguments了,由於它是類數組,我們想拿到參數值,所以這裡我們使用slice。最終,我們其實還是調用a+b+c的運算。
同理,偏應用的存在其實就是彌補了柯里化傳參順序的短板
const partial = function (fn,...partialArgs){ let args = partialArgs; return function(...fullArgs){ let arg = 0; for(let i = 0; i<args.length && fullArgs.length;i++){ if(arg[i] === undefined){ args[i] = fullArgs[arg++]; } } return fn.apply(null,args) } } let delayTenMs = partial(setTimeout , undefined , 10); delayTenMs(() => console.log('this is Nealyang'));
同樣利用閉包存儲參數,利用undefined來佔位
組合、管道
概念
官方解釋為,函數式編程中的函數組合被稱之為組合。說的雲里霧裡的,其實就是多個函數一起完成一件事,組合嘛。那管道呢?咱通俗點,類似gulp的pipe概念,你處理完了,吐出來,我接着處理(此處不禁想起人體蜈蚣,哇~),咳咳,正式點,將最左側的函數輸出所為輸入發送給右側函數,從技術上來說,就是管道。
為什麼要這樣呢?其實還是我們之前說的,函數的原則就是小、單一、簡單。因為易測、簡單。而我們呢,通過組合使用這些簡單的函數而實現一個不簡單的函數,完成一個不簡單的功能。是不是類似於React編寫組件的概念。通過組合各種小組件完成頁面編寫的感覺?
bingo~
compose 函數的實現
先看一個簡答的實現
const compose = (a,b)=>(c)=>a(b(c)); let splitIntoSpaces = (str) => str.split(" "); let count = (array) => array.length; const countWords = compose(count,splitIntoSpaces); countWords('Hello , I am Nealyang');
在後面的開發中,我們只需要通過countWords就可以統計出單詞的數量,通過這種方式實現的也非常的優雅。
其實這種編寫的技巧就是將多個小而巧的函數組合完成不一樣的功效出來。舉個栗子:
let map = (array,fn) => { let results = [] for(const value of array) results.push(fn(value)) return results; }; let filter = (array,fn) => { let results = [] for(const value of array) (fn(value)) ? results.push(value) : undefined return results; }; let apressBooks = [ { "id": 111, "title": "C# 6.0", "author": "ANDREW TROELSEN", "rating": [4.7], "reviews": [{good : 4 , excellent : 12}] }, { "id": 222, "title": "Efficient Learning Machines", "author": "Rahul Khanna", "rating": [4.5], "reviews": [] }, { "id": 333, "title": "Pro AngularJS", "author": "Adam Freeman", "rating": [4.0], "reviews": [] }, { "id": 444, "title": "Pro ASP.NET", "author": "Adam Freeman", "rating": [4.2], "reviews": [{good : 14 , excellent : 12}] } ]; const compose = (a, b) => (c) => a(b(c)) const partial = function (fn,...partialArgs){ let args = partialArgs.slice(0); return function(...fullArguments) { let arg = 0; for (let i = 0; i < args.length && arg < fullArguments.length; i++) { if (args[i] === undefined) { args[i] = fullArguments[arg++]; } } return fn.apply(this, args); }; }; console.log("篩選結果",map(filter(apressBooks, (book) => book.rating[0] > 4.5),(book) => { return {title: book.title,author:book.author} })) //工具類函數 let filterOutStandingBooks = (book) => book.rating[0] === 5; let filterGoodBooks = (book) => book.rating[0] > 4.5; let filterBadBooks = (book) => book.rating[0] < 3.5; let projectTitleAndAuthor = (book) => { return {title: book.title,author:book.author} } let projectAuthor = (book) => { return {author:book.author} } let projectTitle = (book) => { return {title: book.title} } let queryGoodBooks = partial(filter,undefined,filterGoodBooks); let mapTitleAndAuthor = partial(map,undefined,projectTitleAndAuthor) let titleAndAuthorForGoodBooks = compose(mapTitleAndAuthor,queryGoodBooks) console.log(titleAndAuthorForGoodBooks(apressBooks)) let mapTitle = partial(map,undefined,projectTitle) let titleForGoodBooks = compose(mapTitle,queryGoodBooks) //console.log(titleForGoodBooks(apressBooks))
通過如上的代碼,我們可以很輕鬆的看出通過組合這些小函數,而實現很多功能。非常的靈活。
多個函數的組合
當前版本的compose只實現了倆個函數的組合,那麼如果對於多個函數呢?
const compose = (...fns) => (value) => reduce(fns.reverse(),(acc , fn ) => fn(acc),value);
上面最主要的一行是
reduce(fns.reverse(),(acc , fn ) => fn(acc),value)
此處我們首先fns.reverse()反轉了函數數組,並傳入了函數(acc,fn)=>fn(acc) ,它會以傳入的acc作為其參數依次調用每一個函數。很顯然,累加器的初始值為value,它將作為函數的第一個輸入。
const composeN = (...fns) => (value) => reduce(fns.reverse(),(acc, fn) => fn(acc), value); const pipe = (...fns) => (value) => reduce(fns,(acc, fn) => fn(acc), value); let oddOrEven = (ip) => ip % 2 == 0 ? "even" : "odd" var oddOrEvenWords = composeN(oddOrEven,count,splitIntoSpaces); let count = (array) => array.length; console.log(oddOrEvenWords("hello your reading about composition")) oddOrEvenWords = pipe(splitIntoSpaces,count,oddOrEven); console.log(oddOrEvenWords("hello your reading about composition"))
如上的代碼,有沒有發現composeN和pipe非常的相似?其實就是執行序列的不同而已。從左至右處理數據流我們稱之為管道。
函子
概念
在編寫代碼中的時候,我們肯定會涉及到關於錯誤的處理,而我們現在涉及到的新名詞:函子,其實也不是什麼高大上的東西,簡單的說就是在函數式編程中的一種錯誤處理方式。我們用這種純函數的方式來幫助我們處理錯誤。
函子是一個普通對象,它實現了map函數,在遍歷每一個對象的時候生成新的對象
一步步梳理概念
首先我們可以將函子理解為容器。
const Container = function(val){ this.value = val; }
優化上面的容器,我們給Container添加一個of的靜態方法,就是用來創建對象的
Container.of = function(val){ return new Container(val); }
到這一步,我們再回頭看概念,函子是一個普通對象,它實現了一個map函數。。。,所以下一步,我們就來實現一個map函數吧
Container.property.map = function(fn){ return Container.of(fn(this.value)); }
如上,我們就編寫除了一個函子,是不是也就那麼回事?所以有哥們會問了,咱編寫這個幹嘛呢?有啥用?啊哈,咱接着往下看唄
MayBe 函子
MayBe函子能夠讓我們能夠以更加函數式的方式處理錯誤
簡單的看下具體的實現吧 const MayBe = function(val) { this.value = val; }MayBe.of = function(val) { return new MayBe(val); }MayBe.prototype.isNothing = function() { return (this.value === null || this.value === undefined); };MayBe.prototype.map = function(fn) { return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value)); };console.log("MayBe chaining",MayBe.of("George") .map((x) => x.toUpperCase()) .map((x) => "Mr. " + x))console.log("MayBe chaining null", MayBe.of("George") .map(() => undefined) .map((x) => "Mr. " + x))
如上代碼的執行結果為:

IMAGE
### MayBe函子的用途
在說用途之前呢,我們可以看一下在之前處理接口返回數據的一般邏輯(hack方式)
let value = 'string'; if(value != null || value != undefind){ return value.toupperCase(); }//實際項目中的例子 getPageModuleData = () => { return this.getDataFromXctrl(pageData.moduleData).then(moduleData => { if (moduleData.filter.data.hotBrands.length) { this.setState({ moduleData: moduleData.filter.data }); } // 小於多少個拍品,進行推薦 if ( moduleData && moduleData.list && moduleData.list.data && moduleData.list.data.settings && moduleData.list.data.settings.length ) { this.recLimit = moduleData.list.data.settings[0].showRecLimit; } if (!this.recLimit) { this.recLimit = 5; // 兜底 } }); };
對,這種命令式的方式總是把一些不必要的邏輯暴露出來,使用MayBe函子就不會有這個問題
他的操作,會讓你感覺非常的舒服 MayBe.of('Nealyang') .map((x)=>x.toUpperCase()) .map(x=>`Mr.${x}`);
啰嗦了這麼多,我們就為了說明兩個MayBe函子重要的屬性
1:即使給map傳入返回null或者undefined的函數,MayBe也依舊可以處理
2:所有的map函數都會調用,無論他是否接收到null or undefined
實際操刀
說了這麼多,那麼在我們的日常開發中,我們MayBe到底如何使用呢。這裡我們還是拿項目中常見的請求接口來舉栗子~

重點
var request = require('sync-request'); ... let getTopTenSubRedditPosts = (type) => { let response try{ response = JSON.parse(request('GET',"https://www.reddit.com/r/subreddits/" + type + ".json?limit=10").getBody('utf8')) }catch(err) { response = { message: "Something went wrong" , errorCode: err['statusCode'] } } return response } let getTopTenSubRedditData = (type) => { let response = getTopTenSubRedditPosts(type); return MayBe.of(response).map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr, (x) => { return { title : x['data'].title, url : x['data'].url } } )) } console.log("正確的接收到返回:",getTopTenSubRedditData('new')) console.log("錯誤時候的情況",getTopTenSubRedditData('neww')) //MayBe{value:[{title:...,url:...},{}...]}
如上,我們請求一個接口,然後日常處理接口返回數據,並不需要去擔心值是否存在而導致程序異常~

img
Either函子
上面,我們可以正確的處理數據了,但是錯誤的數據呢?我們需要將錯誤信息跑出給出提示,這也是我們常見的需求,但是使用MayBe函子就不能夠很好地定位到錯誤的分支到底在哪了。!!!哇,搞了半天,你MayBe不咋地啊~ 其實不然,只是不同的函子有自己不同的側重,在這個時候,我們就需要一個更加強大的MayBe函子了:Either函子
大家都是聰明人,我就不多介紹了,直接看代碼:
const Nothing = function(val) { this.value = val; }; Nothing.of = function(val) { return new Nothing(val); }; Nothing.prototype.map = function(f) { return this; }; const Some = function(val) { this.value = val; }; Some.of = function(val) { return new Some(val); }; Some.prototype.map = function(fn) { return Some.of(fn(this.value)); } const Either = { Some : Some, Nothing: Nothing }
上面我們寫了兩個函數,Some和Nothing,Some簡單易懂,我們來說說Nothing,他也是一個Container,但是其map不執行指定的函數,而是直接返回對象本身。直接的說就是一些函數可以在Some上運行但是不能再Nothing中運行
console.log("Something example", Some.of("test").map((x) => x.toUpperCase())) console.log("Nothing example", Nothing.of("test").map((x) => x.toUpperCase()))
比較簡單,在實際的應用中,我們只需要簡單修改response的處理方式即可
let getTopTenSubRedditPostsEither = (type) => { let response try{ response = Some.of(JSON.parse(request('GET',"https://www.reddit.com/r/subreddits/" + type + ".json?limit=10").getBody('utf8'))) }catch(err) { response = Nothing.of({ message: "Something went wrong" , errorCode: err['statusCode'] }) } return response } let getTopTenSubRedditDataEither = (type) => { let response = getTopTenSubRedditPostsEither(type); return response.map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr, (x) => { return { title : x['data'].title, url : x['data'].url } } )) } console.log("正確的運行: ",getTopTenSubRedditDataEither('new')) console.log("錯誤:",getTopTenSubRedditDataEither('new2'))//Nothing{value:{ message: "Something went wrong" , errorCode: err['statusCode'] }}
題外話
如果大家對Java有些了解的話,一定會發現這個跟Java8 中Optional非常的相似。其實Optional就是一個函子~

img
最後談一談Monad
概念
直接點,Monad其實也是一個函子,存在即合理,咱來說一說他到底是一個啥樣子的函子。現在我們的需求是獲取Reddit的評論,當然,我們可以使用MayBe函子來搞定的,稍後我們來看下實現。只不過,這裡需要說明的是,MayBe函子更加的專註問題本身,而不必關心不必要的麻煩例如undefined或者null
需求
該需求分為兩步:
- 為了搜索指定的帖子或者評論,需要調用接口:https://www.reddit.com/search.json?q=keyword 如上,我們搜索functional programming得到如下結果

IMAGE
- 對,標記出來的Permalink是我們的下一步,訪問permalink字段,拼接地址為:https://www.reddit.com//r/programming/comments/3ejsyq/john_carmack_why_functional_programming_is_the/.json得到如下結果:

IMAGE
我們需要獲取評論對象後,將我們需要的title合併結果並返回新對象:{title:…,comments:[Object,Object,…]}
MayBe 版本實現
第一步的實現
let searchReddit = (search) => { let response try{ response = JSON.parse(request('GET',"https://www.reddit.com/search.json?q=" + encodeURI(search) + "&limit=2").getBody('utf8')) }catch(err) { response = { message: "Something went wrong" , errorCode: err['statusCode'] } } return response } let getComments = (link) => { let response try { console.log("https://www.reddit.com/" + link) response = JSON.parse(request('GET',"https://www.reddit.com/" + link).getBody('utf8')) } catch(err) { console.log(err) response = { message: "Something went wrong" , errorCode: err['statusCode'] } } return response }
上面代碼就是實現了兩個請求api。具體實現不解釋了,非常簡單。
第二步的實現
let mergeViaMayBe = (searchText) => { let redditMayBe = MayBe.of(searchReddit(searchText)) let ans = redditMayBe .map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr,(x) => { return { title : x['data'].title, permalink : x['data'].permalink } } )) .map((obj) => arrayUtils.map(obj, (x) => { return { title: x.title, comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json"))) } })) return ans; }

img
說說問題
是的,我們解決了我們的需求,但是仔細看上面代碼,貌似丟失我們使用函子的初衷:代碼簡潔,看着爽~ 而上面的map多到懷疑人生,自己寫起來可能會很好,但是別人維護起來是一個非常頭疼的事情!
最頭痛的時候,運行上面的函數後,我們拿到的值也是函子套函子,所以,該如何解決呢?這就是我們要說的Monad函子的用途
let answer = mergeViaMayBe("functional programming") console.log(answer) /* 需要兩次map才能拿到我們想要的 */ answer.map((result) => { arrayUtils.map(result,(mergeResults) => { mergeResults.comments.map(comment => { console.log(comment) }) }) })
在我們獲取Components的時候,他也是一個函子,所以我們得使用map
簡單的把問題展開是醬紫的:
let example=MayBe.of(MayBe.of(5)); //將value 加 4 的需求 example.map(x=>x.map(v=>v+4)) //MayBe{value:MayBe{value:9}}
得到的結果還是套兩層,+4的需求麻煩,得到的結果嵌套也麻煩,那麼是否可以將兩層,撥開呢????

interesting
join 來也
來的目標很簡單,撥開嵌套!!!
直接看實現:
MayBe.prototype.join = function(){ return this.isNothing?MayBe.of(null):this.value }
搞定!
在回頭看上面的需求:
let example=MayBe.of(MayBe.of(5)); example.join().map(v=>v+4);//=> MayBe(value:9)
搞定!!!
再回頭看上上面的需求:
let mergeViaJoin = (searchText) => { let redditMayBe = MayBe.of(searchReddit(searchText)) let ans = redditMayBe.map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr,(x) => { return { title : x['data'].title, permalink : x['data'].permalink } } )) .map((obj) => arrayUtils.map(obj, (x) => { return { title: x.title, comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json"))).join() } })) .join() return ans; } let answer = mergeViaJoin("functional programming") console.log(answer)
如上代碼,我們在函子後添加了兩個join,成功的解決了函子套函子的問題。
對的,上面的join的確加入的方式有點尷尬~~~~ OK~我們在改造改造。
目前,我們總是要在map後調用join方法,下面我們把邏輯封裝到一個名為chain中
MayBe.prototype.chain = function(f){ return this.map(f).join() } ... let mergeViaChain = (searchText) => { let redditMayBe = MayBe.of(searchReddit(searchText)) let ans = redditMayBe.map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr,(x) => { return { title : x['data'].title, permalink : x['data'].permalink } } )) .chain((obj) => arrayUtils.map(obj, (x) => { return { title: x.title, comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json"))).chain(x => { return x.length }) } })) return ans; } //trying our old problem with chain answer = mergeViaChain("functional programming") console.log(answer)

完美
什麼是Monad
啰嗦了這麼多,所以到底什麼是Monad呢?貌似我們一直以來都在解決問題,這種感覺就像現實中,這個人很面熟了,但是。。。還不知道怎麼稱呼一樣。尷尬~
OK,Monad就是一個含有chain方法的函子,這就是Monad!(是不是感覺這個定義非常的山寨,哈哈)
如你所見,我們通過添加一個chain(當然也包括join)來展開MayBe函子,是其成為了一個Monad!
這種感覺就像~給單車加了個電瓶,他就叫電瓶車了一樣,哈啊
結束語
函數式編程,意在告訴我們使用數學式函數思維來解決問題,別忘了我們的原則:最小單一原則!
我也還在學習的路上,不當的地方,還希望多多指教~