函數式編程相識一下(上)

一直以來沒有對函數式編程有一個周全的進修和應用,也許說沒有一個深入的思索。近來看到一些博客文章,倏忽以為函數式編程照樣蠻有意思的。看了些書和文章。這裏紀錄下感悟和收成。 迎接團隊姜或人多多指點@姜少。 由於博客秉持着簡短且周全準繩。遂分為高低兩篇

原文地點 Nealyang

部份簡介

### 函數式編程相識一下(上)

  • 入門簡介
  • HOC簡介
  • 函數柯里化與偏應用

    函數式編程相識一下(下)

  • 組合與管道
  • 函子和Monad
  • 再回首Generator

入門簡介

函數的第一準繩是要小,函數的第二準繩是要更小

什麼是函數式編程?為何他重要

在邃曉什麼是函數式編程的最先,我們先相識下什麼數學中,函數具有的特徵

  • 函數必需老是接收一個參數
  • 函數必需老是返回一個值
  • 函數應當根據接收到的參數,而不是外部的環境運轉
  • 關於一個指定的x,必需返回一個一定的y

所以我們說,函數式編程是一種範式,我們能夠以此建立僅依靠輸入就能夠完成本身邏輯的函數。這保證了當函數屢次挪用時,依舊能夠返回雷同的效果。因而能夠發生可緩存的、可測試的代碼庫

援用通明

一切的函數關於雷同的輸入都返回雷同的構造,這一特徵,我們稱之為援用通明。
比方:

let identity = (i) => {return i};

這麼簡樸?對,實在就是如許,也就是說他沒有依靠任何外部變量、外部環境,只需你給我東西,我經由一頓鼓搗,老是給你返回你所能展望的效果。

這也為我們背面的併發代碼、緩存成為可能。

敕令式、聲明式和籠統

函數式編程主意聲明式編程和編寫籠統代碼。實在這個比較有意思,覺得更像是面向對象的編程。

言而不可都是扯淡。舉個栗子

  var array = [1,2,3,4,5,6];
  for(let i = 0;i<array.length;i++){
    console.log(array[i])
  }

這段代碼的作用簡樸明了,就是遍歷!然則你有無覺得這個代碼獃獃的。沒有一丁點的靈氣?都是我通知你該怎樣該怎樣做的。我們通知編譯器,你先去獲取下數組的長度的,然後挨個log出來。這類編碼體式格局,我們平常稱之為“敕令式”解決方案。

而在函數式編程中,我們實在越發主意用“聲明式”解決方案

let array = [1,2,3,4,5,6];
array.forEach(item=>{console.log(item)})

簡樸體會下,是不是是有那末一丟丟的靈感來了?等等,你這個forEach函數哪來的嘛!對,也是本身寫的,然則否是我們經由歷程編寫這類籠統邏輯代碼,而讓團體的營業代碼越發的清楚明了了呢?開發者是須要體貼手頭上的題目就好了,只須要通知編譯器去幹嗎而不是怎樣幹了。是不是是輕鬆了?

實在函數式編程主意的就是以籠統的體式格局建立函數。這些函數能夠在代碼的其他部份被重用。

函數式編程的優點

優點個人不喜歡扯太多,不是由於他沒有優點,而是關於方才打仗函數式編程的哥們,上來就說優點實際上是沒什麼觀點的,所以這裏我簡樸提一提,背面文章會細細申明。

純函數 => 可緩存

熟習redux的同硯應當對這個詞語都不生疏,所謂的純函數,實在也就是我們說的援用通明,穩固輸出!優點呢?可展望嘛,輕易編寫測試代碼哇,可緩存嘛。什麼是可緩存?能夠看我之前發的文章哈,這裏簡樸舉個栗子

let longRunningFunction = (input)=>{
  //進行了異常貧苦的盤算,然後返回出來效果
  return output;
}

假如longRunningFunction是一個純函數,援用通明。我們就能夠說關於一樣的輸出,老是返回一樣的效果,所以我們為何不能夠應用一個對象將我們每一次的運算效果存起來呢?

let longRunningFunctionResult = {1:2,2:3,3:4};
//搜檢key是不是存在,存在直接用,不存在再盤算
longRunningFunctionResult.hasOwnProperty(input)?longRunningFunctionResult[input]:longRunningFunctionResult[input] = longRunningFunction(input)

