函数式编程 入门指南

# 函数式编程 简介

# 什么是 函数式编程

函数式编程,Functional Programming,简称 FP,
是一种编程范式。

它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ 演算(lambda calculus)为该语言最重要的基础。而且,λ 演算的函数可以接受函数当作输入(引数)和输出(传出值)。

比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

它涉及很多数学上的学科:

  • 抽象代数
  • 群论
  • 范畴论

但 FP 和 OOP 的关系也不是高低贵贱之分,而是风格差异。
这些思想不是教条,而是前人为了写出更好的代码进行的思考和总结。
在学习 FP 的过程中也一定会碰到和 OOP 在设计模式上的对比。

在实际业务中,经常会不得不和副作用打交道。
(网络请求,用户事件,I/O 等)

前端中 JS 是基本语言,JS 是一个多范式的编程语言,
初学 FP,可以先保持原有的编码习惯,并从计算层逐渐切入。

也没有必要教条地遵循 FP 的开发模式,
如:完全不使用 if/for、只用 const 等新手 FP 装逼行为。
因为从编程语言设计的层面来说(如编译优化),JS 不完全适用于纯 FP 模式。

# 代码风格

函数式编程和其他范式之间的风格差异
在 JavaScript 中:

# 函数式 vs 面向对象

// FP,函数、传参
grow(circle, 3);

// * OOP,对象、方法
circle.grow(3);

# 函数式 vs 指令式

找出大于 35 岁的程序员的名字

const people = [
  { name: 'Bob Martin', age: 68 },
  { name: 'Dan Abramov', age: 27 },
  { name: 'Joel Spolsky', age: 55 },
];
// => [ 'Bob Martin', 'Joel Spolsky' ]

函数式:

// * 拆分成子任务,纯函数、函子、无状态
const result = people
  .filter((p) => p.age > 35)
  .map((p) => p.name);

指令式:

// * 手动处理运算过程,循环结构、外部变量
const result = [];
for (let i = 0; i < people.length; i++) {
  const p = people[i];
  if (p.age > 35) result.push(p.name);
}

# 为什么要学习 函数式编程

  • 当你学习一些新兴前端工具,你需要了解基本的 FP
  • FP 的优势
    • 代码可靠性
    • 易于测试
    • 组合开发
  • 注重 FP 的编程语言
    • Haskell
    • F#
    • Clojure(from Lisp)
    • ClojureScript(from JavaScript)

# 学习 函数式编程

# 概览

  • 耗时:
    • 从入门到理解基本概念,大约 10~20 小时
    • Functional JavaScript 代码训练,大约 4~12 小时
    • 系统学习函数式架构和模式,至少 80 小时
  • 难点:
    • 新的编程思维方式
    • 数学和 FP 高等概念
  • 工具:

# 学习路线

  • 前置学习
  • 学习函数式编程
    • 观看资料中的视频部分,理解函数式编程的核心原则
    • 练习 JS 中自带的 map/filter/reducer
    • 学习使用包含轻度 FP 思想的库(Ramda,Redux 等)
  • 实战
    • 在业务中编写纯函数,处理副作用
    • 将已有的逻辑以 FP 方式重新实现
  • 进阶
    • 系统地 FP 概念代码训练(如 Monad 等)
    • 学习 Haskell/F#/Clojure
    • 学习 FP 架构设计和设计模式
  • 迷思
    • 如何更好地管理副作用
    • 专为 FP 设计的语言有相应优化(如内置 Immutable),如何处理 JS 中的 FP 性能

# 资料

# 代码训练

# GitHub

