FP$1-MethodChainingAndRecrusive

110 阅读1分钟

FP$1-MethodChainingAndRecrusive

Computational processes are abstract beings that inhabit computers.
As they evolve, processes manipulate other abstract things called data.
—Harold Abelson and Gerald Jay Sussman (Structure and Interpretation of Computer Programs, MIT Press, 1979)

函数式编程,就是把函数串起来,其中的一个方法就是 method chaining。 Method chaining 的好处是写起来简单,局限是只能使用某个对象上的方法。

循环在 FP 中并不推荐,迭代是一种替换方案。将迭代的逻辑封装成方法,方便使用。

0. Understanding your application’s control flow

  • 命令式的表现是详细列出每一步的过程,包含循环和分支及状态的更改。
  • 声明式的表现是粗略列出每一步的目的(通过函数具体实现),尽量避免循环和分支及状态的更改(工具函数封装了循环和分支,递归替换循环;纯函数避免状态更改)。

Control flow: the path a program takes to arrive at a solution.

An imperative program describes its flow or path in great detail by exposing all the necessary steps needed to fulfill its task. These steps usually involve lots of loops and branches, as well as variables that change with each statement.

On the other hand, declarative programs, specifically functional ones, raise the level of abstraction by using a minimally structured flow made up of independent blackbox operations that connect in a simple topology. These connected operations are nothing more than higher-order functions that move state from one operation to the next.

Working functionally with data structures such as arrays lends itself to this style of development and treats data and control flow as simple connections between high-level components.

optA().optB().optC().optD(); // a shared object.

Chaining operations in this manner leads to concise, fluent, expressive programs that let you separate a program’s control flow from its computational logic. Thus, you can reason about your code and your data more effectively.

1. Method chaining

Method chaining is an OOP pattern that allows multiple methods to be called in a single statement. When these methods all belong to the same object, method chaining is referred to as method cascading.

FP 强调 immutability。如果操作的数据是 immutable,比如 string;或者 method 不改变原数据,比如 map,则满足了 immutability。

'Functional Programming'.substring(0, 10).toLowerCase() + ' is fun';
concat(toLowerCase(substring('Functional Programming', 1, 10))),' is fun');

2. Function chaining

在我的理解中,上面的 method chaining 指的是对象调用方法;而这里的 function chaining 指的是函数直接调用。但是给的例子里使用了 _(),感觉不是 function。

Functional programming uses common ones like arrays and applies a number of coarse-grained, higher-order operations that are agnostic to the underlying representation of the data. These operations are designed to do the following:

  • Accept function arguments in order to inject specialized behavior that solves your particular task
  • Replace the traditional, manual looping mechanisms that contain mutations of temporary variables and side effects, thereby creating less code to maintain and fewer places where errors can occur

Functional programming promotes the use of three central higher order functions—map, reduce, and filter (array extras).

2.1 _.filter (predicate)

filter(p, [d0, d1, d2, d3...dn]) -> [d0,d1,...dn] (subset of original input)
// Listing 3.5 filter implementation
function filter(arr, predicate) {
  let idx = -1,
    len = arr.length,
    result = [];
  while (++idx < len) {
    let value = arr[idx];
    if (predicate(value, idx, this)) {
      result.push(value);
    }
  }
  return result;
}
_(persons).filter(isValid).map(fullname);
const bornIn1903 = person => person.birthYear === 1903;
_(persons).filter(bornIn1903).map(fullname).join(' and ');
// Array comprehension: [for (x of iterable) if (condition) x]
[for (p of people) if (p.birthYear === 1903) p.fullname]
	.join(' and ');

2.2 _.map (collect, transform)

A formal definition of this operation is as follows:

map(f, [e0, e1, e2...]) -> [r0, r1, r2...]; where, f(dn) = rn
// Listing 3.1 Map implementation
function map(arr, fn) {
  const len = arr.length,
        result = new Array(len);
  for (let idx = 0; idx < len; ++idx) {
    result[idx] = fn(arr[idx], idx, arr);
  }
  return result;
}
_.map(persons, 
 s => (s !== null && s !== undefined) ? s.fullname : '' 
);
_(persons).reverse().map(
 p => (p !== null && p !== undefined) ? p.fullname : ''
);

2.3 _.reduce

