浅析函数式编程与前端

简介: 浅析函数式编程与前端

什么是函数式编程


我们常见的编程范式有两种:命令式和声明式,比如我们熟悉的面向对象思想就属于命令式,而函数式编程属于声明式。而且顺带说一句,函数式编程里面提到的“函数”不是我们理解的编程中的“function”概念,而是数学中的函数,即变量之间的映射。

那么,函数式编程和我们熟知的声明式编程区别是什么?引用知乎用户的一句话来做总结:函数式编程关心数据的映射,命令式编程关心解决问题的步骤

举个例子,如果编写一个函数来实现把数组的每个数字都变成它本身的2倍,命令式编程的思路应该是:遍历一次数组,并且把每个数字乘以2,代码如下:

const solution = (arr) => {
  const newArr = [];
  for(let i = 0;i < arr.length;i++) {
    newArr.push(arr[i]*2);
  }
  return newArr;
}
复制代码

但是如果从函数式编程的思维去思考,无非就是数组A的每个元素是数组B每个元素的两倍,存在一个映射:[a, b, c, d, ...] => [2a, 2b, 2c, 2d, ...],代码如下:

const solution = (arr) => {
  return arr.map(item => {
    return item*2;
  })
}
复制代码

从上面这两个简单的例子可以看出来,函数式编程与命令式编程的思路最大的不同在于:函数式更关心数据的映射。

在前端开发领域中,有很多函数式的使用,比如React框架,它本身的设计理念就是View = Fn(Data),而且还有函数式组件以及高阶组件等等,无一不都透露着对函数式编程的实践。


纯函数


纯函数是函数式编程中一个很重要的概念,它的定义是:纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,且不依赖外部环境。

比如说对于数组的方法slice和splice来说(例子来自于函数式编程指北):

var xs = [1,2,3,4,5];
// 纯的
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
// 不纯的
xs.splice(0,3);
//=> [1,2,3]
xs.splice(0,3);
//=> [4,5]
xs.splice(0,3);
//=> []
复制代码

可以从例子里看出,对于slice来说,它对于相同的输入总能返回相同的输出;而splice直接在原数组上作出改变,产生了可观察到的副作用,即改变了数组。

再来一个例子(例子来自函数式编程指北):

// 不纯的
var minimum = 21;
var checkAge = function(age) {
  return age >= minimum;
};
// 纯的
var checkAge = function(age) {
  var minimum = 21;
  return age >= minimum;
};
复制代码

很明显,在不纯的版本中,checkAge的返回结果直接依赖于外部的变量minimum,这样子就做不到对于相同的输入总是返回相同的输出了,因为只要外部环境的变量一变,直接影响到函数的输出。


副作用


前面一直在说副作用,到底什么是副作用?哪些影响可以被算作是副作用?

副作用的定义是:副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。

副作用包含但不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态

可以看出来,只要是跟外部环境发生了交互的行为都会带来副作用,与外部环境交互了就可能会影响到函数的输出,函数式编程之所以这么在乎副作用,就是因为函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。


优势


纯函数因为它的纯和没有副作用,可以带来以下几个好处


可预测性


因为纯函数没有副作用,对于同一个输入来说,我们可以预测到它的输出,这对于前端常见的共享状态以及修改很有用。

再看看Redux,在它的官方文档里面提到:

To specify how the state tree is transformed by actions, you write pure reducers. Reducers are just pure functions that take the previous state and an action, and return the next > state. Remember to return new state objects, instead of mutating the previous state.

很明显,Redux要求你使用纯函数的方式来写reducer,可预测、无副作用的纯函数正中下怀。


可缓存性


为对于纯函数,固定的输入可以得到固定的输出,所以可利用这一点来做缓存。

例如(例子来自函数式编程指北):

