JS函數式編程 - 函子和範疇論

在前面幾篇介紹了函數式比較重要的一些概念和如何用函數組合去解決相對複雜的邏輯。是時候開始介紹如何控制反作用了。git

數據類型

咱們來看看上一篇最後例子:github

const split = curry((tag, xs) => xs.split(tag))
const reverse = xs => xs.reverse()
const join = curry((tag, xs) => xs.join(tag))

const reverseWords = compose(join(''), reverse, split(''))

reverseWords('Hello,world!');

這裏其實reverseWords仍是很難閱讀,你不知道他入參是啥,返回值又是啥。你若是不去看一下代碼,一開始在使用他的時候,你應該是比較懼怕的。 「我是否是少傳了一個參數?是否是傳錯了參數?返回值真的一直都是一個字符串嗎?」。這也是類型系統的重要性了,在不斷了解函數式後,你會發現,函數式編程和類型是密切相關的。若是在這裏reverseWords的類型明確給出,就至關於文檔了。編程

可是,JavaScript是動態類型語言,咱們不會去明確的指定類型。不過咱們能夠經過註釋的方式加上類型:segmentfault

// reverseWords: string => string
const reverseWords = compose(join(''), reverse, split(''))

上面就至關於指定了reverseWords是一個接收字符串,並返回字符串的函數。後端

JS 自己不支持靜態類型檢測,可是社區有不少JS的超集是支持類型檢測的,好比Flow還有TypeScript。固然類型檢測不光是上面所說的自文檔的好處,它還能在預編譯階段提早發現錯誤,能約束行爲等。數組

固然個人後續文章仍是以JS爲語言,可是會在註釋裏面加上類型。安全

範疇論相關概念

範疇論其實並非特別難,不過是些抽象點的概念。並且咱們不須要了解的特別深,函數式編程不少概念是從範疇論映射過來的。瞭解範疇論相關概念有助於咱們理解函數式編程。另外,相信我,只要你小學初中學過一元函數和集合,看懂下面的沒有問題。數據結構

定義

範疇的定義:函數式編程

  1. 一組對象,是須要操做的數據的一個集合
  2. 一組態射,是數據對象上的映射關係,好比 f: A -> B
  3. 態射組合,就是態射可以幾個組合在一塊兒造成一個新的態射

圖片描述
圖片出處:https://en.wikipedia.org/wiki...函數

一個簡單的例子,上圖來自維基百科。上面就是一個範疇,一共有3個數據對象A,B,C,而後fg是態射,而gof是一組態射組合。是否是很簡單?

其中態射能夠理解是函數,而態射的組合,咱們能夠理解爲函數的組合。而裏面的一組對象,不就是一個具備一些相同屬性的數據集嘛。

函子(functor)

函子是用來將兩個範疇關聯起來的。

圖片描述
圖片出處:https://ncatlab.org/nlab/show...

對應上圖,好比對於範疇 C 和 D ,函子 F : C => D 可以:將 C 中任意對象X 轉換爲 D 中的 F(X); 將 C 中的態射 f : X => Y 轉換爲 D 中的 F(f) : F(X) => F(Y)。你能夠發現函子能夠:

  1. 轉換對象
  2. 轉換態射

構建一個函子(functor)

Container

正如上面所說,函子可以關聯兩個範疇。而範疇裏面必然是有一組數據對象的。這裏引入Container,就是爲了引入數據對象:

class Container {
  constructor (value) {
    this.$value = value
  }
  // (value) => Container(value)
  static of(value) {
    return new Container(value)
  }
}

咱們聲明瞭一個Container的類,而後給了一個靜態的of方法用於去生成這個Container的實例。這個of其實還有個好聽的名字,賣個關子,後面介紹。

咱們來看一下使用這個Container的例子:

// Container(123)
Container.of(123)

// Container("Hello Conatiner!")
Container.of("Hello Conatiner!")

// Container(Conatiner("Test !"))
Container.of(Container.of("Test !"))

