FP$0-WhyFuctional

148 阅读1分钟

FP$0-WhyFuctional

OO makes code understandable by encapsulating moving parts.
FP makes code understandable by minimizing moving parts.
— Michael Feathers (Twitter)

1. What is Functional Programming?

FP 函数式编程指的是以函数为中心的编程方式。核心概念是 declarative、pure function 和 immutability。Function 就像是动词,使用起来的感觉就像:do this, do that and do it.

Functional programming refers to the declarative evaluation of pure functions to create immutable programs by avoiding externally observable side effects.

It’s not a matter of just applying functions to come up with a result; the goal is to abstract control flows and operations on data with functions in order to avoid side effects and reduce mutation of state in your application.

Functional vs. object-oriented programming

面向对象编程的关注点在于对象的封装:fields 和 methods 要紧密的结合 (tight coupled)。继承 extends 是复用的主要手段(当然还有 composition 之类的)。

函数式编程的关注点在于函数—— data 和 function 的分离 (loosely coupled)。这一点和 TypeScript 的 structural type system 很像——我不在乎你是人是狗,只要有 name 和 age,我就能处理。

  • OOP 面向对象编程:

    1. 封装 encapsulation——类 class 封装了 fields & methods (actions)。数据和操作紧密结合。(抽象 abstraction 在我看来就是封装的源头,即封装是抽象的实现。)
    2. 继承 inheritance——继承是主要的共享功能(复用)的方式。(多态(重写)在我看来是继承的变体。)
    • 组合 composition——Composition over inheritance.(在我看来是封装的分级——把一部分功能分给“下级”去做(策略模式),算是组装类的方式。)
  • FP:

    1. fat functions & thin data——数据(类)和操作的关系不再紧密。
    • composition (decompose & compose) & function chain:函数像是流水线上的一个个机器,只要输入原料(数据),就会输出产品(数据)。
    1. function:
      1. declarative——一些工具类已经提供了 imperative 的过程, 所以 function chain 中的 functions 就可以 declarative 了。(对于重复工作,imperative 使用 control flows,而 declarative 则使用 recursion。)
      2. pure function——function chain 是流水线工作,相同的输入要产生相同的输出,且不影响外界。(no side effects + same )
    2. data: immutability——如果操作数据会改变原始数据,那么 pure function 就不能实现了。

OOP 里,一般是先设计类,再使用实例。FP 里,则是先设计函数,再使用函数(通过高阶函数)。

Example: procedural to functional

0. original thoughts

document.querySelector('#msg').innerHTML = '<h1>Hello World</h1>';

1. extract the moving parts:

Wrap this code with a function and make the change points parameters:

function printMessage(elementId, format, message) {
	document.querySelector(`#${elementId}`).innerHTML =
		`<${format}>${message}</${format}>`;
}
printMessage('msg', 'h1','Hello World');

What about writing to a file? Change again?

2. FP

Functional programming is a bit like using functions on steroids, because your sole objective is to evaluate and combine lots of functions with others to achieve greater behavior.

// Listing 1.1 Functional printMessage
const printMessage = run(addToDom('msg'), h1, echo);
printMessage('Hello World');

Listing 1.1 captures the process of decomposing a program into smaller pieces that are more reusable, more reliable, and easier to understand, and then combining them to form an entire program that is easier to reason about as a whole.

run links each function in a chain-like manner by passing the return value of one as input to the next.

// Listing 1.2 Extending printMessage
const printMessage = run(console.log, repeat(2), h2, echo);
printMessage('Get Functional');

The fundamental concepts on which FP is based:

  • Declarative programming
  • Pure functions
    • Referential transparency
  • Immutability

1.1 Functional programming is declarative

  • Declarative programming paradigms: a paradigm that expresses a set of operations without revealing how they’re implemented or how data flows through them. (what to do)
  • Imperative or procedural: treats a computer program as merely a sequence of top-to-bottom statements that changes the state of the system in order to compute a result. (how to do)
// imperative: how to do
const array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
for(let i = 0; i < array.length; i++) {
	array[i] = Math.pow(array[i], 2);
}
array; //-> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
// declarative: what to do
// separates program description from evaluation.
// focuses on the use of **expressions** to describe what the logic of a program is without necessarily specifying its control flow or state changes. (SQL)
// you only need to be concerned with applying the right behavior at each element and **cede** control of looping to other parts of the system
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(
	function pow(num) {
		return Math.pow(num, 2);
}); 
//-> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
// let Array.prototype.map() do the heavy lifting.

我对于箭头函数保持观望状态。因为箭头函数没有名字。

// Abstracting loops with functions lets you take advantage of lambda expressions or arrow functions
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(num => Math.pow(num, 2));
//-> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

为了清除 loop,我们需要另一种机制来遍历,recursion + first & rest 是 FP 的替换方案。

1.2 Pure functions and the problem with side effects

A pure function has the following qualities:

  • It minds its own business. (no side effects)
  • Same inputs, same output.
function showStudent(ssn) {
	let student = db.find(ssn); // out-db
	if(student !== null) {
		document.querySelector(`#${elementId}`).innerHTML = // out-DOM; global variable
			`${student.ssn}, 
			${student.firstname},
			${student.lastname}`; 
	 }
	else {
		throw new Error('Student not found!'); // error
	}
}
showStudent('444-44-4444');

