函數式編程瞭解一下(下)

GitHub原文地址:https://github.com/Nealyanggit

回顧柯里化、偏應用

函數式編程瞭解一下(上)github

對於上一篇文章,有朋友羣裏艾特說不是很明白柯里化的函數,這裏咱們拿出來簡單說下編程

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的運算。json

同理,偏應用的存在其實就是彌補了柯里化傳參順序的短板gulp

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來佔位api

組合、管道

概念

官方解釋爲,函數式編程中的函數組合被稱之爲組合。說的雲裏霧裏的,其實就是多個函數一塊兒完成一件事,組合嘛。那管道呢?咱通俗點,相似gulp的pipe概念,你處理完了,吐出來,我接着處理(此處不由想起人體蜈蚣,哇~),咳咳,正式點,將最左側的函數輸出所爲輸入發送給右側函數,從技術上來講,就是管道。數組

爲何要這樣呢?其實仍是咱們以前說的,函數的原則就是小、單1、簡單。由於易測、簡單。而咱們呢,經過組合使用這些簡單的函數而實現一個不簡單的函數,完成一個不簡單的功能。是否是相似於React編寫組件的概念。經過組合各類小組件完成頁面編寫的感受?bash

bingo~閉包

compose 函數的實現

先看一個簡答的實現app

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!

這種感受就像~給自行車加了個電瓶,他就叫電瓶車了同樣,哈啊

結束語

函數式編程,意在告訴咱們使用數學式函數思惟來解決問題,別忘了咱們的原則:最小單一原則!

我也還在學習的路上,不當的地方,還但願多多指教~

相關文章
相關標籤/搜索