函數語言程式設計1-基礎知識

瘋狂的小蘑菇發表於2017-06-26

示例程式碼庫
本文原始碼庫

為什麼使用函式式? js天生支援函式式,與函式式無縫結合

  • 高階函式
    [1, 2, 3].forEach(console.log)複製程式碼
  • 函式分離
    const splat = handle => (...array) => handle(array)
    console.log(splat(array => array.reduce((a, b) => a * b))(1, 2, 3, 4))複製程式碼

與物件導向的區別

  • 與物件導向方法將問題分解成多組"名詞"或物件不同,函式式方法將相同的問題分解成多組"動詞"或者函式。

  • 與物件導向類似的是,函數語言程式設計也通過"粘結"或"組合"其他函式的方式構建更大的函式,以實現更抽象的行為。

  • 函式式:通過把功能拆解成一個個小函式元件,再用函式講各個元件結合完成需求。

程式碼風格

  • 物件導向:以物件為抽象單元

      function Team() {
        this.persons = []
      }
    
      Team.prototype.push = function(person) {
        this.persons.push(person)
      }
    
      Team.prototype.getAll = function() {
        return this.persons
      }
    
      const team = new Team()
      team.push('張三')
      team.push('李四')
      team.push('王五')
      console.log(team.getAll())複製程式碼
  • 函式式:以函式為抽象單元

      const Team = () => {
        const persons = []
        return {
          push: person => persons.push(person),
          getAll: () => persons,
        }
      }
    
      const team = Team()
      team.push('張三')
      team.push('李四')
      team.push('王五')
      console.log(team.getAll())複製程式碼

資料抽象

  • 字串列印表格

      const csv = `name, age, hair
        merble,  , red
        bob, 64, blonde`
    
      const keys = Object.keys
      const reduce = (obj, handler, initial = {}) => {
        return keys(obj).reduce((last, key) => {
          return handler(last, obj[key], key)
        }, initial)
      }
      const chain = actions => (array) => {
        return reduce(actions, (last, handle, action) => {
          return last[action](handle)
        }, array)
      }
      const split = (str, separator) => str.split(separator)
      const trim = str => str.trim()
      const trust = str => !!str.trim()
      const csv2Table = str => split(str, '\n').map(row => chain({ filter: trust, map: trim })(split(row, ',')))
      console.log(csv2Table(csv))複製程式碼

函式加工

  • 用來判斷object與array是否非空

      // 非空校驗
      function existy(obj) {
        return !!obj
      }
    
      console.log([null, undefined, false, '', [], {}].map(existy))
    
      // 函式再加工,增加object與array非空校驗
      function truthy(obj) {
        return existy(obj) && (typeof obj === 'object' ? !!Object.keys(obj).length : true)
      }
    
      console.log([null, undefined, false, '', [], {}].map(truthy))複製程式碼

函式執行

  • 如果引數是物件的屬性,屬性是個方法,則執行方法,屬性是個值,則直接返回值。如果不存在,返回undefined

      const existy = x => !!x
    
      const execer = (condition, action) => (...args) => {
        return existy(condition) ? action(...args) : undefined
      }
    
      const propExecer = (target, name) => (...args) => {
        const action = target[name]
        return execer(action, () => {
          return typeof action === 'function' ? action.apply(target, args) : action
        })()
      }
    
      [
        propExecer([1, 2, 3], 'reverse')(),
        propExecer({ foo: 42 }, 'foo')(),
        propExecer([1, 2, 3], 'concat')([4], [5], [6]),
      ].map(console.log)複製程式碼