reduce(f,[e0, e1, e2, e3],accum) -> f(f(f(f(acc, e0), e1, e2, e3)))) -> R
// 上面是书里的,下面是我认为的
reduce(f,[e0, e1, e2, e3],accum) -> f(f(f(f(acc, e0), e1), e2), e3)) -> R
// Listing 3.2 Implementing reduce
function reduce(arr, fn, accumulator) {
  let idx = -1,
    len = arr.length;
  if (!accumulator && len > 0) {
    accumulator = arr[++idx];
  }
  while (++idx < len) {
    accumulator = fn(accumulator, arr[idx], idx, arr);
  }
  return accumulator;
}
// Listing 3.3 Computing country counts
_(persons).reduce(function (stat, person) {
	const country = person.address.country;
	stat[country] = _.isUndefined(stat[country]) ? 1 : 
		stat[country] + 1;
	return stat;
}, {});
_(persons).map(func1).reduce(func2);
// Listing 3.4 Combining map and reduce to compute statistics
const getCountry = person => person.address.country;
const gatherStats = function (stat, criteria) {
	stat[criteria] = _.isUndefined(stat[criteria]) ? 1 :
		stat[criteria] + 1;
	return stat;
};
_(persons).map(getCountry).reduce(gatherStats, {});
const countryPath = ['address','country'];
const contryLens = R.lens(R.path(countryPath), R.assocPath(countryPath));
_(persons).map(R.view(contryLens)).reduce(gatherStats, {});
// or
_.groupBy(persons, R.view(cityLens));

_.reduceRight

reduceRight(f, [e0, e1, e2],accum) -> f(e0, f(e1, f(e2, f(e3,accum)))) -> R

_.some

const isNotValid = val => _.isUndefined(val) || _.isNull(val); 
const notAllValid = args => _(args).some(isNotValid); 
notAllValid(['string', 0, null, undefined]) //-> true
notAllValid(['string', 0, {}]) //-> false

_.every

const isValid = val => !_.isUndefined(val) && !_.isNull(val);
const allValid = args => _(args).every(isValid);
allValid(['string', 0, null]); //-> false
allValid(['string', 0, {}]); //-> true

3. Reasoning about your code

3.1 Declarative and lazy function chains

Learn a way to build entire an program by linking a set of functions.

var names = ['alonzo church', 'Haskell curry', 'stephen_kleene',
'John Von Neumann', 'stephen_kleene'];
// Listing 3.6 Performing sequential operations on arrays (imperative approach)
var result = [];
for (let i = 0; i < names.length; i++) {
  var n = names[i];
  if (n !== undefined && n !== null) {
    var ns = n.replace(/_/, " ").split(" ");
    for (let j = 0; j < ns.length; j++) {
      var p = ns[j];
      p = p.charAt(0).toUpperCase() + p.slice(1);
      ns[j] = p;
    }
    if (result.indexOf(ns.join(" ")) < 0) {
      result.push(ns.join(" "));
    }
  }
}
result.sort();
// Listing 3.7 Performing sequential operations on arrays (functional approach)
_.chain(names)
  .filter(isValid)
  .map((s) => s.replace(/_/, " "))
  .uniq()
  .map(_.startCase)
  .sort()
  .value();
// Listing 3.8 Demonstrating lazy function chains with Lodash
_.chain(persons)
  .filter(isValid)
  .map(_.property("address.country"))
  .reduce(gatherStats, {})
  .values()
  .sortBy("count")
  .reverse()
  .first()
  .value()
  .name; //-> 'US

3.2 SQL-like data: functions as data

SELECT p.firstname FROM Person p
WHERE p.birthYear > 1903 and p.country IS NOT 'US'
ORDER BY p.firstname
_.mixin({
  select: _.map,
  from: _.chain,
  where: _.filter,
  sortBy: _.sortByOrder,
});
// Listing 3.9 Writing SQL-like JavaScript
_.from(persons)
  .where(p => p.birthYear > 1903 && p.address.country !== 'US')
  .sortBy(['firstname'])
  .select(p => p.firstname)
  .value();
//-> ['Alan', 'Barkley', 'John']

Like SQL, this JavaScript code models the data in the form of functions, also known as functions as data.

4. Learning to think recursively

4.1 What is recursion?

Recursion is a technique designed to solve problems by decomposing them into smaller, self-similar problems that, when combined, arrive at the original solution. A recursive function has two main parts:

  • Base cases (also known as the terminating condition) // done
  • Recursive cases // todo

The base cases are a set of inputs for which a recursive function computes a concrete result, without having to recur. The recursive case deals with a set of inputs (necessarily smaller than the original) for which the function calls itself.

4.2 Learning to think recursively

done and todo

first and rest

// imperative
var acc = 0;
for (let i = 0; i < nums.length; i++) {
  acc += nums[i];
}
// declarative, and recursive
_(nums).reduce((acc, current) => acc + current, 0);
// lateral thinking
sum[1,2,3,4,5,6,7,8,9] = 1 + sum[2,3,4,5,6,7,8,9] 
						= 1 + 2 + sum[3,4,5,6,7,8,9]
						= 1 + 2 + 3 + sum[4,5,6,7,8,9]