var memoize = function(f) {
  var cache = {};
  return function() {
    var arg_str = JSON.stringify(arguments);
    cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
    return cache[arg_str];
  };
};
var squareNumber  = memoize(function(x){ return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // 从缓存中读取输入值为 4 的结果
//=> 16
squareNumber(5);
//=> 25
squareNumber(5); // 从缓存中读取输入值为 5 的结果
//=> 25
复制代码


柯里化


柯里化(curry)的理念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

举个例子(例子来自函数式编程指北):

var add = function(x) {
  return function(y) {
    return x + y;
  };
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
复制代码

上面这个例子定义了一个add函数,它接受参数x并且返回一个函数,调用add函数之后,返回的函数就以闭包的形式记住了第一个参数x。

柯里化在日常编程中的使用主要体现在“预加载函数”,即提前缓存一部分参数,以便在未来使用。例如在vue源码中,就有一个柯里化的经典使用:

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    //...
  }
}
复制代码

因为vue需要支持web和weex,而且patch操作里面的逻辑在不同的平台上基本相同,只是把虚拟DOM映射到真实元素的操作方法不一样,所以提前把相关的API传入createPatchFunction,返回一个固化好平台操作元素API的patch方法,以后使用的时候只需要传入oldVnode和vnode等等参数就好了,这种利用柯里化的技巧值得学习。


组合


考虑一下存在如下的方法:

var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = function(x){
  return exclaim(toUpperCase(x)); // 不优雅
};
shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
复制代码

这种方法里面套方法的做法,看起来略难受且不优雅,如果使用组合的方式,可以修改成下面这样:

var shout = compose(exclaim, toUpperCase);
shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
复制代码

组合式的写法可以让我们在开发中用拼装来代替直接封装,使代码更加优雅,比如Redux中的compose方法:

Composes functions from right to left.

This is a functional programming utility, and is included in Redux as a convenience. You might want to use it to apply several store enhancers in a row.

Arguments

(arguments): The functions to compose. Each function is expected to accept a single parameter. Its return value will be provided as an argument to the function standing to the left, and so on. The exception is the right-most argument which can accept multiple parameters, as it will provide the signature for the resulting composed function.

Returns

(Function): The final function obtained by composing the given functions from right to left.

import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import DevTools from './containers/DevTools'
import reducer from '../reducers'
const store = createStore(
  reducer,
  compose(applyMiddleware(thunk), DevTools.instrument())
)
复制代码


没有银弹


本文只是对函数式最基本的一些特性的学习和介绍,以及在前端开发的实践。如果想深入学习函数式编程,比如Monad之类的,可以看一下参考链接,更上一层楼。

软件开发领域没有银弹,没有一种编程范式或者框架放之四海而皆准。对于函数式编程也是如此,取其精华去其糟粕,防止拿着锤子看什么都像是钉子。


参考

函数式编程指南

编程的宗派

什么是函数式编程思维?

前端开发js函数式编程真实用途体现在哪里?

有哪些函数式编程在前端的实践经验?

JavaScript函数式编程

相关文章
|
程序员 Swift 开发者
26 函数式编程
函数式编程
63 0
|
1月前
|
机器学习/深度学习 数据采集 人工智能
函数式编程的实际应用
【10月更文挑战第12天】 函数式编程作为一种编程范式,在数据处理、金融、科学计算、Web 开发、游戏开发、物联网、人工智能等多个领域有着广泛应用。本文通过具体案例,详细介绍了函数式编程在这些领域的实际应用,展示了其在提高效率、确保准确性、增强可维护性等方面的显著优势。
112 60
|
12天前
|
数据采集 并行计算 算法
函数式编程
函数式编程是一种编程范式,它将计算视为数学函数的求值,并避免改变状态和可变数据。其核心思想是使用纯函数,减少副作用,提高代码的可读性和并行处理能力。
|
22天前
|
SQL 前端开发 测试技术
对函数式编程的深入理解
【10月更文挑战第25天】函数式编程提供了一种不同的编程思维方式,具有诸多优点,如提高代码质量、便于并发和并行编程、易于测试等。然而,它也存在一些局限性,需要根据具体的项目需求和场景来选择是否采用。随着对函数式编程的理解和应用的深入,它在现代软件开发中扮演着越来越重要的角色,为开发者提供了更多的编程选择和可能性。
10 1
|
5月前
|
存储 算法 编译器
C++ 函数式编程教程
C++ 函数式编程学习
|
6月前
|
Java 程序员 C#
Lambda表达式:简洁而强大的函数式编程利器
【4月更文挑战第3天】本文探讨了Lambda表达式的基础和在编程中的应用,包括简化代码和提高可读性。Lambda表达式是匿名函数,用于简单的功能,如示例中的平方运算和列表筛选。通过`map`、`filter`等函数,Lambda表达式能有效处理列表操作。文中还展示了Lambda表达式的高级用法,如闭包特性、异常处理及与高阶函数的结合。通过实例,读者可以学习如何利用Lambda表达式实现更高效、简洁的编程。
63 0
|
设计模式 前端开发 JavaScript
前端函数式编程浅析
函数式编程的概念与应用
92 0
|
算法 Java Scala
函数式编程基础介绍|学习笔记
快速学习函数式编程基础介绍。
函数式编程基础介绍|学习笔记
|
存储 SQL 分布式计算
深入理解函数式编程
深入理解函数式编程
深入理解函数式编程
|
设计模式 缓存 前端开发
前端开发函数式编程入门
函数式编程是一门古老的技术,从上个世纪60年代Lisp语言诞生开始,各种方言层出不穷。各种方言带来欣欣向荣的生态的同时,也给兼容性带来很大麻烦。于是更种标准化工作也在不断根据现有的实现去整理,比如Lisp就定义了Common Lisp规范,但是一大分支scheme是独立的分支。另一种函数式语言ML,后来也标准化成Standard ML,但也拦不住另一门方言ocaml。后来的实践干脆成立一个委员会,定义一个通用的函数式编程语言,这就是Haskell。后来Haskell被函数式原教旨主义者认为是纯函数式语言,而Lisp, ML系都有不符合纯函数式的地方。
前端开发函数式编程入门
下一篇
无影云桌面