判斷執行

  • 這種例子比比皆是,比如根據後端的返回值,如果success是true,則執行重新整理,如果是false,則提示使用者錯誤。

      const existy = x => !!x
    
      const execer = (condition, action) => (...args) => {
        return existy(condition) ? action(...args) : undefined
      }
    
      const divider = actions => (...args) =>
        actions.map(({ condition, action, name }) => ({ name, return: execer(condition, action)(...args) }))
    
      /* 測試函式 */
      const submit = (res) => {
        const actions = {
          waiting: (...args) => {
            console.log(`waiting執行,引數:${args}`)
            return '返回值: 成功'
          },
    
          success: (...args) => {
            console.log(`success執行,引數:${args}`)
            return '返回值: 成功'
          },
    
          fail: (...args) => {
            console.log(`fail執行,引數:${args}`)
            return '返回值: 失敗'
          },
        }
    
        const mapper = isSuccess =>
          [{
            name: 'waiting',
            condition: isSuccess === undefined,
            action: actions.waiting,
          }, {
            name: 'success',
            condition: isSuccess === true,
            action: actions.success,
          }, {
            name: 'fail',
            condition: isSuccess === false,
            action: actions.fail,
          }]
        const { success, data } = res
        return divider(mapper(success))(data)
      }
    
      console.log(submit({ success: undefined, data: '介面返回資料: 讀取中' }))
      console.log(submit({ success: true, data: '介面返回資料: 讀取成功' }))
      console.log(submit({ success: false, data: '介面返回資料: 讀取失敗' }))複製程式碼

一等公民

函式可以去任何值可以去的地方,很少有限制。比如數字在js中就是一等公民,函式同理。

  • 函式和數字一樣可以儲存為變數
    var fortytwo = function() { return 42 };

  • 函式和數字一樣可以儲存在陣列的一個元素中
    var fortytwos = [42, function() { return 42 }];

  • 函式和數字一樣可以作為物件的成員變數
    var fortytwos = {number: 42, fun: function() { return 42 } };

  • 函式和數字一樣可以在使用時直接建立
    42 + (function() { return 42 })();

  • 函式和數字一樣可以傳遞給另一個函式
    function weirdAdd(n ,f) { return n + f() }

  • 函式和數字一樣可以被另一個函式返回
    function weirdAdd() { return function() { return 42 } }

多種JS程式設計方式

  • 指令式程式設計
    通過詳細描述行為的程式設計方式
  • 基於原型的物件程式設計
    基於原型物件和例項的程式設計方式
  • 超程式設計
    基於模型資料進行編寫和操作的程式設計方式
  • 函數語言程式設計
    基於函式進行操作的程式設計方式
    • Applicative程式設計
      函式作為引數的程式設計方式
    • 集合中心程式設計
      對資料進行操作,包括物件和陣列的程式設計方式
  • 其他程式設計:
    • 面向型別
    • 事件程式設計

用各種模式編寫一個例子:

  • 開始數字為x=99
  • 重複唱一下內容直到數字為1

    • 牆上有x瓶啤酒
    • x瓶啤酒
    • 拿下一個來,分給大家
    • 牆上還有x-1瓶啤酒
    • x-1後,再迴圈唱一次。
    • 直到x-1=1後,改為:牆上已經沒有啤酒了。

      const _ = require('../util/understore')
      
      // 牆上有x瓶啤酒
      // x瓶啤酒
      // 拿下一個來,分給大家
      // 牆上還有x-1瓶啤酒
      // x-1後,再迴圈唱一次。
      // 直到x-1=1後,改為:牆上已經沒有啤酒了。
      
      // 命令程式設計
      for (let i = 99; i > 0; i--) {
      if (i === 1) {
        console.log('沒有啤酒了')
      } else {
        console.log(`牆上有${i}瓶啤酒,拿下一個來,分給大家。牆上還有${i - 1}瓶啤酒`)
      }
      }
      
      // 函數語言程式設計
      const segment = i =>
      _.chain([]).push(`牆上有${i}瓶啤酒,拿下一個來,分給大家,`).tap((data) => {
        const remain = i - 1
        if (remain > 0) {
          data.push(`還剩${i - 1}瓶啤酒,`)
        } else {
          data.push('沒有啤酒了,')
        }
      }).push('大家喝吧\n')
      .value()
      
      const song = (start, end, seg) => _.range(start, end).reduce((arr, next) => arr.concat(seg(next)), [])
      console.log(song(99, 1, segment).join(''))
      
      // 超程式設計
      function Point2D(x, y) {
      this._x = x
      this._y = y
      }
      
      function Ponit3D(x, y, z) {
      Point2D.call(this, x, y)
      this._z = z
      }
      
      console.log(new Ponit3D(1, 2, 3))複製程式碼