Recursion and iteration are two sides of the same coin.

// Listing 3.10 Performing recursive addition
function sum(arr) {
  if (_.isEmpty(arr)) {
    return 0;
  }
  return _.first(arr) + sum(_.rest(arr));
}
sum([]); //-> 0
sum([1, 2, 3, 4, 5, 6, 7, 8, 9]); //->45
1 + sum[2,3,4,5,6,7,8,9] 
1 + 2 + sum[3,4,5,6,7,8,9]
1 + 2 + 3 + sum[4,5,6,7,8,9]
1 + 2 + 3 + 4 + sum[5,6,7,8,9]
1 + 2 + 3 + 4 + 5 + sum[6,7,8,9]
1 + 2 + 3 + 4 + 5 + 6 + sum[7,8,9]
1 + 2 + 3 + 4 + 5 + 6 + 7 + sum[8,9]
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + sum[9]
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + sum[]
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 -> halts, stack unwinds
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9
1 + 2 + 3 + 4 + 5 + 6 + 7 + 17
1 + 2 + 3 + 4 + 5 + 6 + 24
1 + 2 + 3 + 4 + 5 + 30
1 + 2 + 3 + 4 + 35
1 + 2 + 3 + 39
1 + 2 + 42
1 + 44
45
// tail-call optimization
function sum(arr, acc = 0) {
  if (_.isEmpty(arr)) {
    return 0;
  }
  return sum(_.rest(arr), acc + _.first(arr));
}

4.3 Recursively defined data structures

Because JavaScript doesn’t have a built-in tree object, you create a simple data structure based on nodes. A node is an object that contains a value, a reference to its parent, and an array of children.

/**
 * Chapter 3 model class Node for tree navigation
 */
// helper function internal to the module
const _ = require("lodash");

const isValid = (val) => !_.isUndefined(val) && !_.isNull(val);

exports.Node = class Node {
  constructor(val) {
    this._val = val;
    this._parent = null;
    this._children = [];
  }

  isRoot() {
    return !isValid(this._parent);
  }

  get children() {
    return this._children;
  }

  hasChildren() {
    return this._children.length > 0;
  }

  get value() {
    return this._val;
  }

  set value(val) {
    this._val = val;
  }

  append(child) {
    child._parent = this;
    this._children.push(child);
    return this;
  }

  toString() {
    return `Node (val: ${this._val}, children:
			${this._children.length})`;
  }
};
const church = new Node(new Person('Alonzo', 'Church', '111-11-1111'));

Trees are recursively defined data structures that contain a root node:

/**
 * Simple tree class
 * Author: Luis Atencio
 */

const _ = require("lodash");

class Tree {
  constructor(root) {
    this._root = root;
  }

  static map(node, fn, tree = null) {
    node.value = fn(node.value); // 这里直接改变了原来的值,not pure
    if (tree === null) {
      tree = new Tree(node);
    }
    if (node.hasChildren()) {
      _.map(node.children, function (child) {
        Tree.map(child, fn, tree);
      });
    }
    return tree;
  }

  get root() {
    return this._root;
  }

  toArray(node = null, arr = []) {
    if (node === null) {
      node = this._root;
    }
    arr.push(node.value);
    // Base case
    if (node.hasChildren()) {
      var that = this; // TODO revisit Lodash doc to insert objec context
      _.map(node.children, function (child) {
        that.toArray(child, arr);
      });
    }
    return arr;
  }
}
exports.Tree = Tree;
church.append(rosser).append(turing).append(kleene);
kleene.append(nelson).append(constable);
rosser.append(mendelson).append(sacks);
turing.append(gandy);
Tree.map(church, p => p.fullname)
/*
'Alonzo Church', 'Barkley Rosser', 'Elliot Mendelson', 'Gerald Sacks', 'Alan
Turing', 'Robin Gandy', 'Stephen Kleene', 'Nels Nelson', 'Robert Constable'
 */

This idea of encapsulating data to control how its' accessed is key to functional programming when working with immutability and side effect-free data types.

总结

  • 在 FP 中编写业务函数的一种方法是 method chaining。对于数组类型,Lodash 提供了 _()_.chain() 来实现 method chaining,中间的 chaining 包括 filter, map, reduce 等。这样写起来的代码容易看懂,而不是命令式的很多循环、分支等。
  • 循环可以被迭代替换。done 和 todo / base cases 和 recursive cases。将数据封装起来控制其获取是 FP 实现数据不可变和无副作用的关键手段。