十四、答案

以下是本书各章节中所包含问题的解决方案(部分或全部)。 在很多情况下,如果你愿意,还会有额外的问题,这样你就可以做更多的工作。

第 1 章,函数式——若干问题

1.1。 类作为一级对象:您可能还记得,类基本上是一个函数,可以使用new。 因此,理所当然地,我们应该能够将类作为参数传递给其他函数。 makeSaluteClass()创建一个使用闭包来记住term值的类(也就是一个特殊函数)。 在本书中,我们还会看到更多这样的例子。

1.2。 :避免重复测试的关键是编写一个函数来检查参数的值,以确保它是有效的,如果是这样,调用内部函数来自己做阶乘,而不用担心错误的参数:

const carefulFact = n => {
  if (
    typeof n !== "undefined" &&
    Number(n) === n &&
    n >= 0 &&
    n === Math.floor(n)
  ) {
    const innerFact = n => (n === 0 ? 1 : n * innerFact(n - 1));
    return innerFact(n);
  }
};

console.log(carefulFact(3)); // 6, correct
console.log(carefulFact(3.1)); // undefined
console.log(carefulFact(-3)); // undefined
console.log(carefulFact(-3.1)); // undefined
console.log(carefulFact("3")); // undefined
console.log(carefulFact(false)); // undefined
console.log(carefulFact([])); // undefined
console.log(carefulFact({})); // undefined

当识别到不正确的参数时,可以抛出一个错误,但在这里,我只是忽略它,让函数返回undefined

1.3。 攀爬阶乘:下面的代码做的把戏。 我们添加一个辅助变量f,使其1爬升n。 我们必须小心,以便factUp(0) === 1:

const factUp = (n, f = 1) => (n <= f ? f : f * factUp(n, f + 1));