reduce

reduce是函數語言程式設計的核心之一,用途之廣隨處可見。

// 注意reduce與reduceRight的區別,一個從左計算,一個從右計算
const nums = [100, 2, 25]
const div = (x, y) => x / y

console.log(nums.reduce(div))
console.log(nums.reduceRight(div))

// 沒有明顯的順序關係,reduce 與 reduceRight相等, 如下reduce可以換成reduceRight,結果相同
const all = (...args) => condition =>
  args.reduce((truth, f) => (truth && f() === condition), true)

const any = (...args) => condition =>
  args.reduce((truth, f) => (truth || f() === condition), false)

const T = () => true
const F = () => false

// 全部為真
console.log(all(T, T)(true))
// 全部為假
console.log(all(F, F)(false))
// 全部為真,傳入全部假
console.log(all(F, F)(true))
// 全部為假,傳入全部真
console.log(all(T, T)(false))
// 部分為真
console.log(any(T, F)(true))
// 部分為假
console.log(any(T, F)(false))
// 部分為真,傳入全部假
console.log(any(F, F)(true))
// 部分為假,傳入全部真
console.log(any(T, T)(false))複製程式碼
陣列操作

使用reduce運算元組,進行排序,分組,統計數量等。

const people = [
  { name: 'Rick', age: 30, sex: 'man' },
  { name: 'Lucy', age: 24, sex: 'woman' },
  { name: 'Lily', age: 40, sex: 'woman' },
]

const sortBy = (datas, fn) =>
  datas.sort((d1, d2) => fn(d1) - fn(d2))

console.log('sortBy', sortBy(people, p => p.age))

const groupBy = (datas, fn) =>
  datas.reduce((last, data) =>
      ({ ...last, [`${fn(data)}`]: (last[fn(data)] || []).concat(data) }), {})

console.log('groupBy', groupBy(people, p => p.sex))

const countBy = (datas, fn) =>
  datas.reduce((last, data) =>
      ({ ...last, [`${fn(data)}`]: (last[fn(data)] ? ++last[fn(data)] : 1) }), {})

console.log('countBy', countBy(people, p => p.sex))複製程式碼

假如沒有join,那麼你如何實現陣列拼接?

function cat(...rest) {
  const [head, ...tail] = rest
  return head.concat(...tail)
}

function mapcat(coll, fun) {
  return cat(...coll.map(fun))
}

function removeLast(...coll) {
  return coll.slice(0, -1)
}

function construct(head, ...tail) {
  return cat([head], ...tail)
}

function interpose(coll, sep) {
  return removeLast(...mapcat(coll, item => construct(item, sep)))
}

console.log(interpose([1, 2, 3], ','))複製程式碼

一臉懵逼有木有。這就是函式式的精髓,把全部操作封裝成函式。仔細品味,思路清晰可見。

interpose連線最後結果,對外暴露非常簡單的API。removeLast用來移除陣列最後一項。cat連線所有操作結果,合成一個陣列。mapcat用來把每一項函式執行的結果傳遞給cat函式, 第二個引數函式,用來自定義操作函式如何拼接。當然,拼接也是個動作,我們封裝在construct函式中。

邏輯思維不好的人,請放棄函式式吧。哈哈哈。

資料操作

最重要的環節來了,不囉嗦,直接上程式碼

/* 物件操作 */
const keys = Object.keys
const identity = value => value