比較直觀。不多說了哈。實在優點另有之前說到的併發。不說的這麼堂而皇之了,啥並不併發呀,我不依靠他人的任何要素,只根據你的輸出我產出。你說我支撐什麼就是什麼咯,只需你給我對的參數傳進來就能夠了。

結束語

漸漸掃尾!僅作為舉一反三。背面我們在體系性的進修下函數式編程。

高階函數(HOC)簡介

觀點

JavaScript作為一門言語,將函數視為數據。許可函數替代數據通報是一個異常壯大的觀點。接收一個函數作為參數的函數成為高階函數(Higher-Order Function)

從數據入門HOC

JavaScript支撐以下幾種數據類型:

  • Number
  • String
  • Boolean
  • Object
  • null
  • undefined

這內里想強調的是JavaScript將函數也一樣是為一種數據類型。當一門言語許可將函數作為數據那樣通報和應用的時刻,我們就稱函數為一等國民。

所以說這個就是為了強調申明,在JavaScript中,函數能夠被賦值,作為參數通報,也能夠被其他函數返回。

//通報函數
let tellType = (arg)=>{
  if(typeof arg === 'function'){
    arg();
  }else{
    console.log(`this data is ${arg}`)
  }
}

let dataFn = ()=> {
  console.log('this is a Function');
}

tellType(dataFn);
//返回函數
let returnStr = ()=> String;

returnStr()('Nealyang')

//let fn = returnStr();
//fn('Nealyang');

從上我們能夠看到函數能夠接收另一個函數作為參數,一樣,函數也能夠將兩一個函數作為返回值返回。

所以高階函數就是接收函數作為參數而且/也許返回函數作為輸出的函數

HOC 究竟你是幹嗎的

當我們相識到怎樣去建立並實行一個高階函數的時刻,偕行我們都想去相識,他究竟是幹嗎的?OK,簡樸的說,高階函數常用於籠統通用的題目。換句話說,高階函數就是定義籠統。簡樸的說,實在就類似於敕令式的編程體式格局,將詳細的完成細節封裝、籠統起來,讓開發者越發的體貼營業。籠統讓我們專註於預定的目標而不是去體貼底層的體系觀點。

邃曉這個觀點異常重要,所以下面我們將經由歷程大批的栗子來申明

舉斤栗子

const every = (arr,fn)=>{
  let result = true;
  for(const value of arr){
    result  = result && fn(value);
  }
  return result;
}

every([NaN,NaN,4],isNaN);

const some = (arr,fn)=>{
  let result = true;
  for(const value of arr){
    result  = result || fn(value);
  }
  return result;
}
some([3,1,2],isNaN);
//這裏都是低效的完成。這裏主如果邃曉高階函數的觀點
let sortObj = [
  {firstName:'aYang',lastName:'dNeal'},
  {firstName:'bYang',lastName:'cNeal'},
  {firstName:'cYang',lastName:'bNeal'},
  {firstName:'dYang',lastName:'aNeal'},
];

const sortBy = (property)=>{
  return (a,b) => {
    return (a[property]<b[property])?-1:(a[property]>b[property])?1:0
  }
}

sortObj.sort(sortBy('lastName'));
//sort函數接收了被sortBy函數返回的比較函數,我們再次籠統出compareFunction的邏輯,讓用戶越發關注比較,而不用去在意怎樣比較的。

HOC必定離不開閉包

上面的sortBy實在人人都應當看到了閉包的蹤跡。關於閉包的發生、觀點這裏就不煩瑣了。總之我們曉得,閉包異常壯大的緣由就是它對作用域的接見。

簡樸說下閉包的三個可接見的作用域:

  • 在它本身聲明以內的變量
  • 對全局變量的接見
  • 對外部函數變量的接見(*)

接着舉栗子

const forEach = (arr,fn)=>{
  for(const item of arr){
    fn(item);
  }
}
//tap接收一個value,返回一個帶有value的閉包函數
const tap = (value)=>(fn)=>{
  typeof fn === 'function'?fn(value):console.log(value);
}

forEach([1,2,3,4,5],(a)=>{
  tap(a)(()=>{
    console.log(`Nealyang:${a}`)
  })
});

函數柯里化與偏應用

函數柯里化

觀點

直接看觀點,柯里化是把一個多參函數轉換為一個嵌套的一元函數的歷程

不邃曉,莫方!舉個栗子就邃曉了。

