函数式编程初探

什么是函数式编程

最近通过小册和网上的一些资料学习了一下函数式编程,这里做个记录。

函数式编程是一种风格范式,没有一个标准的教条式定义。

我们把函数式编程和命令式编程做个比较

命令式编程关注的是一系列具体的执行步骤,当你想要使用一段命令式的代码来达到某个目的,你需要一步一步地告诉计算机应该"怎样做"。

与命令式编程严格对立的其实是"声明式编程":不关心"怎样做",只关心"得到什么",关注的是数据的映射。

具体到范式表达上,函数式编程总是需要我们去思考这样两个问题:

  • 我想要什么样的输出?
  • 我应该提供什么样的输入?
ini 复制代码
(1 + 3) * 4 / 2

// 命令式
let a = 1 + 3;
let b = a * 4;
let c = b / 2;

// 声明式
let result = divide(multiply(add(1,3), 4), 2);

我们来看一下维基百科的定义:

函数式编程(英语:functional programming)或称函数程序设计、泛函编程,是一种编程范式。它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ演算为该语言最重要的基础。而且,λ演算的函数可以接受函数作为输入参数和输出返回值。 ------Wikipedia

维基百科中的定义比较晦涩难懂,但是我们从【特征即定义】的角度去定义,就 JS 编程而言,具备以下特征

  • 拥抱 纯函数 ,隔离副作用
  • 函数是"一等公民"
  • 避免对状态的改变(不可变值)

函数式编程的优势

  • 代码简洁,负担小,方便读写
scss 复制代码
add(1,2).multiply(3).subtract(4)
  • 代码可复用,方便管理和维护,利人利己

  • 清晰的逻辑边界,更少的测试工作

为什么要拥抱纯函数,隔离副作用

什么是纯函数

同时满足以下两个特征的函数,我们就认为是纯函数:

  • 对于相同的输入,总是会得到相同的输出

  • 在执行过程中没有语义上可观察的副作用。

什么是副作用

如果一个函数除了计算之外,还对它的执行上下文、执行宿主等外部环境造成了一些其它的影响,那么这些影响就是所谓的"副作用"。

javascript 复制代码
// 栗子 1
let a = 10
let b = 20
function add() {
  return a+b
}


// 栗子 2 
function processName(firstName, secondName) {
  const fullName = `${firstName}·${secondName}`
  console.log(`我是 ${fullName}`)
  return fullName
}
processName('蔡', '徐坤')


// 栗子 3
function getData(url) {
  const response = await fetch(url)
  const { data } = response   
  return data
}

纯函数:输入只能够以参数形式传入,输出只能够以返回值形式传递,除了入参和返回值之外,不以任何其它形式和外界进行数据交换的函数。

为何非纯不可

  1. 高度的确定性。不确定性意味着风险,而风险是万恶之源。

以测试过程为例:单元测试的主要判断的依据就是函数的输入和输出。如果对于同样的输入,函数不能够给到确定的输出,测试的难度将会陡然上升。不确定性也会导致我们的代码难以被调试、数据变化难以被追溯、计算结果难以被复用等等。

  1. 没有副作用

因为不纯的函数有可能访问同一块资源,进而相互影响,引发意想不到的混乱结果。试想这样一种场景,A函数和B函数都需要向某个文件写入信息,一旦我们先后调用了A、B两个函数,就将触发两个并行的写入过程,进入混乱的竞争态。

  1. 更加灵活,可以改善代码质量

无论是引入了外部变量的 add()函数,还是依赖了 JS 内置对象的 getToday() 函数,它们的执行都严重地依赖了函数的运行环境。更确切地说,这些函数是被困在了特定的上下文里。

从研发效率上来看,纯函数的实践,实际上是将程序的"外部影响"和"内部计算"解耦了。

这间接地促成了程序逻辑的分层,将会使得模块的功能更加内聚。

函数组合思想

因为 DRY 所以 HOF

数组方法map()reduce()filter() ...

