Skip to content

Functional Programming

函数式编程概念

Functional Programming in JavaScript

Eric Elliott

  • Functional programming is the process of building software by composing pure functions , avoiding shared state , mutable data , and side-effects. Functional programming is declarative rather than imperative , and application state flows through pure functions.
  • Contrast with object oriented programming, where application state is usually shared and colocated with methods in objects.
  • 函数式编程是通过 组合纯函数、避免 共享状态可变数据副作用 来构建软件的过程。函数式编程是 声明性的而不是命令性的 ,应用程序状态通过纯函数流动。
  • 相反,在面向对象编程中,应用程序状态通常由于对象中的方法来共享和协作。

关键概念

  • Avoid side effects/避免副作用
  • Avoid mutating state/避免改变状态
  • Avoid shared state/避免共享状态
  • Pure functions/纯函数
  • Function composition/函数组合
  • Declarative rather than imperative/声明式而非命令式

对比OOP

  • 面向对象优点:对象的概念容易理解,方法调用灵活。
  • 面向对象缺点:对象可在多个函数中共享状态、被修改,极有可能会产生“竞争”的情况(多处修改同一对象)。
  • 函数式优点:避免变量的共享、修改,纯函数不产生副作用;声明式代码风格更易阅读,更易代码重组、复用。
  • 函数式缺点:过度抽象,学习难度更大。

避免副作用,使用纯函数

Avoid Side Effects and Using Pure Functions

例子

  • 副作用函数 vs 纯函数
// 副作用函数
let count = 1;
let incrememt = function() {
 count++;
 return count;
};


// 纯函数
let incrememt = function(count) {
 count++;
 return count;
};

纯函数(Pure Functions)

  • 定义:当时函数被调用,它不会修改函数范围(Scope/作用域)外的任何东西。
  • 特性1:该函数依赖于所提供的输入,而不是依赖于外部数据的变化。
  • 特性2:该函数不引发副作用,不会引起超出其范围的变化。
  • 特性3:给定相同的输入,函数总是返回相同的输出。

副作用(Side Effects)

  • 改变一个全局值(变量、属性、数据结构)
  • 改变函数参数的原始值
  • 抛出异常(Throwing an exception)
  • 打印控制台日志(Logging to the console)
  • 写到屏幕或记录日志(Writing to the screen)
  • 写到文件(Writing to a file)
  • 写到网络(Writing to the network)
  • 触发外部进程
  • 调用一个有副作用的函数

副作用能消除吗

  • 不能,相反,副作用必须发生。
  • 我们需要副作用发生来达成目标,但滥用副作用导致问题。
  • 函数式编程的目标只是为了更好的管理副作用,减少不必要的发生。
  • 管理方法:将 有副作用的代码 集中隔离;无副作用的部分 被依赖,使代码更易测试、可推理、可阅读。
  • 「变化是一只🐱,把猫关在笼子里」。

避免共享状态

Avoid Shared State

状态(State)

  • A program is considered stateful if it is designed to remember data from events or user interactions.
  • 如果程序旨在记住来自事件或用户交互的数据,则该程序被认为是 有状态的
  • 被记住的信息,称为程序的状态。

共享状态

  • Shared state is any variable, object, or memory space that exists in a shared scope, or as the property of an object being passed between scopes. A shared scope can include global scope or closure scopes. -- Eric Elliott
  • 共享状态 是存在于共享作用域中的任何变量、对象或内存空间,或作为作用域之间传递的对象的属性。共享作用域可以包括全局作用域或闭包作用域。
  • In OOP, shared states are often passed around as objects.
  • A shared state prevents a function from being pure.

避免改变状态

Avoid Mutable Data

可变的(Mutable)

  • An immutable object is an object that can’t be modified after it’s created. Conversely, a mutable object is any object which can be modified after it’s created.
  • 不可变对象是在创建后无法修改的对象。相反,可变对象是在创建后可以修改的任何对象。