# 视频

  • Object Oriented vs Functional Programming with TypeScript:12 分钟,OOP vs FP
    • 1: 26 - FP:纯函数、副作用、Immutable、函数作为参数/返回值、高阶函数、无状态
    • 4: 19 - OOP:类、构造函数、公有/私有、getter/setter、有状态、方法
    • 7: 39 - 继承 vs 组合
    • 10: 03 - Mixin
  • Computerphile 频道
  • 讲座
    • GOTO 2018 • Functional Programming in 40 Minutes • Russ Olsen:42 分钟
      • 0: 00 - 介绍,产业历史背景,概述
      • 2: 55 - 什么是 FP,FP 不是黑魔法,OOP 概览和可能存在的局限性
      • 8: 29 - 什么是编程,不同编程语言中的概念
      • 10: 06 - FP 和 OOP 中相同的部分,FP 更相当于是一种组织代码的理念
      • 11: 17 - 计科和数学碰到的相同困境:如何组织系统、如何做抽象?
      • 13: 48 - 函数,集合,输入输出,map,纯函数,副作用
      • 17: 41 - 为什么 FP 使程序更易于编写和理解,新的问题和解决方案,Immutable,可持久化数据结构
      • 26: 03 - 如何处理副作用
      • 33: 22 - FP 不是万能的,它不能解决人工错误(如边界错误),线程安全
      • 37: 43 - 一个 Clojure 项目中的 FP 使用情况
    • Why Isn't Functional Programming the Norm? – Richard Feldman:46 分钟
      • 0: 00 - 概述,语言、范式、风格
      • 0: 59 - 现在流行的编程语言,流行的几个原因:
        • 2: 05 - 独角兽应用:VisiCalc、Ruby、PHP、C
        • 6: 22 - 平台独占:Objective-C、swift、JavaScript、C#
        • 10: 22 - 快速替换:优势、熟悉程度、学习曲线、生态系统、代码迁移成本,CoffeeScript、TypeScript、C++、Kotlin
        • 13: 14 - 商业营销:Java 的故事
        • 16: 15 - 稳步增长:Python
        • 17: 45 - 其他原因:语法、产业规模、社区
      • 18: 46 - 范式,为什么几乎所有主流语言都是 OOP
        • 19: 37 - OO 具有独特特性?封装、继承、对象、方法?
          • 21: 13 - Go 支持 OO 但是不支持继承,对象和方法只是结构体和过程的语法糖
          • 23: 05 - 模块化编程 vs OO 封装,几乎所有语言都支持模块化
        • 24: 37 - OO 演进史:ALGOL、simula、Smalltalk、ObjC、C++
        • 32: 34 - 语言的流行基于很多别的原因,而不是 OO 特性的先进性
      • 35: 47 - 风格,FP 是语言特性无关的
        • 36: 30 - 为什么 FP 不是主流,其实很多 OO 语言已经支持 FP,事情正在起变化
      • 39: 58 - 小结 & 问答
        • 42: 04 - FP 没有精确的定义
        • 44: 00 - 性能需求应根据使用场景进行权衡
    • Learning Functional Programming with JavaScript - Anjana Vakil - JSUnconf:30 分钟
      • 0: 00 - 开场,在 JS 中使用 FP 的经历
      • 1: 50 - 什么是 FP,Why FP JS,JS 中的 this 陷阱
      • 4: 42 - 纯函数,副作用,HOC,Map/Reduce/Filter,Immutable
      • 16: 12 - 可持久化数据(Persistent Data),结构共享
      • 22: 27 - 结语 & 提问
        • 24: 54 - 编程范式之争
        • 28: 10 - Map
      • 提到的资料:
    • Anjana Vakil: Immutable data structures for functional JS | JSConf EU:26 分钟
      • 0: 00 - 开场,FP JS 与不可变数据,个人介绍
      • 2: 00 - FP 和 Immutable 简介,结构共享,Trie(字典树)
      • 11: 52 - 二进制化,Bitmapped Bector Trie,Hash Array Mapped Trie
      • 18: 42 - 小结,JS 库介绍:Mori、Immutable
    • Scenic City Summit 2016: Jeremy Fairbank - Functional Programming Basics in ES6 (JavaScript):58 分钟
      • 0: 00 - 开场,FP in ES6,个人介绍
      • 0: 55 - JS 中的 FP 库,什么是 FP,数学中的函数,Lambda 演算
      • 4: 24 - 四个原则:纯函数,声明式;安全性,Immutable;透明性,状态;模块化,组合开发
      • 6: 03 - ES6 简介:const,箭头函数、剩余参数、默认参数,解构,Object.assign,class
      • 12: 56 - 纯函数 vs 非纯函数,副作用,引用透明,非纯函数难以测试,隐藏的状态就是不确定的状态
      • 17: 11 - 指令式 vs 声明式,
      • 19: 49 - Immutable,避免 Immutable 的技术方法,Object.freeze
        • 优势:数据安全、UNDO/REDO、显式数据流、内存使用、并发安全
        • 劣势:代码冗余,更多对象创建、更多垃圾回收、内存使用
      • 26: 28 - 函数一等公民,别名,作为参数,闭包,作为返回值,HOC
      • 31: 16 - 偏应用,partial,柯里化,闭包(数学),compose
      • 45: 22 - 递归,Stack Overflow,尾调用优化
      • 56: 00:资源
        • 书:Mostly Adequate Guide
        • 语言:Elm、ClojureScript、PureScript(emmm…这是 2016 年的视频…so…)
        • 库:Lodash、Ramda、RxJS、Bacon.js、Immutable.js
        • 框架:React、Redux
    • Functional programming design patterns by Scott Wlaschin:65 分钟
      • 0: 00 - 前言,FP 设计模式,写 OO 和 FP 的经历,F#
      • 3: 43 - OO 模式/原则
      • 4: 50 - FP 模式:FP 核心原则、函数作为参数、Monad、Map、Monoid
      • 6: 04 - FP 核心原则:函数是实体、组合、类型/而不是类。组合是分形的,类型是可组合的,完整度(Totality),静态类型
      • 18: 16 - 函数作为参数:DIY 原则,函数类型作为接口,策略模式,装饰器,单参数
      • 27: 49 - 偏应用,依赖注入
      • 32: 46 - 好莱坞原则/Continuous,利用偏函数将错误处理外置化
      • 36: 10 - 回调函数,链式化
      • 38: 42 - Monad,Chaining Continuous
      • 41: 35 - Bind,Monadic Bind,Bind 错误处理模式
      • 44: 52 - Map,Option,Lift,Functor
      • 49: 30 - Monoid,数学性质:闭包(数学);结合律,并行化;单位元,半群,MapReduce,Homomorphism,Endomorphism
      • 64: 01 - Monad and Monoid
    • Functional architecture - The pits of success - Mark Seemann:60 分钟
    • Brian Lonsdorf - Oh Composable World!:28 分钟
      • 0: 40 - 组合编程,chain、pipeline、控制流
      • 3: 51 - 编程中的反数学范式,范畴论,组合
        • 7: 09 - 声明:Box
        • 10: 39 - 循环:map/filter/reduce
        • 11: 29 - 回调、副作用:lazy Promise
        • 14: 51 - 分支、错误处理:Either
      • 17: 58 - 风格
      • 20: 24 - 在 React 中的尝试
      • 26: 18 - 小结,映射 = 组合 = 程序结构