正如上面看到的,Container是能夠嵌套的。咱們仔細看一下這個Contaienr:

  1. $value的類型不肯定,可是一旦賦值以後,類型就肯定了
  2. 一個Conatiner只會有一個value
  3. 咱們雖然能直接拿到$value,可是不要這樣作,否則咱們要個container幹啥呢

第一個functor

讓咱們回看一下定義,函子是用來將兩個範疇關聯起來的。因此咱們還須要一個態射(函數)去把兩個範疇關聯起來:

class Container {
  constructor (value) {
    this.$value = value
  }
  // (value) => Container(value)
  static of(value) {
    return new Container(value)
  }
  // (fn: x=>y) => Container(fn(value))
  map(fn) {
    return new Container(fn(this.$value))
  } 
}

先來用一把:

const concat = curry((str, xs) => xs.concat(str))
const prop = curry((prop, xs) => xs[prop])

// Container('TEST')
Container.of('test').map(s => s.toUpperCase())

// Container(10)
Container.of('bombs').map(concat(' away')).map(prop('length'));

不曉得上面的curry是啥的看第二篇文章

你可能會說:「哦,這是你說的functor,那又有啥用呢?」。接下來,就講一個應用。

不過再講應用前先講一下這個of,其實上面這種functor,叫作pointed functor, ES5裏面的Array就應用了這種模式:Array.of。他是一種模式,不只僅是用來省略構建對象的new關鍵字的。我感受和scala裏面的compaion object有點相似。

Maybe type

在現實的代碼中,存在不少數據是可選的,返回的數據多是存在的也可能不存在:

type Person = {
  info?: {
    age?: string
  }
}

上面是flow裏面的類型聲明,其中?表明這個數據可能存在,可能不存在。我相信像上面的數據結構,你在接收後端返回的數據的時候常常遇到。假如咱們要取這個age屬性,咱們一般是怎麼處理的呢?

固然是加判斷啦:

const person = { info: {} }

const getAge = (person) => {
  return person && person.info && person.info.age
}

getAge(person) // undefined

你會發現爲了取個age,咱們須要加不少的判斷。當數據中有不少是可選的數據,你會發現你的代碼充滿了這種類型判斷。心累不?

Okey,Maybe type就是爲了解決這個問題的,先讓咱們實現一個:

class Maybe {
  static of(x) {
    return new Maybe(x);
  }

  get isNothing() {
    return this.$value === null || this.$value === undefined;
  }

  constructor(x) {
    this.$value = x;
  }

  map(fn) {
    return this.isNothing ? this : Maybe.of(fn(this.$value));
  }

  get() {
    if (this.isNothing) {
      throw new Error("Get Nothing")
    } else {
      return this.$value
    }
  }

  getOrElse(optionValue) {
    if (this.isNothing) {
      return optionValue
    } else {
      return this.$value
    }
  }
}

應用一波:

type Person = {
  info?: {
    age?: string
  }
}

const prop = curry((tag, xs) => xs[tag])
const map = curry((fn, f) => f.map(fn))

const person = { info: {} }

// safe get age
Maybe.of(person.info).map(prop("age")) // Nothing

// safe get age Point free style
const safeInfo = xs => Maybe.of(person.info)
const getAge = compose(map(prop('age')), safeInfo)
getAge(person) // Nothing

來複盤一波,上面的map依然是一個functor(函子)。不過呢,在作類型轉換的時候加上了邏輯:

map(fn) {
  return this.isNothing ? this : Maybe.of(fn(this.$value));
}

因此也就是上面的轉換關係能夠表示爲:

圖片描述

其實一看圖就出來了,「哦,你把判斷移動到了map裏面。有啥用?」。ok,羅列一下好處:

  1. 更安全
  2. 將判斷邏輯進行封裝,代碼更簡潔
  3. 聲明式代碼,沒有各類各樣的判斷

其實,不肯定性,也是一種反作用。對於可選的數據,咱們在運行時是很難肯定他的真實的數據類型的,咱們用Maybe封裝一下其實自己就是封裝這種不肯定性。這樣就能保證咱們的一個入參只有可能會返回一種輸出了。

相關文章
相關標籤/搜索