不可变性(Immutability)

  • ES6 的 const 修饰符

    • 无法被重新赋值
    • 但对象的属性可变
  • Object.freeze()

    • 能 freeze 对象的顶层属性

    'use strict';
    const a = Object.freeze({  foo: 'Hello',  bar: 'world',  baz: '!' });
    
    a.foo = 'Goodbye';
    // Error: Cannot assign to read only property 'foo' of object Object
    

    • 但无法 freeze 更深层次的属性依旧可变

    'use strict';
    
    const a = Object.freeze({
      foo: { greeting: 'Hello' },
      bar: 'world',
      baz: '!'
    });
    
    a.foo.greeting = 'Goodbye';
    
    console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);
    

  • 数据结构 trie

    • 真正的 deep frozen
    • Immutable.jsMori 都使用 tries

    // 使用 immutable.js 后
    import Immutable from 'immutable';
    
    const foo = Immutable.fromJS({ a: { b: 1 } });
    const bar = foo.setIn(['a', 'b'], 2);  // 使用 setIn 赋值
    
    console.log(foo.getIn(['a', 'b'])); // 使用 getIn 取值,打印 1
    console.log(foo === bar); // 打印 false
    

    • 算法思想:如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享

    z

克隆对象

  • Object.assign()
  • JSON.parse(JSON.stringify(obj))

使用 Reduce, Map 和 Filter

函数组合

Function composition

一个例子

  • 题设:string -> trims 空格 -> splits 分词 -> check 长度 -> Boolean
  • 解法:check(splits(trims(str)))

function trims(str) {
  console.log('trims');
  return str.replace(/\s/g, ' ');
}

function splits(str) {
  console.log('splits');
  return str.split('');
}

function check(arr) {
  console.log('check');
  return arr.length >= 3;
}

check(splits(trims('A C ')))

  • 问题:嵌套多、执行顺序与阅读顺序相反;难以阅读、难以增加函数。
  • 这代码咋读:var a = b => c => d => "Wat?!";

Kyle Simpson

  • 「You Don't Know JS」的作者
  • ...readability is not just about fewer characters. Readability is actually most impacted by familiarity.
  • 可读性不仅仅是更精简的代码。 可读性实际上更多与熟悉、熟练相关。
  • ...A functional programmer sees every function in their program like a simple little Lego piece. They recognize the blue 2x2 brick at a glance, and know exactly how it works and what they can do with it. When they begin building a bigger, more complex Lego model, as they need each next piece, they already have an instinct for which of their many spare pieces to grab.
  • 函数式程序员将程序中的每个函数都视为一个简单的乐高积木。他们一眼就能认出这块蓝色的2x2砖块,并确切地知道它是如何工作的,以及他们可以用它做什么。当他们开始建造一个更大、更复杂的乐高模型时,因为他们需要下一块积木,他们已经 有了一种本能,知道他们的许多备用积木中的哪一块。

compose 函数

function compose(...fns) {
  return function(value) {
    return fns.reduceRight((acc, fn) => {
      return fn(acc)
    }, value)
  }
}

// 测试
compose(check, splits, trims)('A C ')

pipe 函数

function pipe(...fns) {
  return function(value) {
    return fns.reduce((acc, fn) => {
      return fn(acc)
    }, value)
  }
}

pipe(trims, splits, check)('A C ')

科里化

Curry Function

思考题

  • 多参数的 compose 函数怎么写?
  • 提示:使用 bind 方法。(partial function)

function addition(x, y) {
   return x + y;
}
const plus5 = addition.bind(null, 5)
plus5(10) // output -> 15

  • 引出核心问题:Arity and How it Affects Compostion.

概念

  • 参数个数(Arity)
  • 科里化是 Arity 大于 1 的函数,变成多个 Arity 为 1 的函数,利用高阶函数和闭包。
  • 单个参数的函数,称为「单元函数(unary function)」。

例子

  • 多参数函数

function f1(a, b, c) {
  return a + b + c;
}

function f2(d, e) {
  return d + e;
}

function f3(f, g, h) {
  return f + g + h;
}

  • 科里化函数

function curry(fn, arity = fn.length) {
  return function curried(...prevArgs) {
    // 收集完参数退出
    if (prevArgs.length >= arity) {
      return fn(...prevArgs);
    }

    // 递归收集
    return function(nextArg) {
      return curried(...prevArgs, nextArg);
    }
  }
}

  • 组合单元函数

const newFn = pipe(
  curry(f1)(1)(2),
  curry(f2)(4),
  curry(f3)(5)(6),
)

newFn(3);

