编程范式 —— 函数式编程入门

该系列会有 3 篇文章,分别介绍什么是函数式编程、剖析函数式编程库、以及函数式编程在 React 中的应用,欢迎关注我的 blog

命令式编程和声明式编程

拿泡茶这个事例进行区分命令式编程和声明式编程

  • 命令式编程

1.烧开水(为第一人称)
2.拿个茶杯
3.放茶叶
4.冲水

  • 声明式编程

1.给我泡杯茶(为第二人称)

举个 demo

// 命令式编程
const convert = function(arr) {
  const result = []
  for (let i = 0; i < arr.length; i++) {
    result[i] = arr[i].toLowerCase()
  }
  return result
}

// 声明式编程
const convert = function(arr) {
  return arr.map(r => r.toLowerCase())
}

什么是函数式编程

函数式编程是声明式编程的范式。在函数式编程中数据在由纯函数组成的管道中传递。

函数式编程可以用简单如交换律、结合律、分配律的数学之法来帮我们简化代码的实现。

它具有如下一些特性:

  • 纯粹性: 纯函数不改变除当前作用域以外的值;
// 反面示例
let a = 0
const add = (b) => a = a + b // 两次 add(1) 结果不一致

// 正确示例
const add = (a, b) => a + b
  • 数据不可变性: Immutable
// 反面示例
const arr = [1, 2]
const arrAdd = (value) => {
  arr.push(value)
  return arr
}

arrAdd(3) // [1, 2, 3]
arrAdd(3) // [1, 2, 3, 3]

// 正面示例
const arr = [1, 2]
const arrAdd = (value) => {
  return arr.concat(value)
}

arrAdd(3) // [1, 2, 3]
arrAdd(3) // [1, 2, 3]

在后记 1 中对数组字符串方法是否对原值有影响作了整理

  • 函数柯里化: 将多个入参的函数转化为一个入参的函数;
const add = a => b => c => a + b + c
add(1)(2)(3)
  • 偏函数: 将多个入参的函数转化成两部分;
const add = a => (b, c) => a + b + c
add(1)(2, 3)
  • 可组合: 函数之间能组合使用
const add = (x) => x + x
const mult = (x) => x * x

const addAndMult = (x) => add(mult(x))

柯里化(curry)

如下是一个加法函数:

var add = (a, b, c) => a + b + c

add(1, 2, 3) // 6

假如有这样一个 curry 函数, 用其包装 add 函数后返回一个新的函数 curryAdd, 我们可以将参数 a、b 进行分开传递进行调用。

var curryAdd = curry(add)

// 以下输出结果都相同
curryAdd(1, 2, 3) // 6
curryAdd(1, 2)(3) // 6
curryAdd(1)(2)(3) // 6
curryAdd(1)(2, 3) // 6

动手实现一个 curry 函数

核心思路: 若传进去的参数个数未达到 curryAdd 的个数,则将参数缓存在闭包变量 lists 中:

function curry(fn, ...args) {
  const length = fn.length
  let lists = args || []

  let listLen
  return function (..._args) {
    lists = [...lists, ..._args]
    listLen = lists.length

    if (listLen < length) {
      const that = lists
      lists = []
      return curry(fn, ...that)
    } else if (listLen === length) {
      const that = lists
      lists = []
      return fn.apply(this, that)
    }
  }
}

代码组合(compose)

现在有 toUpperCasereversehead 三个函数, 分别如下:

var toUpperCase = (str) => str.toUpperCase()
var reverse = (arr) => arr.reverse()
var head = (arr) => arr[0]

接着使用它们实现将数组末位元素大写化输出, 可以这样做:

var reverseHeadUpperCase = (arr) => toUpperCase(head(reverse(arr)))

reverseHeadUpperCase(['apple', 'banana', 'peach']) // "PEACH"

此时在构建 reverseHeadUpperCase 函数的时候, 必须手动声明传入参数 arr, 是否能提供一个 compose 函数让使用者更加友好的使用呢? 类似如下形式:

var reverseHeadUpperCase = compose(toUpperCase, head, reverse)

reverseHeadUpperCase(['apple', 'banana', 'peach']) // "PEACH"

此外 compose 函数符合结合律, 我们可以这样子使用:

compose(compose(toUpperCase, head), reverse)
compose(toUpperCase, compose(head, reverse))

以上两种写法与 compose(toUpperCase, head, reverse) 的效果完全相同, 都是依次从右到左执行传参中的函数。

此外 composemap 一起使用时也有相关的结合律, 以下两种写法效果相等

compose(map(f), map(g))
map(compose(f, g))

动手实现一个 compose 函数

代码精华集中在一行之内, 其为众多开源库(比如 Redux) 所采用。

var compose = (...args) => (initValue) => args.reduceRight((a, c) => c(a), initValue)

范畴论

范畴论是数学中的一个分支。可以将范畴理解为一个容器, 把原来对值的操作,现转为对容器的操作。如下图:

学习函数式编程就是学习各种函子的过程。

函数式编程中, 函子(Functor) 是实现了 map 函数的容器, 下文中将函子视为范畴,模型可表示如下:

class Functor {
  constructor(value) {
    this.value = value
  }

  map(fn) {
    return new Functor(fn(this.value))
  }
}

但是在函数式编程中, 要避免使用 new 这种面向对象的编程方式, 取而代之对外暴露了一个 of 的接口, 也称为 pointed functor

Functor.of = value => new Functor(value)

Maybe 函子

Maybe 函子是为了解决 this.value 为 null 的情形, 用法如下:

Maybe.of(null).map(r => r.toUpperCase()) // null
Maybe.of('m').map(r => r.toUpperCase())  // Maybe?{value: "M"}

实现代码如下:

class Maybe {
  constructor(value) {
    this.value = value
  }

  map(fn) {
    return this.value ? new Maybe(fn(this.value)) : null
  }
}

Maybe.of = value => new Maybe(value)

Either 函子

Either 函子 是为了对应 if...else... 的语法, 即非左即右。因此可以将之拆分为 LeftRight 两个函子, 它们的用法如下:

Left.of(1).map(r => r + 1)  // Left?{value: 1}

Right.of(1).map(r => r + 1) // Right?{value: 2}

Left 函子实现代码如下:

class Left {
  constructor(value) {
    this.value = value
  }

  map(fn) {
    return this
  }
}

Left.of = value => new Left(value)

Right 函子实现代码如下(其实就是上面的 Functor):

class Right {
  constructor(value) {
    this.value = value
  }

  map(fn) {
    return new Right(fn(this.value))
  }
}

Right.of = value => new Right(value)

具体 Either 函数只是对调用 Left 函子Right 函子 作一层筛选, 其接收 fg 两个函数以及一个函子(Left or Right)

var Either = function(f, g, functor) {
  switch(functor.constructor) {
    case 'Left':
      return f(functor.value)
    case 'Right':
      return g(functor.value)
    default:
      return f(functor.value)
  }
}

使用 demo:

Either((v) => console.log('left', v), (v) => console.log('def', v), left)   // left 1
Either((v) => console.log('rigth', v), (v) => console.log('def', v), rigth) // rigth 2

Monad 函子

函子会发生嵌套, 比如下面这样:

Functor.of(Functor.of(1)) // Functor { value: Functor { value: 1 } }

Monad 函子 对外暴露了 joinflatmap 接口, 调用者从而可以扁平化嵌套的函子。

class Monad {
  constructor(value) {
    this.value = value
  }

  map(fn) {
    return new Monad(fn(this.value))
  }

  join() {
    return this.value
  }

  flatmap(fn) {
    return this.map(fn).join()
  }
}

Monad.of = value => new Monad(value)

使用方法:

// join
Monad.of(Monad.of(1).join()) // Monad { value: 1 }
Monad.of(Monad.of(1)).join() // Monad { value: 1 }

// flatmap
Monad.of(1).flatmap(r => r + 1)  // 2

Monad 函子可以运用在 I/O 这种不纯的操作上将之变为纯函数的操作,目前比较懵懂,日后补充。

后记 1: 数组字符串方法小结(是否对原值有影响)

不会对原数组有影响的方法

slice
var test = [1, 2, 3]
var result = test.slice(0, 1)

console.log(test)   // [1, 2, 3]
console.log(result) // [1]
concat
var test = [1, 2, 3]
var result = test.concat(4)

console.log(test)   // [1, 2, 3]
console.log(result) // [1, 2, 3, 4]

对原数组有影响的方法

splice(这个需要特别记一下)
var test = [1, 2, 3]
var result = test.splice(0, 1)

console.log(test)   // [2, 3]
console.log(result) // [1]
sort
var arr = [2, 1, 3, 4]
arr.sort((r1, r2) => (r1 - r2))

console.log(arr) // [1, 2, 3, 4]
reverse
var test = [1, 2, 3]
var result = test.reverse()

console.log(test)   // [3, 2, 1]
console.log(result) // [3, 2, 1]
push/pop/unshift/shift
var test = [1, 2, 3]
var result = test.push(4)

console.log(test)   // [1, 2, 3, 4]
console.log(result) // 4

不会对原字符串造成影响的方法

substr/substring/slice
// substr
var test = 'abc'
var result = test.substr(0, 1)

console.log(test)   // 'abc'
console.log(result) // a

// substring
var test = 'abc'
var result = test.substring(0, 1)

console.log(test)   // 'abc'
console.log(result) // a

// slice
var test = 'abc'
var result = test.slice(0, 1)

console.log(test)   // 'abc'
console.log(result) // a

参考

原文地址:https://www.cnblogs.com/MuYunyun/p/10352716.html

时间: 2024-09-29 19:42:37

编程范式 —— 函数式编程入门的相关文章

我是如何开始去了解Python函数式编程--Python函数式编程初涉