1.4。 代码挤压:用箭头功能,建议,以及前缀++操作符(有关更多信息,请参见 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#增量),你可以压缩newCounter()如下:

const shorterCounter = () => {
  let count = 0;
  return () => ++count;
};

使用箭头函数并不难理解,但请注意,许多开发人员可能对使用++作为前缀操作符有疑问或怀疑,因此这个版本可能更难理解。

第二章,功能思维——第一个例子

2.1。 :我们可以使用fn变量本身作为标志。 调用fn()之后,我们将变量设置为null。 在拨打fn()之前,我们检查是否为null:

const once = fn => {
  return (...args) => {
    fn && fn(...args);
    fn = null;
  };
};

2.2。 :以类似于我们在上一个问题中所做的方式,我们调用第一个函数,然后切换到下一个函数。 在这里,我们使用了解构赋值以更紧凑的方式写入交换。 详情请参考https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Swapping_variables:

const alternator = (fn1, fn2) => {
  return (...args) => {
    fn1(...args);
    [fn1, fn2] = [fn2, fn1];
  };
};

2.3。 凡事都有限度! 我们只检查limit变量是否大于0。 如果是,则将其减 1 并调用原始函数; 否则,我们什么也不做:

const thisManyTimes = (fn, limit) => {
  return (...args) => {
    if (limit > 0) {
 limit--;
      return fn(...args);
    }
  };
};

第三章,从函数开始——一个核心概念

3.1。 未初始化的对象? 关键是我们没有把返回的对象用括号括起来,所以 JavaScript 认为括号括起来的是要执行的代码。 在这种情况下,type被认为是在标记一个语句,它实际上没有做任何事情:它是一个没有被使用的表达式(t)。 因此,代码被认为是有效的,因为它没有显式的return语句,隐式的返回值是undefined。 看到【https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label T6】更多标签,和【显示】https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions Returning_object_literals 返回对象。 修改后的代码如下:

const simpleAction = t => ({
    type: t;
});

3.2。 允许使用箭头吗? 对于listArguments2()没有问题,但是对于listArguments(),你会得到一个错误,因为arguments没有为箭头函数定义:

listArguments(22,9,60); 
Uncaught ReferenceError: arguments is not defined

3.3。 一行:It works! (是的,在这种情况下,一行的答案是合适的!)

3.4。 发现 bug! 最初,许多人会查看奇怪的(console(...), window.store.set(...))代码,但 bug 并不存在:因为逗号操作符的工作方式,JavaScript 首先进行日志记录,然后进行设置。 真正的问题是oldSet()没有绑定到window.store对象,所以第二行应该如下所示:

const oldSet = window.store.set.bind(window.store);

请重新阅读使用方法小节,了解更多相关内容,以及11.1问题,了解另一种日志记录方式,即使用 decorator。

3.5。 Bindless 绑定:如果bind()并不可用,您可以使用一个闭包,that的诀窍(我们在处理的【显示】部分),和apply()的方法,如下:

function bind(context) {
  var that = this;
  return function() {
    return that.apply(context, arguments);
  };
}

我们可以做一些类似于在中添加部分所做的事情。 或者,为了多样化,我们可以使用一个基于||操作符的通用习惯用法:如果存在Function.prototype.bind,计算就会停止,并使用现有的bind()方法; 否则,我们的新函数将被应用:

Function.prototype.bind =
  Function.prototype.bind ||
  function(context) {
    var that = this;
    return function() {
      return that.apply(context, arguments);
    };
  };

第四章,行为恰当-纯函数

4.1。 简约功能:它的工作原理,因为fib(0)= 0 和 fib(1)= 1,所以确实【T6 n】<,【显示】fib(n) = n【病人】

4.2。 :基本上,这个算法的工作方式就像你手工计算斐波那契数列一样。 你先写下来fib(0)= 0 和 fib(1)= 1,添加他们【显示】fib(2)= 1,添加最后两把fib(3)= 2,等等。 在这个版本的算法中,ab代表两个连续的斐波那契数。 这个实现非常高效!

4.3。 洗牌测试:在洗牌数组之前,对数组的一个副本进行排序,JSON.stringify(),并保存结果。 在改组之后,对改组数组的副本进行排序,并对其进行JSON.stringify()排序。 最后,应该生成两个相等的 JSON 字符串。 这就消除了所有其他的测试,因为它确保了数组的长度和元素不变:

describe("shuffleTest", function() {
  it("shouldn't change the array length or its elements", () => {
    let a = [22, 9, 60, 12, 4, 56];
    let old = JSON.stringify([...a].sort());
    shuffle(a);
    let new = JSON.stringify([...a].sort());
    expect(old).toBe(new);
  });
});

4.4。 :有些财产不再总是有效的。 为了简化我们的例子,让我们假设两个数字相差不超过 0.1,那么它们彼此接近。 如果是这样,那么我们有以下几点:

  • 0.5 接近 0.6,0.6 接近 0.7,但 0.5 不接近 0.7。
  • 0.5 接近 0.6,0.7 接近 0.8,但 0.5+0.7=1.2 不接近 0.6+0.8=1.4; 同样的数字,0.50.7=0.35 并不接近 0.60.8=0.48。
  • 0.5 接近 0.4,0.2 接近 0.3,但 0.5-0.2=0.3 不接近 0.4-0.3=0.1。
  • 0.6 接近 0.5,0.9 接近 1.0,但 0.6/0.9=0.667 不接近 0.5/1.0=0.5。

其他引用的属性始终为真。

4.5。 必须返回? 如果一个纯函数不返回任何东西,这意味着该函数不做任何事情,因为它不能修改它的输入或任何其他副作用。

4.6。 JavaScript 做数学? 如果您运行该代码,您将(意外地)得到Math failure?消息。 问题在于 JavaScript 内部使用二进制而不是十进制,而且浮点精度有限。 在十进制中,0.1,0.2 和 0.3 有一个固定的,短的表示,但在二进制中,它们有无限的表示,很像 1/3=0.33333… 在小数。

如果你在测试后写出了a+b的值,你会得到 0.30000000000000004——这就是为什么你必须非常小心地在 JavaScript 中测试相等。

第五章,声明式编程——更好的风格

5.1。 过滤… 但是什么? Boolean(x)!!x相同,分别将转换为真或假。 因此,.filter()操作将从数组中删除所有伪元素。

5.2。 生成 HTML 代码,限制:在现实生活中,你不会限制自己只使用filter(),map(),reduce(),但这个问题的目的是让你思考如何管理只有这些。 使用join()或其他额外的字符串函数将使问题更容易解决。 例如,找到一种添加外围的<div><ul> ... </ul></div>标记的方法是很棘手的,所以我们必须让第一个reduce()操作产生一个数组,这样我们才能继续处理它:

var characters = [
  { name: "Fred", plays: "bowling" },
  { name: "Barney", plays: "chess" },
  { name: "Wilma", plays: "bridge" },
  { name: "Betty", plays: "checkers" },
  { name: "Pebbles", plays: "chess" }
];

let list = characters
  .filter(x => x.plays === "chess" || x.plays == "checkers")
  .map(x => `<li>${x.name}</li>`)
  .reduce((a, x) => [a[0] + x], [""])
  .map(x => `<div><ul>${x}</ul></div>`)
  .reduce((a, x) => x);

console.log(list);
// *<div><ul><li>Barney</li><li>Betty</li><li>Pebbles</li></ul></div>*

访问map()reduce()回调函数的数组和索引参数也可以提供解决方案:

let list2 = characters
  .filter(x => x.plays === "chess" || x.plays == "checkers")
  .map(
 (x, i, t) =>
 `${i === 0 ? "<div><ul>" : ""}` +
 `<li>${x.name}</li>` +
 `${i == t.length - 1 ? "</ul></div>" : ""}`
 )
  .reduce((a, x) => a + x, "");

我们还可以这样做:

let list3 = characters
  .filter(x => x.plays === "chess" || x.plays == "checkers")
  .map(x => `<li>${x.name}</li>`)
 .reduce(
 (a, x, i, t) => a + x + (i === t.length - 1 ? "</ul></div>" : ""),
 "<div><ul>"
 );

学习这三个例子:它们将帮助你深入了解这些高阶函数,并为你提供思路,使你能够独立完成工作。

5.3。 更正式的测试:使用一个想法从 4.3 问题:选择一个数组和一个函数,找到的结果映射使用标准map()法和新myMap()功能,并比较这两个JSON.stringify()结果:他们应该匹配。

5.4。 :这需要一点仔细的算术,但应该不会太麻烦。 在这里,我们需要区分两种情况:向上和向下的范围。 前者的默认步长为 1,后者的默认步长为-1。 我们用Math.sign()来表示:

const range2 = (start, stop, step = Math.sign(stop - start)) =>
  new Array(Math.ceil((stop - start) / step))
    .fill(0)
    .map((v, i) => start + i * step);

计算范围的几个例子显示了我们的选择的多样性:

console.log(range2(1, 10));       // *[1, 2, 3, 4, 5, 6, 7, 8, 9]*
console.log(range2(1, 10, 2));    // *[1, 3, 5, 7, 9]*
console.log(range2(1, 10, 3));    // *[1, 4, 7]*
console.log(range2(1, 10, 6));    // *[1, 7]*
console.log(range2(1, 10, 11));   // *[1]*

console.log(range2(21, 10));      // *[21, 20, 19, ... 13, 12, 11]*
console.log(range2(21, 10, -3));  // *[21, 18, 15, 12]*
console.log(range2(21, 10, -4));  // *[21, 17, 13]*
console.log(range2(21, 10, -7));  // *[21, 14]*
console.log(range2(21, 10, -12)); // *[21]*

使用这个新的range2()函数意味着您可以以函数的方式编写更多种类的循环,而不需要for(...)语句。

5.5。 :问题是String.fromCharCode()不是一元的。 这个方法可以接收任意数量的参数,当你写map(String.fromCharCode)时,回调函数会被三个参数(当前值、索引和数组)调用,这会导致意想不到的结果。 使用第 6 章中的变化章节中的unary()生产函数——高阶函数也可以。 详情请登录https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCharCode

5.6。 生成 CSV:第一个解决方案,连同一些辅助功能如下; 你能理解每个函数的作用吗?

const concatNumbers = (a, b) => (a == " " ? b : a + "," + b);
const concatLines = (c, d) => c + "\n" + d;
const makeCSV = t =>
  t.reduce(concatLines, " ", t.map(f => f.reduce(concatNumbers, " ")));

也可以使用另一种单行代码,但不太清楚——您同意吗?

const makeCSV2 = t =>
  t.reduce(
    (c, d) => c + "\n" + d,
    " ",
    t.map(x => x.reduce((a, b) => (a == " " ? b : a + "," + b), " "))
  );

5.7产生更好的输出:为此,你需要做一些额外的映射,如下:

const better = apiAnswer
  .flatMap(c => c.states.map(s => ({...s, country: c.name})))
  .flatMap(s => s.cities.map(t => ({...t, state: s.name, country: s.country})))
  .map(t => `${t.name}, ${t.state}, ${t.country}`);

/*
[ 'Lincoln, Buenos Aires, Argentine',
 'Lincoln, England, Great Britain',
 'Lincoln, California, United States of America',
 'Lincoln, Rhode Island, United States of America',
 'Lincolnia, Virginia, United States of America',
 'Lincoln Park, Michigan, United States of America',
 'Lincoln, Nebraska, United States of America',
 'Lincoln Park, Illinois, United States of America',
 'Lincoln Square, Illinois, United States of America' ]
*/

5.8仅支持老式代码! 一种方法是使用join()从单个句子中构建一个长长的字符串,然后使用split()将其拆分为单词,最后查看结果数组的长度:

const words = gettysburg.join(" ").split(" ").length;

5.9异步链接:一篇文章博季诺夫卡尔波夫,可以在发现 https://thecodebarbarian.com/basic-functional-programming-with-async-await.html,提供 polyfills 方法如forEach()map(),等等,也为异步开发一个类数组允许链接。

5.10Missing equivalent:首先使用mapAsync()获取异步值,然后将原始函数应用到返回的数组中。 some()的例子如下:

const someAsync = (arr, fn) =>
 mapAsync(arr, fn).then(mapped => mapped.some(Boolean));

(async () => {
  const someEven = await someAsync([1, 2, 3, 4], fakeFilter);
  useResult(someEven);

  const someEven2 = await someAsync([1, 3, 5, 7, 9], fakeFilter);
  useResult(someEven2);
})();
/*
2019-10-13T22:05:32.215Z true
2019-10-13T22:05:33.257Z false
*/

第 6 章,生成函数-高阶函数

6.1。 :将函数应用到 null 对象将抛出一个错误:

const getField = attr => obj => obj[attr];

getField("someField")(null);
// *Uncaught TypeError: Cannot read property 'a' of null*

在 FP 中,让函数抛出异常通常不是很好。 你可以选择生成undefined,或者使用单子,就像我们在本书的最后第 12 章Building Better Containers - Functional Data Types中所做的那样。 getField()的安全版本如下:

const getField2 = attr => obj => (attr && obj ? obj[attr] : undefined);

6.2。 How many?多少个? 调用calc(n)评估fib(n)所需的调用次数。 分析显示所有需要计算的树,我们得到以下结果:

  • (0)=1
  • (1)=1
  • n>1、calc(n) = 1 +calc(【显示】n1)+calc(【病人】n 2)

遵循的是最后一行,当我们叫fib(n),我们有一个电话,加上调用fib(【T6 n】1)和【显示】fib(n 2)。 电子表格显示,calc(50)是 40,730,022,147 -相当高!

如果你照顾一些代数,它可以显示calc(n) = 5fib(【T6 n】1)+【显示】fib(n 4) 1, 或者随着 n【病人】,calc(【t16.1】n)成为约(1 +√5)= 3.236 倍的价值fib(n),但因为这不是数学书,我不会提这些结果!

6.3。 :使用我们的第 4 章shuffle()函数行为恰当-纯函数,我们可以编写以下代码。 在这里,我们将第一个函数从列表中移除,然后将其余函数移动,并将其添加到数组的末尾,以避免重复任何调用:

const randomizer = (...fns) => (...args) => {
  const first = fns.shift();
  fns = shuffle(fns);
  fns.push(first);
  return fns[0](...args);
};

快速验证表明它满足了我们的所有要求:

const say1 = () => console.log(1);
const say22 = () => console.log(22);
const say333 = () => console.log(333);
const say4444 = () => console.log(4444);

const rrr = randomizer(say1, say22, say333, say4444);
rrr(); // *333*
rrr(); // *4444*
rrr(); // *333*
rrr(); // *22*
rrr(); // *333*
rrr(); // *22* 
rrr(); // *333*
rrr(); // *4444*
rrr(); // *1*
rrr(); // *4444*

一个小注意事项:由于randomizer()的编写方式,列表中的第一个函数永远不能第一次调用。 您能否提供一个更好的版本,不存在这个小缺陷,以便列表中的所有函数在第一次调用时都有同样的机会?

6.4。 【知识点讲解】Just say no! 调用原函数,然后使用typeof检查返回值是数值还是布尔值,然后再决定返回什么。

6.5。 :一个简单的单行版本可以如下。 在这里,我们使用展开来获取原始对象的浅副本,然后通过使用计算属性名将指定的属性设置为其新值。 详情见https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer:

const setField = (attr, value, obj) => ({...obj, [attr]: value});

第 10 章ensure Purity - Immutability中,我们写了deepCopy(),当涉及到创建一个全新对象而不是一个浅拷贝时,这将比扩展更好。 通过使用这个,我们可以得到以下结果:

const setField = (attr, value, obj) => ({...deepCopy(obj), [attr]: value});

最后,您还可以查看修改updateObject()功能,也从第 10 章确保纯度-不可变性通过删除冻结代码; 我让你来决定。

6.6。 函数长度错误*:我们可以用eval()来解决这个问题——总的来说不是一个好主意! 如果你坚持,我们可以写一个function.length保存版本arity()如下; 让我们称之为arityL():

const arityL = (fn, n) => {
  const args1n = range(0, n)
    .map(i => `x${i}`)
    .join(",");
  return eval(`(${args1n}) => ${fn.name}(${args1n})`);
};

如果将arityL()应用于Number.parseInt,结果如下(注意生成的函数具有正确的length性质):

const parseInt1 = arityL(parseInt, 1);
/*
  (x0) => parseInt(x0,x1)
 parseInt1.length === 1
*/

const parseInt2 = arity(Number.parseInt,2)
/*
  (x0,x1) => parseInt(x0,x1)
  parseInt2.length === 2
*/

6.7。 :我们可以使用Math.max()Math.min()如下:

const findMaximum2 = findOptimum2((x, y) => Math.max(x, y));

const findMinimum2 = findOptimum2((x, y) => Math.min(x, y));

另一种写法是先定义以下内容:

const max = (...args) => Math.max(...arr);

const min = (...args) => Math.min(...arr);

然后,我们可以用无点的风格来写:

const findMaximum3 = findOptimum2(max);

const findMinimum3 = findOptimum2(min);

第七章,转换函数- curry 和部分应用

7.1。 :以下sumMany()函数起作用:

const sumMany = total => number =>
  number === undefined ? total : sumMany(total + number);

sumMany(2)(2)(9)(6)(0)(-3)(); // 16

7.2。 :We can do curting by hand forapplyStyle():

const applyStyle = style => string => `<${style}>${string}</${style}>`;

7.3。 :基本上,我们只是改造curryByBind()版本,使其使用this:

Function.prototype.curry = function() {
  return this.length === 0 ? this() : p => this.bind(this, p).curry();
};

您可以以类似的方式工作,而是提供一个partial()方法。

7.4。 :我们可以用与curryByEval()相似的方式工作:

const uncurryByEval = (fn, len) =>
  eval(
    `(${range(0, len)
      .map(i => `x${i}`)
      .join(",")}) => ${fn.name}${range(0, len)
      .map(i => `(x${i})`)
      .join("")}`
  );

在前面,当 curry 时,给定一个属性为 3 的fn()函数,我们将生成以下内容:

x0=>x1=>x2=> make3(x0,x1,x2)

现在,要 uncurry 一个函数(比如,curriedFn()),我们需要做一些非常类似的事情:唯一的区别是括号的位置:

(x0,x1,x2) => curriedFn(x0)(x1)(x2)

预期行为如下:

const make3 = (a, b, c) => String(100 * a + 10 * b + c);
const make3c = a => b => c => make3(a, b, c);
console.log(make3c(1)(2)(3));  // *123*

const remake3 = uncurryByEval(make3c, 3);
console.log(remake3(4, 5, 6)); // *456*

7.5。 谜题功能:实现局部 curry; 下面是一个例子:

const sum3 = (a, b, c) => 100 * a + 10 * b + c;
const alt3 = what(sum3);

console.log(alt3(1, 2, 4));
console.log(alt3(1, 2)(4));
console.log(alt3(1)(2, 4));
console.log(alt3(1)(2)(4));
/*
  "124", four times
*/

what()函数的一个更易于理解和更好命名的版本如下:

const partial = fn => (...params) =>
  fn.length <= params.length
    ? fn(...params)
    : (...otherParams) => partial(fn)(...params, ...otherParams);

7.6。 更多的咖喱! 这只是我们partialCurryingByBind()的另一个版本。 唯一的区别是,如果您为一个函数提供了所有的参数,这个新的curry()将直接调用 curry 过的函数,而partialCurryingByBind()将先将函数绑定到它的所有参数,然后递归地调用它以返回最终的结果。 我们可以使用下面的代码来检查结果是否完全相同:

const make3 = (a, b, c) => String(100 * a + 10 * b + c);

const make3curried = curry(make3);

console.log(make3curried(1)(2)(3));
console.log(make3curried(4, 5)(6));
console.log(make3curried(7, 8, 9));

/*
123
456
789
*/

第 8 章,连接函数-管道和组合

8.1。 标题大写:我们可以使用几种不同方法的功能对等物,如split()map()join()。 第六章利用demethodize()【显示】,生产函数——高阶函数flipTwo()第七章【病人】,转换函数局部套用和部分应用,也可能:

const split = str => arr => arr.split(str);
const map = fn => arr => arr.map(fn);
const firstToUpper = word =>
  word[0].toUpperCase() + word.substr(1).toLowerCase();
const join = str => arr => arr.join(str);

const headline = pipeline(split(" "), map(firstToUpper), join(" "));

管道按照预期工作:我们将字符串分割成单词,映射每个单词使其首字母大写,然后连接数组元素再次形成一个字符串。 我们本可以在最后一步使用reduce(),但join()已经做了我们需要的事情,为什么要白费力气呢?

console.log(headline("Alice's ADVENTURES in WoNdErLaNd")); 
// Alice's Adventures In Wonderland

8.2。 挂起的任务:以下管道执行该任务:

const getField = attr => obj => obj[attr];
const filter = fn => arr => arr.filter(fn);
const map = fn => arr => arr.map(fn);
const reduce = (fn, init) => arr => arr.reduce(fn, init);

const pending = (listOfTasks, name) =>
  pipeline(
    getField("byPerson"),
    filter(t => t.responsible === name),
    map(t => t.tasks),
    reduce((y, x) => x, []),
    filter(t => t && !t.done),
    map(getField("id"))
  )(allTasks || {byPerson: []}); //

reduce()的呼吁可能令人困惑。 到那时,我们正在处理一个只有一个元素的数组——一个对象——而且我们希望对象在管道中,而不是数组中。 即使负责人不存在,或者所有的任务都已经完成,这个代码也可以工作; 你知道为什么吗? 另外,请注意,如果allTasksnull,则必须为对象提供byPerson属性,以便将来的函数不会崩溃! 对于一个更好的解决方案,我认为单子更好:更多见问题 12.1。

8.3。 :简单的解决方法意味着构成。 我更喜欢它,而不是管道,以保持函数列表的相同顺序:

const getSomeResults2 = compose(sort, group, filter, select);

8.4未检出杂质? 是的,函数是不洁净的,但使用它会直接下 SFP近似函数式编程(SFP)风格我们提到的【T6 理论与实践】的【显示】第一章,成为功能——几个问题。 我们使用的版本不是纯数组,但在我们使用它的方式中,最终的结果是纯的:我们在适当的地方修改数组,但它是我们正在创建的一个新数组。 替代实现是纯的,也可以工作,但会慢一些,因为每次调用它都会创建一个全新的数组。 所以,接受这一点不纯可以帮助我们得到一个性能更好的函数; 我们可以接受!

8.5不必要的转导?** 如果您只有一个map()操作序列,您可以应用一个映射,并将所有映射函数管道到一个单一的映射函数中。 对于filter()操作,它变得有点困难,但这里有一个提示:使用reduce()以一个仔细考虑过的累积函数,按顺序应用所有的过滤器。

第 9 章,设计函数——递归

9.1。 :空字符串通过不做任何事来反转。 要反转一个非空字符串,请删除它的第一个字符,反转其余字符,并将删除的字符附加在末尾。 例如,使用reverse("ONTEVIDEO")+"M"可以找到reverse("MONTEVIDEO")。 同理,reverse("ONTEVIDEO")将等于reverse("NTEVIDEO")+"O",依此类推:

const reverse = str =>
  str.length === 0 ? "" : reverse(str.slice(1)) + str[0];

9.2。 :假设我们要爬一架有n台阶的梯子。 我们可以通过两种方式做到这一点:

  • 先爬一个台阶,然后再爬一个(n-1)阶梯
  • 一次爬两级,然后爬上(n-2)级梯子

因此,如果我们把ladder(n)称为爬梯子的方法的数量,我们就知道ladder(n)=ladder(n-1)+ladder(n-2)。 添加这一事实【显示】梯子(0)= 1(只有一个办法爬上梯子没有步骤:什么都不做),梯(1)= 1,解决方案是【病人】梯子(n)= (n1)【t16.1】th 斐波纳契数! ladder(2)=2,ladder(3)=3,ladder(4)=5,依此类推。

9.3。 LCS:最长公共子序列的长度(LCS)的两个字符串,和【显示】,可以找到与递归,如下:***

如果a的长度为 0,或者b的长度为 0,则返回 0。 * 如果ab的首字符匹配,则答案是 1 加上ab的 LCS,都减去它们的首字符。 * 如果ab的首字符不匹配,则为以下两个结果中最大的一个: * a减去初始性状,b的 LCS * ab的 LCS 减去其初始特征

我们可以如下实现它。 我们“手工”记忆,以避免重复计算; 我们也可以使用我们的记忆功能:

const LCS = (strA, strB) => {
  let cache = {}; // memoization "by hand"

  const innerLCS = (strA, strB) => {
    const key = strA + "/" + strB;

    if (!(key in cache)) {
      if (strA.length === 0 || strB.length === 0) {
        ret = 0;

      } else if (strA[0] === strB[0]) {
        ret = 1 + innerLCS(strA.substr(1), strB.substr(1));

      } else {
        ret = Math.max(
          innerLCS(strA, strB.substr(1)),
          innerLCS(strA.substr(1), strB)
        );
      }

      cache[key] = ret;
    }

    return cache[key];
  };

  return innerLCS(strA, strB);
};

console.log(LCS("INTERNATIONAL", "CONTRACTOR")); // 6, as in the text

作为额外的练习,您不仅可以尝试生成 LCS 的长度,还可以生成所涉及的字符。

9.4。 对称皇后:求对称解的关键如下: 当前四张皇后(暂时)被放置在棋盘的前半部分后,我们不必为其他的皇后尝试所有可能的位置; 它们是根据前一个自动确定的:

const SIZE = 8;
let places = Array(SIZE);
const checkPlace = (column, row) =>
  places
    .slice(0, column)
    .every((v, i) => v !== row && Math.abs(v - row) !== column - i);

const symmetricFinder = (column = 0) => {
  if (column === SIZE) {
    console.log(places.map(x => x + 1)); // print out solution

  } else if (column <= SIZE / 2) {
 // first half of the board?
    const testRowsInColumn = j => {
      if (j < SIZE) {
        if (checkPlace(column, j)) {
          places[column] = j;
          symmetricFinder(column + 1);
        }
        testRowsInColumn(j + 1);
      }
    };
    testRowsInColumn(0);

  } else {
    // second half of the board
 let symmetric = SIZE - 1 - places[SIZE - 1 - column];
 if (checkPlace(column, symmetric)) {
 places[column] = symmetric;
 symmetricFinder(column + 1);
 }
 }
};

调用symmetricFinder()产生四种解决方案,它们本质上是相同的。 制作图纸并检查,以确保它是正确的!

[3, 5, 2, 8, 1, 7, 4, 6]
[4, 6, 8, 2, 7, 1, 3, 5]
[5, 3, 1, 7, 2, 8, 6, 4]
[6, 4, 7, 1, 8, 2, 5, 3]

9.5。 递归排序:让我们看看第一个算法; 这里的许多技巧将帮助您编写其他类型的代码。 如果数组是空的,对它进行排序将产生一个(新的)空数组。 否则,我们找到数组的最大值(max),创建一个新的数组副本,但不包含该元素,对该副本进行排序,然后返回在末尾添加max的已排序副本。 看看我们是如何处理 mutator 函数以避免修改原始字符串的:

const selectionSort = arr => {
  if (arr.length === 0) {
    return [];
  } else {
    const max = Math.max(...arr);
    const rest = [...arr];
    rest.splice(arr.indexOf(max), 1);
    return [...selectionSort(rest), max];
  }
};

selectionSort([2, 2, 0, 9, 1, 9, 6, 0]);
// *[0, 0, 1, 2, 2, 6, 9, 9]*

9.6。 可能出什么问题? 如果在任何时候,要排序的数组(或子数组)包含所有相等的值,则该操作将失败。 在这种情况下,smaller将是一个空数组,而greaterEqual将等于要排序的整个数组,因此逻辑将进入一个无限循环。

9.7。 :The following code does The work for us。 在这里,我们使用三元运算符来决定将新项推到哪里:

const partition = (arr, fn) =>
  arr.reduce(
    (result, elem) => {
 result[fn(elem) ? 0 : 1].push(elem);
      return result;
    },
    [[], []]
  );

第十章,确保纯度-不变性

10.1。 通过代理冻结:根据请求,使用代理允许您拦截对象上的更改。 (详见https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 我们使用递归应用代理一直到,以防某些属性本身是对象:

const proxySetAll = obj => {
  Object.keys(obj).forEach(v => {
    if (typeof obj[v] === "object") {
      obj[v] = proxySetAll(obj[v]);
    }
  });

  return new Proxy(obj, {
    set(target, key, value) {
      throw new Error("DON'T MODIFY ANYTHING IN ME");
    },
    deleteProperty(target, key) {
      throw new Error("DON'T DELETE ANYTHING IN ME");
    }
  });
};

下面是上述代码的输出。 当然,除了DON'T MODIFY ANYTHING IN ME信息之外,您可能还需要其他信息!

let myObj = {a: 5, b: 6, c: {d: 7, e: 8}};
myObj = proxySetAll(myObj);

myObj.a = 777;  // *Uncaught Error: DON'T MODIFY ANYTHING IN ME*
myObj.f = 888;  // *Uncaught Error: DON'T MODIFY ANYTHING IN ME*
delete myObj.b; // *Uncaught Error: DON'T DELETE ANYTHING IN ME*

10.2。 :使用递归帮助:

  • 如果列表为空,则不能插入新键。
  • 如果我们在一个节点上,而该节点的键不是oldKey,则创建该节点的克隆,并将新键插入到原始节点列表的其他位置。

  • 如果我们在一个节点上,它的键是oldKey,我们创建一个节点的克隆,它指向一个以新节点开始的列表,以newKey作为它的值,它本身指向原始节点列表的其余部分:

const insertAfter = (list, newKey, oldKey) => {
  if (list === null) {
    return null;

  } else if (list.key !== oldKey) {
    return node(list.key, insertAfter(list.next, newKey, oldKey));

  } else {
    return node(list.key, node(newKey, list.next));
  }
};

在下面的代码中,我们可以看到它是如何工作的。 新列表类似于图 10.2。 然而,打印出列表(c3newList)是不够的; 这样做的话,您将无法识别新节点或旧节点,因此我包含了几个比较。 下面的最后一个比较表明,从"F"节点开始,列表是相同的:

class Node {
  constructor(key, next = null) {
    this.key = key;
    this.next = next;
  }
}
const node = (key, next) => new Node(key, next);

let c3 = node("G", node("B", node("F", node("A", node("C", node("E"))))));
let newList = insertAfter(c3, "D", "B"); 

c3 === newList // false
c3.key === newList.key // true (both are "G")
c3.next === newList.next // false

c3.next.key === newList.next.key // true (both are "B")
c3.next.next === newList.next.next // false

c3.next.next.key === "F" // true
newList.next.next.key === "D" // true
c3.next.next.next === newList.next.next.next.next // true

当我们实现这个时,如果没有找到oldKey,则不会插入任何内容。 您是否可以更改逻辑,以便将新节点添加到列表的末尾?

10.3。 :我们想从左到右组合镜头,这样我们就可以直接使用reduce()了。 让我们编写composeManyLenses()函数,并将其应用到文本中所示的相同示例中:

const composeManyLenses = (...lenses) =>
 lenses.reduce((acc, lens) => composeTwoLenses(acc, lens));

console.log(view(composeManyLenses(lC, lE, lG, lJ, lK), deepObject));
/*
    ***11**, same as earlier*
*/

10.4。 镜径:提示:从getField()getByPath()所需要的改变将与我们所做的相同。

10.5。 :使用 getter 总是可行的,对于这个问题,你可以这样写:

const lastNameLens = composeTwoLenses(lensProp("name"), lensProp("last"));

const fullNameGetter = obj => `${view(lastNameLens)(obj)}, ${view(firstNameLens)(obj)}`;

能够基于单个值设置多个属性并不总是可能的,但如果我们假设输入的全名格式正确,我们可以用逗号将其分割,并将这两部分分别赋给名和姓:

const fullNameSetter = (fullName, obj) => {
  const parts = fullName.split(",");
  return set(firstNameLens, parts[1], set(lastNameLens, parts[0], obj));
};

10.6。 阵列用镜头? 函数可以很好地工作,但是set()over()不能纯粹地工作,因为setArray()没有返回一个新的数组; 相反,它修改当前的位置。 看下一个问题的相关问题。

10.7。 :从地图中获取值没有问题,但对于设置,我们需要克隆地图:

const getMap = curry((key, map) => map.get(key));

const setMap = curry((key, value, map) => new Map(map).set(key, value));

const lensMap = key => lens(getMap(key), setMap(key));

第十一章,实现设计模式-功能方式

11.1。 :正如我们已经提到的,decorator 目前并不是一个固定的、明确的特征。 然而,通过跟随https://tc39.github.io/proposal-decorators/,我们可以写:

const logging = (target, name, descriptor) => {
  const savedMethod = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`entering ${name}: ${args}`);

    try {
      const valueToReturn = savedMethod.bind(this)(...args);
      console.log(`exiting ${name}: ${valueToReturn}`);
      return valueToReturn;

    } catch (thrownError) {
      console.log(`exiting ${name}: threw ${thrownError}`);
      throw thrownError;
    }
  };

  return descriptor;
};