偏应用(Partial Application )

  • 概念

    • A partial application is a function which has been applied to some of its arguments, but not yet all of its argumens.
    • 偏应用是一个函数,它应用于 某些参数,而非所有参数。
    • In other words, it‘s a function which has some arguments fixed inside its closure scope.
    • 换句话说,它是一个函数,在其闭包范围内固定了某些参数。
  • 偏应用(Partial Application ) vs 科里化函数(Curried Function)

    • Partial applications can take as many or as few arguments a time as desired.
    • 偏应用一次可以根据需要使用多个或少个参数。
    • Curried functions on the other hand always return a unary function:a function which takes one argument.
    • Curried函数总是返回一元函数:接受一个参数的函数。

科里化优点

  • 1、Currying can be used to specialize functions. 科里化能用于编写专业函数。比如, addUserAge10、movePointBy20。

const getUsersUser = pipe(
    curry(getUser)(users),
    cloneObject,
);

const getHery = function() {
  return getUsersUser('Hery');
};

  • 2.Currying simplifies function composition.科里化更易于完成函数组合。

声明式而非命令式

Declarative rather than imperative

关键概念

  • 命令式编程(Imperative Programming)

    • Imperative Programming is a programming style that tells the computer "how" to accomplish some task.
    • 命令式编程是一种编程风格,它告诉计算机“如何”完成某些任务。
  • 声明式编程(Declarative Programming)

    • Declarative Programming expresses the logic of a program without identifying the control flow. Control flow is abstracted away, so declarative code only needs to specify "what" to do.
    • 声明式编程在不识别控制流的情况下表达程序的逻辑。控制流被抽象出来了,所以声明式代码只需要指定 “做什么”。
    • if条件、循环语句减少,不必知道“如何去做”,声明式代码更容易推理、可预测、可复用。
  • 编程范式

z

命令式 vs 声明式

  • 如图

z

  • 看看代码 Code

    • Code 1

    let state = {
      foreground: '#999999',
      background: '#FFFFFF'
    };
    
    const imperativeMakeBackgroundBlack = () => {
      state.background = '#000000';
    };
    // directly changes the state object outside of its internal scope
    
    const declarativeMakeBackgroundBlack = state => ({...state, background: '#000000'});
    // takes current state as its input and returns new state with changed value
    // without changing the original state
    

    • Code 2

    let turtles = ['Galápagos tortoise', 'Greek Tortoise'];
    
    const imperativeAddTurtle = turtle => turtles.push(turtle);
    // changes the turtles external array and returns the length of the new array
    
    const declarativeAddTurtle = turtles => turtle => [...turtles, turtle];
    // takes 'array of turtles' and the 'new turtle' as its input.
    // It returns new array of turtles without changing the original array
    

  • Imperative code directly accesses the state and changes it and declarative expressions never change the external state.

  • 命令式代码直接访问状态并更改它,声明式表达式从不更改外部状态。
  • 声明式更专注于 function composition
  • 声明式以「intention revealing name」的方式编写函数代码,更容易阅读、推理、和理解。

工具库 Lodash & Ramda

Lodash

Ramda

  • Ramda emphasizes a purer functional style. Immutability and side-effect free functions are at the heart of its design philosophy. This can help you get the job done with simple, elegant code.
  • 1、Ramda 强调更加纯粹的函数式风格。数据不变性和函数无副作用是其核心设计理念。这可以帮助你使用简洁、优雅的代码来完成工作。
  • Ramda functions are automatically curried. This allows you to easily build up new functions from old ones simply by not supplying the final parameters.
  • 2、Ramda 函数本身都是自动柯里化的。这可以让你在只提供部分参数的情况下,轻松地在已有函数的基础上创建新函数。
  • The parameters to Ramda functions are arranged to make it convenient for currying. The data to be operated on is generally supplied last.
  • 3、Ramda 函数参数的排列顺序更便于柯里化。要操作的数据通常在最后面。
  • 最后两点一起,使得将多个函数构建为简单的函数序列变得非常容易,每个函数对数据进行变换并将结果传递给下一个函数。Ramda 的设计能很好地支持这种风格的编程。

RxJS

ReactiveX

  • 相关概念

x

RxJS

  • 概念及定义:RxJS is a functional reactive programming library.

x

  • 关键词:functional reactive programming异步
  • 核心概念

x

  • 样例代码

x

References