假定我們有一個函數,add:

const add = (x,y)=>x+y;

我們挪用的時刻固然就是add(1,2),沒有什麼迥殊的。當我們柯里化了今後呢,就是以下版本:

const addCurried = x => y => x + y;

挪用的時刻呢,就是這個模樣的:

addCurried(4)(4)//8

是不是是異常的簡樸?

說到這,我們在來回憶下,柯里化的觀點:把一個多參函數轉換成一個嵌套的一元函數的歷程。

怎樣完成多參函數轉為一元

上面的代碼中,我們完成了二元函數轉為一元函數的歷程。那末關於多參我們該怎樣做呢?

這個是比較重要的部份,我們一步一步來完成

我們先來增加一個劃定規矩,最一層函數搜檢,假如傳入的不是一個函數來挪用curry函數則拋出毛病。當假如供應了柯里化函數的一切參數,則經由歷程應用這些傳入的參數挪用真正的函數。

let curry = (fn) => {
if(typeof fn !== 'function'){
  throw Error('not a function');
}
return function curriedFn (...args){
  return fn.apply(null,args);
}
}

所以如上,我們就能夠這麼玩了

const multiply = (x,y,z) => x * y * z;
curry(multiply)(1,2,3);//6

反動還未勝利,我們繼承哈~下面我們的目標就是把多參函數轉為嵌套的一元函數(重回觀點)

const multiply = (x,y,z) => x * y * z;
let curry = (fn) => {
  if(typeof fn !== 'function'){
    throw Error('not a 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);
  }
}
curry(multiply)(1)(2)(3)

假如是首次看到,可能會有些迷惑。我們一行行來瞅瞅。

args.length < fn.length

這段代碼比價直接,就是推斷,你傳入的參數是不是小於函數參數長度。

args.concat([].slice.call(arguments))

我們應用cancat函數鏈接一次傳入的一個參數,並遞歸挪用curriedFn。由於我們將一切的參數傳入組兼并遞歸挪用,終究if推斷會失效,就返回效果了。

小小實操一下

我們寫一個函數在數組內容中查找到包括数字的項

let curry = (fn) => {
  if(typeof fn !== 'function'){
    throw Error('not a 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);
  }
}
let match = curry(function(expr,str){return str.match(expr)});

let hasNumber = match(/[0-9]+/);

let filter = curry(function(f,ary){
  return ary.filter(f)
});

filter(hasNumber)(['js','number1']);

經由歷程如上的例子,我想我們也應當看出來,為何我們須要函數的柯里化:

  • 順序片斷越小越輕易被設置
  • 盡量的函數化

偏應用

假定我們須要10ms后實行某一個特定操縱,我們平常的做法是

setTimeout(() => console.log('do something'),10);
setTimeout(() => console.log('do other thing'),10);

如上,我們挪用函數都傳入了10,能應用curry函數把他在代碼中隱蔽嗎?我擦,咱curry多牛逼!一定不可的嘛~

由於curry函數應用參數列表是從最左到最右的。由於我們是根據須要通報函數,並將10保存在常量中,所以不能以這類體式格局應用curry。我們能夠這麼做:

const setTimeoutFunction = (time , fn) => {
  setTimeout(fn,time);
}

然則假如如許的話,我們是不是是太過於貧苦了呢?為了減少了10的通報,還須要多造一個包裝函數?

這時刻,偏應用就出來了!!!

簡樸看下代碼完成:

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'));

如上人人應當都能夠邃曉。這裏不做過量空話詮釋了。

簡樸總結的說:

所以,像map,filter我們能夠輕鬆的應用curry函數解決題目,然則關於setTimeout這類,最合適的挑選固然就是偏函數了。總之,我們應用curry也許partial是為了讓函數參數也許函數設置變得越發的簡樸壯大。

下節預報

上一部份說的比較淺易基本,願望人人也能夠從中感受到函數式編程的精巧和天真的地方。大神請直接略過~求斧正求指點~

下一節中,將重要引見下,函數式編程中的組合、管道、函子以及Monad。末了我們在引見下es6的Generator,也許我們能從末了的Generator中恍然大悟獲獲得許多啟示哦~~

手藝交流

nodejs 手藝交流 群號:698239345

React手藝棧群號:398240621

前端手藝雜談群號:604953717

    原文作者:isNealyang
    原文地址: https://segmentfault.com/a/1190000014399295
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