我们想给一个方法添加一个@logging装饰。 我们将原始方法保存在savedMethod中,并替换一个新方法,该方法将记录接收到的参数,调用原始方法保存其返回值,记录该返回值,最后返回它。 如果原始方法抛出异常,我们捕获它、报告它,并再次抛出它,以便按预期处理它。 一个简单的例子如下:

class SumThree {
  constructor(z) {
    this.z = z;
  }
  @logging
  sum(x, y) {
    return x + y + this.z;
  }
}

new SumThree(100).sum(20, 8);
// *entering sum: 20,8*
// *exiting sum: 128*

11.2。 Decorator with mixins:按照问题 1.1 的思路,我们编写一个addBar()函数,接收一个Base类并扩展它。 在本例中,我决定添加一个新属性和一个新方法。 扩展类的构造函数调用原始构造函数并创建.barValue属性。 这个新类既有原来的doSomething()方法,也有新的somethingElse()方法:

class Foo {
  constructor(fooValue) {
    this.fooValue = fooValue;
  }

  doSomething() {
    console.log("something: foo...", this.fooValue);
  }
}

var addBar = Base =>
  class extends Base {
    constructor(fooValue, barValue) {
      super(fooValue);
      this.barValue = barValue;
    }

    somethingElse() {
 console.log("something added: bar... ", this.barValue);
 }
  };