在 JS 中,基于 reduce(),我们不仅能够推导出其它数组方法,更能够推导出经典的函数组合过程。

csharp 复制代码
const arr = [1, 2, 3]

// 0 + 1 + 2 + 3 
const initialValue = 0  
const add = (previousValue, currentValue) => previousValue + currentValue
const sumArr = arr.reduce(
  add,
  initialValue
)

console.log(sumArr)
// expected output: 6

用 reduce 推导 map

Reduce 中的函数组合思想

通过观察 reduce的工作流,我们可以发现这样两个特征:

  • reduce 的回调函数在做参数组合
  • reduce 过程构建了一个函数 pipeline

用 reduce 推导 pipe

JS 的函数可以作为参数传递

callback(0, func1) = func1(0)

scss 复制代码
function callback(input, func) {
  func(input)
}  

funcs.reduce(callback,0)


=======

function pipe(funcs) {
  function callback(input, func) {
    return func(input)
  }  

  return function(param) {
    return funcs.reduce(callback,param)
  }
}
javascript 复制代码
function add4(num) {
  return num + 4
}  

function multiply3(num) {
  return num*3
}  

function divide2(num) {
  return num/2
}

const compute = pipe([add4, multiply3, divide2])

// 输出 21
console.log(compute(10))

在 React 中的体现

  • 计算层:负责根据 state 的变化计算出虚拟 DOM 信息。这是一层较纯的计算逻辑。

  • 副作用层:根据计算层的结果,将变化应用到真实 DOM 上。这是一层绝对不纯的副作用逻辑。

UI = f(data) 这个公式中,数据是自变量,视图是因变量。

组件 作为 React 的核心工作单元,其作用正是描述数据和视图之间的关系

也就是说,若是把这个公式代入到微观的组件世界中去,那么 React 组件毫无疑问对应的就是公式中的 f() 函数。

对于同样的入参(也即固定的 props context state ),函数组件总是能给到相同的输出。因此,函数组件仍然可以被视作是一个"纯函数"。

由此我们可以看出:Hook 对函数能力的拓展,并不影响函数本身的性质。函数组件始终都是从数据到 UI 的映射,是一层很纯的东西 。而以 useEffectuseState 为代表的 Hooks,则负责消化那些不纯的逻辑。比如状态的变化,比如网络请求、DOM 操作等副作用。

也就是说,在组件设计的层面,React 也在引导我们朝着"纯函数/副作用"这个方向去思考问题

现在,设计一个函数组件,我们关注点则被简化为"哪些逻辑可以被抽象为纯函数,哪些逻辑可以被抽象为副作用"

资料推荐

柯里化、偏函数、函子、单子......

函数式编程初探 - 阮一峰的网络日志

深入理解函数式编程(上)

深入理解函数式编程(下)

相关推荐
Oberon10 天前
从零开始的函数式编程(2) —— Church Boolean 编码
数学·函数式编程·λ演算
桦说编程19 天前
CompletableFuture 超时功能有大坑!使用不当直接生产事故!
java·性能优化·函数式编程·并发编程
桦说编程23 天前
如何安全发布 CompletableFuture ?Java9新增方法分析
java·性能优化·函数式编程·并发编程
桦说编程1 个月前
【异步编程实战】如何实现超时功能(以CompletableFuture为例)
java·性能优化·函数式编程·并发编程
鱼樱前端1 个月前
Vue3之ref 实现源码深度解读
vue.js·前端框架·函数式编程
RJiazhen2 个月前
前端项目中的函数式编程初步实践
前端·函数式编程
再思即可3 个月前
sicp每日一题[2.77]
算法·lisp·函数式编程·sicp·scheme
桦说编程3 个月前
把 CompletableFuture 当做 monad 使用的潜在问题与改进
后端·设计模式·函数式编程
蜗牛快跑2133 个月前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
大福是小强4 个月前
002-Kotlin界面开发之Kotlin旋风之旅
kotlin·函数式编程·lambda·语法·运算符重载·扩展函数