# 文章

# 系列文章

# 函数式编程 知识体系

# 前端中常见的 FP 概念

From Functional Programming Jargon

  • Higher-Order Functions (HOF):高阶函数
  • Closure:闭包(数学集合中的概念)
  • Currying:柯里化
  • Function Composition:函数组合
  • Pure Function:纯函数
  • Side effects:副作用
  • Point-Free Style:隐式参数
  • Functor:函子
  • Lambda Calculus:Lambda 演算
  • Lazy evaluation:惰性求值

# Ramda 中有关 FP 概念的 API

  • partial
  • curry
  • lift
  • compose/pipe

# FP 基本原则

  • 纯函数
    • 引用透明(输入输出可控)
    • 无副作用
    • 不要硬编码
  • 面向接口开发(静态类型)
  • 代码设计
    • 数据和方法
      • 行为外置化(OO 方法 => 函数传参)
    • 函数
      • 副作用外置化(如 error handler、callback 通过传参实现)
  • 特性
    • 不使用 this
  • 代码封装
    • 模块化

# 函数式编程 典型代码

# 关于副作用

FP:

import { curry } from 'ramda';

let two = 2;
// * 纯函数,柯里化
const add = curry((a, b) => a + b);

const add2 = add(two); // => fn {}
const result1 = add2(3); // => 5

two = 2.1;
const result2 = add2(3); // => 5

非 FP:

let two = 2;

// * 副作用(引入了外部变量,导致结果不可预期)
const add2 = (b) => two + b;
const result1 = add2(3); // => 5

two = 2.1;
const result2 = add2(3); // => 5.1

# FP 在 JS 中的应用

虽然这些库在 JS 中仅仅采用了 FP 的风格,
或仅仅只实施了 FP 理念中的极小一部分。

但无论如何,FP 在现实开发中的确占有一席之地。

# RxJS

RxJS v6 取消了 v5 版链式调用写法,
转而使用 pipe 和纯函数进行实现。

HowTo: Convert to pipe syntax - RxJS v5.x to v6 Update Guide

// * v5
const v5$ = Observable.interval(500)
  .filter((e) => e % 2 === 0)
  .map((e) => e * 10);

// * v6
const v6$ = interval(500).pipe(
  filter((e) => e % 2 === 0),
  map((e) => e * 10),
);

# React Hooks

React Hooks 系列函数虽然在内部实现机制上有副作用,
(通过切换 ReactCurrentDispatcher,以及在内部保存数据)
但在语法上相比之前 class 的写法已经比较有 FP 的味道了。

const App = () => {
  const [value, setState] = useState(0);
  const count = () => setState((value) => value + 1);
  return <button onClick={count}> {value} </button>;
};