var fooBar = new (addBar(Foo))(22, 9);
fooBar.doSomething();   // *something: foo... 22*
fooBar.somethingElse(); // *something added: bar... 9*

11.3。 :有各种方法来实现这个与计时器和计数,但要确保你不干扰单或双击检测! 你也可以用一个普通的听众,看看event.detail; 详情请登录https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail

第 12 章,构建更好的容器——函数数据类型

12.1。 任务? 下面的代码显示了一个比我们在问题 8.2 中看到的更简单的解决方案:

const pending = Maybe.of(listOfTasks)
  .map(getField("byPerson"))
  .map(filter(t => t.responsible === name))
  .map(t => tasks)
  .map(t => t[0])
  .map(filter(t => !t.done))
  .map(getField("id"))
  .valueOf();

这里,我们一个接一个地应用函数,确保这些函数中的任何一个产生空结果(或者即使原始的listOfTasks为空),调用序列将继续进行。 最后,您将获得一个任务 id 数组或一个空值。

12.2。 :如果你用递归的方式来计算树的高度是很简单的。 空树的高度为 0,而非空树的高度为 1(根结点)加上左、右子树的最大高度:

const treeHeight = tree =>
  tree(
    (val, left, right) =>
      1 + Math.max(treeHeight(left), treeHeight(right)),
    () => 0
  );

