FP$Prepare-JavaScriptForFP

98 阅读4分钟

FP$Prepare-JavaScriptForFP

Natural language has no dominant paradigm, and neither does JavaScript.
Developers can select from a grab bag of approaches— procedural, functional, and object-oriented—and blend them as appropriate.
—Angus Croll, If Hemingway Wrote JavaScript

1. Object

1.0 Managing the state of JavaScript objects

The state of a program can be defined as a snapshot of the data stored in all of its objects at any moment in time.

JavaScript is one of the worst languages when it comes to securing an object’s state. A JavaScript object is highly dynamic, and you can modify, add, or delete its properties at any point in time.

JavaScript 的数据可以随意修改,这和 FP 的 immutability 有冲突。我们需要一些措施来防止数据被修改:

  • 从数据(源头)本身上禁止被修改:Value Object
  • Object.freeze 把对象给冻住(deep freeze)
  • 通过库的 lenses 生成新的数据
  • 通过 StructuredClone / JSON.parse(JSON.stringify()) 生成新的数据

1.1 Treating objects as values

In functional programming, we call types that behave "immutable" values.

Encapsulation is a good strategy to protect against mutations.

  • use const for not reassignment
  • create a Value Object // simple object structure, encapsulation
  • Object.freeze

Value Object Pattern

A value object is one whose equality doesn’t depend on identity or reference, just on its value; once declared, its state may not change. In addition to numbers and strings, some examples of value objects are types like tuple, pair, point, zipCode, coordinate, money, date, and others.

通过闭包隐藏属性,从而防止更改。第二个例子通过调用方法返回新的对象来防止更改原对象。

// Value Object Pattern
function zipCode(code, location) {
  let _code = code; // closures // can use # nowadays
  let _location = location || "";
  return { // object literal interface
    code: function () {
      return _code;
    },
    location: function () {
      return _location;
    },
    fromString: function (str) {
      let parts = str.split("-");
      return zipCode(parts[0], parts[1]);
    },
    toString: function () {
      return _code + "-" + _location;
    },
  };
}
const princetonZip = zipCode("08544", "3345");
princetonZip.toString(); //-> '08544-3345'
function coordinate(lat, long) {
  let _lat = lat;
  let _long = long;
  return {
    latitude: function () {
      return _lat;
    },
    longitude: function () {
      return _long;
    },
    translate: function (dx, dy) {
      return coordinate(_lat + dx, _long + dy); // Returns a new copy with the translated  coordinates
    },
    toString: function () {
      return "(" + _lat + "," + _long + ")";
    },
  };
}
const greenwich = coordinate(51.4778, 0.0015);
greenwich.toString(); //-> '(51.4778, 0.0015)'
greenwich.translate(10, 10).toString(); //-> '(61.4778, 10.0015)'

Deep-freezing moving parts

Object.freeze

Object.freeze 可以“冻住”对象,但不是完全冻住:

  • shallow freeze:只对第一层属性有效
  • setter 依然可以使用(data properties 被冻住了,accessor properties 没被冻住)
  • private properties 不会被冻住(not have the concept of property descriptors)
    • 和普通 properties 不同,如果提供了方法改变值,普通 properties 不能更改,但是 private properties 可以更改。
// shallow freeze
const person = Object.freeze(new Person('Haskell', 'Curry', '444-44-4444'));
person.firstname = 'Bob'; // no warning, but no effect
// Listing 2.2 Recursive function to deep-freeze an object
// deep freeze
const isObject = (val) => val && typeof val === 'object';
function deepFreeze(obj) {
	if(isObject(obj) && !Object.isFrozen(obj)) {
		Object.keys(obj).forEach(name => deepFreeze(obj[name]));
		Object.freeze(obj);
	}
	return obj;
}	

1.2 Navigating and modifying object graphs with lenses

对于对象,直接改变其属性会改变原始值:

  • 使用 lenses:设置时,会生成新的对象
  • structuredClone 直接复制一份新的对象 Your own copy-on-write strategy and return new objects from each method call: too obtrusive and hardcoding boilerplate code everywhere.