// 根據函式,把指定的物件轉成想要的任何格式,萬能函式
const reduce = (obj, handler, initial = {}) => keys(obj).reduce((last, key) => handler(last, obj[key], key), initial)

// 根據函式,把物件轉成想要的格式物件
const map = (obj, handler) => reduce(obj, (last, value, key) => (Object.assign(last, { [key]: handler(value, key) })))

// 根據函式,把物件轉成想要的格式陣列
const map2Array = (obj, handler) => keys(obj).map((key, index) => handler(obj[key], key, index))

// 獲取json的值拼成陣列,相當於Object.values
const values = obj => map2Array(obj, identity)

// 把{key:value} 轉成 [[key,value]] 的格式
const pairs = obj => map2Array(obj, (value, key) => ([key, value]))

// 反轉物件,交換key value
const invert = obj => reduce(obj, (last, value, key) => (Object.assign(last, { [value]: key })))

/* 陣列操作 */
// 萃取json陣列中的欄位
const pluck = (datas, propertyName) => datas.map(data => data[propertyName])

// 把[[key,value]]格式陣列轉json
const object = datas => datas.reduce((last, [key, value]) => (Object.assign(last, { [key]: value })), {})

/* 給json陣列欄位增加預設值 */
const defaults = (datas, misses) => datas.map((data) => {
  const finalData = Object.assign({}, data)
  map(misses, (value, key) => {
    if (finalData[key] === undefined) {
      finalData[key] = value
    }
  })
  return finalData
})

/* 表查詢 */
const findEqual = (datas, where) => {
  const wheres = pairs(where)
  return datas.filter(data => wheres.every(([key, value]) => data[key] === value))
}

/* 測試 */
// 模擬資料
const json = { file: 'day of the dead', name: 'bob' }
const array = [{ title: 't1', name: 'n1' }, { title: 't2', name: 'n2' }, { title: 't3' }]

// 物件
console.log(values(json))
console.log(pairs(json))
console.log(invert(json))

// 陣列
console.log(pluck(array, 'title'))
console.log(defaults(array, { name: 'name' }))
console.log(object(pairs(json)))
console.log(object(pairs(json).map(([key, value]) => ([key.toUpperCase(), value]))))
console.log(findEqual(array, { title: 't3' }))複製程式碼

表格操作

最後是表格操作,完全等同於SQL

/* 表格操作 */
const keys = Object.keys
const reduce = (obj, handler, initial = {}) => keys(obj).reduce((last, key) => handler(last, obj[key], key), initial)
const filter = (obj, handler) => reduce(obj, (last, value, key) => (handler(value, key) ? Object.assign(last, { [key]: value }) : last))
const pick = (obj, names) => filter(obj, (value, key) => names.includes(key))
const pluck = (datas, propertyName) => datas.map(data => data[propertyName])

/* 查詢指定列資料 */
const findColumn = (datas, columns) => datas.map(data => pick(data, columns))

/* 返回物件新列名 */
const rename = (data, newNames) =>
  reduce(newNames, (last, newName, oldName) => {
    if (data[oldName] !== undefined) {
      last[newName] = data[oldName]
      delete last[oldName]
      return last
    }
    return last
  }, Object.assign({}, data))

/* 返回表格新列名 */
const asname = (table, newNames) => table.map(data => rename(data, newNames))

/* 根據where條件進行查詢 */
const findWhere = (datas, handle) => {
  return datas.filter(data => handle(data))
}

/* 測試 */
// 模擬資料
const table = [{ title: 't1', name: 'n1', age: 30 }, { title: 't2', name: 'n2', age: 40 }, { title: 't3', age: 50 }]
console.log(pluck(table, 'title'))
console.log(findColumn(table, ['title', 'name']))
console.log(rename({ title: 't1', name: 'n1' }, { title: 'tit' }))
console.log(asname(table, { title: 'tit' }))
console.log(findWhere(findColumn(asname(table, { title: 'tit' }), ['tit', 'age']), item => item.age > 40))複製程式碼

相關文章