按顺序列出键是众所周知的要求。 由于树的构建方式,你首先列出左子树的键,然后是根,最后是右子树的键,所有这些都是递归的方式:

const treeList = tree =>
  tree(
    (value, left, right) => {
      treeList(left);
      console.log(value);
      treeList(right);
    },
    () => {
      // nothing
    } );

最后,从二叉搜索树中删除一个键有点复杂。 首先,你必须找到要删除的节点,然后有以下几种情况:

  • 如果节点没有子树,删除就很简单。
  • 如果该节点只有一个子树,则只需用它的子树替换该节点。
  • 如果节点有两个子树,那么你必须执行以下操作:
    • 在树中找到最小的键值。
    • 把它放在节点的位置。

由于这个算法在所有的计算机科学教科书中都有很好的介绍,我在这里就不详细介绍了:

const treeRemove = (toRemove, tree) =>
  tree(
    (val, left, right) => {
      const findMinimumAndRemove = (tree /* never empty */) =>
        tree((value, left, right) => {
          if (treeIsEmpty(left)) {
            return { min: value, tree: right };

          } else {
            const result = findMinimumAndRemove(left);
            return {
              min: result.min,
              tree: Tree(value, result.tree, right)
            };
          }
        });

      if (toRemove < val) {
        return Tree(val, treeRemove(toRemove, left), right);

      } else if (toRemove > val) {
        return Tree(val, left, treeRemove(toRemove, right));

      } else if (treeIsEmpty(left) && treeIsEmpty(right)) {
        return EmptyTree();

      } else if (treeIsEmpty(left) !== treeIsEmpty(right)) {
        return tree((val, left, right) => left(() => left, () => right));

      } else {
        const result = findMinimumAndRemove(right);
        return Tree(result.min, left, result.tree);
      }
    },
    () => tree
  );

