函数式编程初探

什么是函数式编程

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

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

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

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

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

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

  • 我想要什么样的输出?
  • 我应该提供什么样的输入?
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 也在引导我们朝着"纯函数/副作用"这个方向去思考问题

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

资料推荐

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

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

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

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

相关推荐
大福是小强7 天前
002-Kotlin界面开发之Kotlin旋风之旅
kotlin·函数式编程·lambda·语法·运算符重载·扩展函数
再思即可9 天前
sicp每日一题[2.63-2.64]
算法·lisp·函数式编程·sicp·scheme
老章学编程i19 天前
Java函数式编程
java·开发语言·函数式编程·1024程序员节·lanmbda
安冬的码畜日常1 个月前
【玩转 JS 函数式编程_014】4.1 JavaScript 纯函数的相关概念(下):纯函数的优势
开发语言·javascript·ecmascript·函数式编程·js·functional·原生js
Dylanioucn1 个月前
【编程进阶知识】Java 8 函数式编程接口全解析:Supplier、Runnable、Function、Consumer、Apply
java·开发语言·函数式编程
矢心1 个月前
函数式编程---js的链式调用理解与实现方法
前端·javascript·函数式编程
安冬的码畜日常1 个月前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
请不要叫我菜鸡1 个月前
mit6824-01-MapReduce详解
大数据·分布式·后端·mapreduce·函数式编程·容错性
安冬的码畜日常1 个月前
【玩转 JS 函数式编程_004】1.4 如何应对 JavaScript 的不同版本
开发语言·前端·javascript·ecmascript·函数式编程·fp·functional
再思即可2 个月前
sicp每日一题[2.31]
编程·lisp·函数式编程·sicp·scheme