You need a solution for mutating stateful objects, in an immutable manner, that’s unobtrusive and doesn’t require hardcoding boilerplate code everywhere.

Lenses, also known as functional references, are functional programming’s solution to accessing and immutably manipulating attributes of stateful data types. Internally, lenses work similarly to a copy-on-write strategy by using an internal storage component that knows how to properly manage and copy state.

const person = new Person('Alonzo', 'Church', '444-44-4444');
// 1. 设置某个属性
const lastnameLens = R.lensProp('lastName');
// 2.1 查看这个属性-基于某个对象
R.view(lastnameLens, person); //-> 'Church'
// 2.2 设置这个属性,返回新的对象
const newPerson = R.set(lastnameLens, 'Mourning', person);
newPerson.lastname; //-> 'Mourning'
person.lastname; //-> 'Church'
person.address = new Address(
  "US",
  "NJ",
  "Princeton",
  zipCode("08544", "1234"),
  "Alexander St."
);
// 1. 设置某个属性-具有层次结构
const zipPath = ["address", "zip"];
const zipLens = R.lens(R.path(zipPath), R.assocPath(zipPath));
// 2.1 读取这个属性-基于某个对象
R.view(zipLens, person); //-> zipCode('08544', '1234')
// 2.2 设置这个属性,返回新的对象
const newPerson = R.set(zipLens, zipCode("90210", "5678"), persion);
const newZip = R.view(zipLens, newPerson); //-> zipCode('90210', '5678')
const originalZip = R.view(zipLens, person); //-> zipCode('08544', '1234')
newZip.toString() !== originalZip.toString(); //-> true

2. Functions

在 FP 中,函数需要时 pure function。为了更好地区分纯函数和非纯函数,这里定义了名词来区分:function & procedure / expression & statement。

2.1 Functions as first-class citizens

所谓的 first-class 指的是函数可以像 object 一样作为值来使用,而不是仅仅作为调用的代码块使用。

In JavaScript, every function is an instance of the Function type.

2.2 High-order functions

// OOP
function printPeopleInTheUs(people) {
  for (let i = 0; i < people.length; i++) {
    const thisPerson = people[i];
    if (thisPerson.address.country === "US") {
      console.log(thisPerson);
    }
  }
}
printPeopleInTheUs([p1, p2, p3]);
// someData, someAction
function printPeople(people, action) {
  for (let i = 0; i < people.length; i++) {
    action(people[i]);
  }
}
var action = function (person) {
  if (person.address.country === "US") {
    console.log(person);
  }
};
printPeople(people, action);
// someData, fileter, do
function printPeople(people, selector, printer) {
  people.forEach(function (person) {
    if (selector(person)) {
      printer(person);
    }
  });
}
const inUs = (person) => person.address.country === "US";
printPeople(people, inUs, console.log);
// FP
const countryPath = ["address", "country"];
const countryL = R.lens(R.path(countryPath), R.assocPath(countryPath));
const inCountry = R.curry((country, person) =>
  R.equals(R.view(countryL, person), country)
);

people.filter(inCountry("US")).map(console.log);

Types of function invocation

  • As a global function
  • As a method
  • As a constructor

Function methods

Use apply or call or bind:

null: globalThis / null in strict mode (undefined => globalThis / undefined)

function negate(func) {
  return function () {
    return !func.apply(null, arguments); //  return !func(...arguments);
  };
}

function isNull(val) {
  return val === null;
}
var isNotNull = negate(isNull);
isNotNull(null); //-> false
isNotNull({}); //-> true

Summary

  • OOP 将 fields & methods 紧密结合
  • FP 加 data & function 分得很清
  • FP 要求 data immutable:
    • 从数据(源头)本身上禁止被修改:Value Object
    • Object.freeze 把对象给冻住(deep freeze)
    • 通过库的 lenses 生成新的数据
    • 通过 StructuredClone / JSON.parse(JSON.stringify()) 生成新的数据
  • JavaScript 的 function 有些特性,理解就行