12.3。 :让我们添加到已经提供的示例中。 如果我们能将列表转换为数组,就可以简化列表的操作,反之亦然:

const listToArray = list =>
    list((head, tail) => [head, ...listToArray(tail)], () => []);

const listFromArray = arr =>
    arr.length
        ? NewList(arr[0], listFromArray(arr.slice(1)))
        : EmptyList();

将两个列表连接在一起并将值附加到列表中有简单的递归实现。 我们也可以使用追加函数来反转列表:

const listConcat = (list1, list2) =>
  list1(
    (head, tail) => NewList(head, listConcat(tail, list2)),
    () => list2
  );

const listAppend = value => list =>
  list(
    (head, tail) => NewList(head, listAppend(value)(tail)),
    () => NewList(value, EmptyList)
  );

const listReverse = list =>
  list(
    (head, tail) => listAppend(head)(listReverse(tail)),
    () => EmptyList
  );

最后,基本的map()filter()reduce()操作是好的:

const listMap = fn => list =>
  list(
    (head, tail) => NewList(fn(head), listMap(fn)(tail)),
    () => EmptyList
  );

const listFilter = fn => list =>
  list(
    (head, tail) =>
      fn(head)
        ? NewList(head, listFilter(fn)(tail))
        : listFilter(fn)(tail),
    () => EmptyList
  );

const listReduce = (fn, accum) => list =>
  list((head, tail) => listReduce(fn, fn(accum, head))(tail), () => accum);

以下是一些有待你去解决的练习:

  • 生成列表的可打印版本。
  • 比较两个列表,看看它们是否具有相同的值和相同的顺序。
  • 在列表中搜索值。
  • 获取、更新或删除列表中n-位置的值。

12.4。 缩短代码:首先要利用||运算符的短路求值,去掉第一个三元运算符:

const treeSearch2 = (findValue, tree) =>
  tree(
    (value, left, right) =>
      findValue === value ||
      (findValue < value
        ? treeSearch2(findValue, left)
        : treeSearch2(findValue, right)),
    () => false
  );

另外,看到第二个三元操作符中的两个选项非常相似,你也可以在这里做一些缩短:

const treeSearch3 = (findValue, tree) =>
  tree(
    (value, left, right) =>
      findValue === value ||
      treeSearch3(findValue, findValue < value ? left : right),
    () => false
  );

记住:短并不意味着更好! 但是,我发现了许多这种代码紧缩的例子,如果您也接触过这种方法,那就更好了。*