two simple enhancements:

  • Separate this long function into shorter functions, each with a single purpose.
  • Reduce the number of side effects by explicitly defining all arguments needed for the functions to carry out their job.
// Listing 1.4 Decomposing the showStudent program
// currying
const find = curry(function (db, id) {
  let obj = db.get(id);
  if (obj === null) {
    throw new Error("Object not found!");
  }
  return obj;
});
const csv = function (student) {
  return `${student.ssn}, ${student.firstname}, ${student.lastname}`;
};
const append = curry(function (elementId, info) {
  document.querySelector(elementId).innerHTML = info;
});
const showStudent = run(
	append("#student-info"),
	csv,
	find(db));
showStudent("444-44-4444");

Referential transparency and substitutability

Referential transparency / equational correctness 只是 pure function 的 same inputs, same output 的一种表现:函数执行处可以被变量替换。

// Program = [Input] + [func1, func2, func3, ...] -> Output

// Imperative version
increment();
increment();
print(counter); //-> ?

// Functional version
const plus2 = run(increment, increment);
print(plus2(0)); //-> 2

1.3 Preserving immutable data

也是 pure function 的表现之一:It minds its own business. (no side effects)。但是需要 data 的支持。

2. Benefits of FP

  • Composition: encourage you to decompose tasks into simple functions
  • Chain: process data using fluent chains
  • Decrease the complexity of event-driven code by enabling reactive paradigms

2.1 Composition: encouraging the decomposition of complex tasks

At a high level, functional programming is effectively the interplay between decomposition (breaking programs into small pieces) and composition (joining the pieces back together).

The unit of modularity, or unit of work, is the function itself.

Modularization in FP is closely related to the singularity principle.

run is an alias for one the most important techniques: composition.

fg=f(g(x))f • g = f(g(x))

This formula reads “f composed of g.” The requirement for two functions to be compatible is that they must agree in the number of arguments as well as their types.

const showStudent = compose(append('#student-info'), csv, find(db));
showStudent('444-44-4444');

Composition isn’t the only way to create fluent, modular code; you can also build sequences of operations by connecting operations in a chain-like manner.

2.2 Chain: processing data using fluent chains

A chain is a sequential invocation of functions that share a common object return value (such as the $ or jQuery object).

let enrollment = [
  { enrolled: 2, grade: 100 },
  { enrolled: 2, grade: 80 },
  { enrolled: 1, grade: 89 },
];
// imperative
let totalGrades = 0;
let totalStudentsFound = 0;
for (let i = 0; i < enrollment.length; i++) {
  let student = enrollment[i];
  if (student !== null) {
    if (student.enrolled > 1) {
      totalGrades += student.grade;
      totalStudentsFound++;
    }
  }
}
const average = totalGrades / totalStudentsFound; //-> 90

Three major steps:

  • Selecting the proper set of students (whose enrollment is greater than one)
  • Extracting their grades
  • Calculating their average grade

A function chain is a lazy evaluated program, which means it defers its execution until needed. This effectively simulates the call-by-need behavior built into other functional languages.

// Listing 1.5 Programming with function chains
_.chain(enrollment)
  .filter(student => student.enrolled > 1)
  .pluck("grade")
  .average()
  .value(); //-> 90 // Calling _.value() kicks off the execution of all operations in the chain.

There’s a distinction between pure error handling and exception handling. The goal is to implement pure error handling as much as possible and allow exceptions to fire in truly exceptional conditions, just like the ones described earlier.

2.3 Reacting to the complexity of asynchronous applications

// Imperative program that reads and validates a student’s SSN
let valid = false;
const elem = document.querySelector("#student-ssn");
elem.onkeyup = function (event) {
  const val = elem.value;
  if (val !== null && val.length !== 0) {
    val = val.replace(/^\s*|\s*$|\-s/g, "");
    if (val.length === 9) {
      console.log(`Valid SSN: ${val}!`);
      valid = true;
    }
  } else {
    console.log(`Invalid SSN: ${val}!`);
  }
};

Observables let you subscribe to a stream of data that you can process by composing and chaining operations together elegantly.

// Listing 1.7 Functional program that reads and validates a student’s SSN
Rx.Observable.fromEvent(
       document.querySelector('#student-ssn'), 'keyup')
  .pluck('srcElement', 'value')
  .map(ssn => ssn.replace(/^\s*|\s*$|\-/g, ''))
  .filter(ssn => ssn !== null && ssn.length === 9)
  .subscribe(validSsn => {
     console.log(`Valid SSN ${validSsn}`);
  });

One of the most important takeaways is that all the operations performed in listing 1.7 are completely immutable, and all the business logic is segregated into individual functions.

Functional reactive programming (FRP).

Summary

  1. FP 的特点 characteristics:
    1. 声明式 declarative
    2. 纯函数 pure function:
      1. mind own business (no side effects)
      2. same inputs, same output
    3. 不可变 immutability
  2. FP 使用时的两种方式:
    1. 组合:分解 => 组合 decomposition => composition
    2. 链式调用 fluent chains