函數語言程式設計瞭解一下(下)

Neal_yang發表於2019-02-27

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

回顧柯里化、偏應用

函數語言程式設計瞭解一下(上)

對於上一篇文章,有朋友群裡艾特說不是很明白柯里化的函式,這裡我們拿出來簡單說下

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!

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

結束語

函數語言程式設計,意在告訴我們使用數學式函式思維來解決問題,別忘了我們的原則:最小單一原則!

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

相關文章