Python函数式编程 1. 开始我们会了解什么是函数式编程: 函数:function 函数式:functional,一种编程范式 函数式编程特点:把计算视为函数而非指令,贴近计算 纯函数式编程:不需要变量,没有副作用,测试简单,支持高阶函数,代码简洁 Python支持的函数式编程特点: 不是纯函数式编程:允许有变量 支持高阶函数:函数也可以作为变量传入 支持闭包:有了闭包就能返回函数 有限度的支持匿名函数 2. 高阶函数 变量可以指向函数,函数名其实就是指向函数的变量,而高阶函数其实就是可以接

【转】 为什么说面向对象编程和函数式编程都有问题

我不理解为什么人们会对面向对象编程和函数式编程做无休无止的争论.就好象这类问题已经超越了人类智力极限,所以你可以几个世纪的这样讨论下去.经过这些年对编程语言的研究,我已经清楚的看到了问题的答案,所以,我经常的发现,人们对这些问题做的都是一些抓不住要领.无意义的争论. 简言之,不论是面向对象编程还是函数式编程,如果你走了极端,那都是错误的.面向对象编程的极端是一切都是对象(纯面向对象).函数式编程的极端是纯函数式编程语言. 面向对象编程的问题 面向对象的问题在于它对"对象"的定义,它试图

[原创译书] JS函数式编程 2.函数式编程基础

2 函数式编程基础 ?? Functional Programming in Javascript 主目录上一章 Javascript函数式编程的力量——举个例子 现在,你已经稍稍领略了一点函数式编程能做的事情.但是到底什么是函数式编程呢? 如何来区分一个语言是否是函数式的?又如何来区分一段程序是否是函数式的呢? 在这章,我们先来看看下面的问题,这些问题覆盖了函数式编程的核心概念: 使用函数和数组实现控制流 编写纯函数.匿名函数.递归函数等等 像对象那样传递函数 利用map().filter()

python编程:函数式编程和面向对象编程的对比

言而简之,面向对象编程就是把函数编程外面包装一个class类,然后再把这个class类指向一个对象 而class类中的函数在这里称为方法 举个例子: 用函数式编程写出一键发送邮件的脚本: def e_mail(email,message): print('邮件发送中...') return True e_mail('[email protected]','邮件内容') 用面向对象编程写出一键发送邮件的脚本: class Foo: #e_mail称为方法 def e_mail(self,email

python编程:函数式编程实现登录和注册

不知道大家有没有一种感觉,我明明学会了,为什么一写代码就不知道从何下笔了. 初学函数编程的小伙伴通常需要检测一下自己的学习成果,我们现在就写一道通过函数来实现登录和注册的小程序 def login(username,password): ''' 用于用户登录 :param username:用户输入的用户名 :param password:用户输入的密码 :return:True表示登录成功,False表示登录失败 ''' fp=open('DB','r') for line in fp: us

Python 进阶(一)函数式编程简介

来自慕课网: 简介: 函数:function ,在入门课程已学 函数式:functional,一种编程范式 函数式编程是一种抽象计算的编程模式,函数≠函数式,好比:计算≠计算机

javascript 函数式编程

编程范式 编程范式是一个由思考问题以及实现问题愿景的工具组成的框架.很多现代语言都是聚范式(或者说多重范式): 他们支持很多不同的编程范式,比如面向对象,元程序设计,泛函,面向过程,等等. 函数式编程范式 函数式编程就像一辆氢燃料驱动的汽车——先进的未来派,但是还没有被广泛推广.与命令式编程相反,他由一系列语句组成,这些语句用于更新执行时的全局状态.函数式编程将计算转化作表达式求值.这些表达式全由纯数学函数组成,这些数学函数都是一流的(可以被当做一般值来运用和处理),并且没有副作用. 函数式编程

Python进阶之函数式编程(把函数作为参数)

什么是函数式编程? 什么是函数式编程? 函数:function 函数式:functional,一种编程范式 函数式编程是一种抽象计算的编程模式 函数≠函数式,比如:计算≠计算机 在计算机当中,计算机硬件是最底层的,而越往上语言越高级 低--------------------------------->高计算机硬件-->汇编语言-->c语言-->Python语言 ↓ ↓ ↓ 指令 函数 函数式计算机------------------------>计算(数学) ○ 函数式编程

Scala学习笔记(1) -- 为什么要学函数式编程

很久之前我就学过Lisp和Erlang,但是也就是写写HelloWorld,写个排序算法.也在Coursera上听过Scala的课,可是那时候我还不怎么用Java,所以后来也没怎么继续.可是对函数式编程的兴趣一直不减,工作中几乎不会用Scala,但是用的是Java,我一直在想着怎么把Scala用到工作中.最近在写一个工具,因为这个工具基本只有我们项目组用,而且很简单,所以我就用Scala写了.以后有机会,把Scala用在生产上. 为什么对函数式编程这么感兴趣呢. 第一个原因,可以装逼,或者说是个