通过(重写)FizzBuzz 来理解函数式编程基础知识
函数式编程是一种通过组合纯函数来思考程序的方式。它试图避免共享状态、可变性和副作用。这使得代码更容易推理,也更容易拆分并用于
其他用途。
函数式编程是声明式的,也就是说,它描述的是做什么,而不是怎么做。这对我们来说更容易理解。(如果你想了解更多关于命令式编程和声明式编程的区别,请查看这篇文章)。
函数编程也有点难学,因为大多数与函数编程相关的文献都有点数学化(因为 FP 基于 lambda 演算)。
让我们通过以更函数式的方式重写经典的 FizzBuzz 来看看函数式编程。
等一下,纯函数?
纯函数是
- 给定相同的输入,给出相同的输出
- 无副作用
/// PURE
const sum = (a, b) => a + b;
sum(1, 2); // 3
sum(1, 2); // still 3
sum(1, 2); // 3s not goin anywhere
/// IMPURE
// impure because the output changes with the same inputs
Math.random(); // 0.21201979699214646
Math.random(); // 0.9585542542409227
Math.random(); // 0.046208832851477144
let g = 1;
// also impure because it mutates state
const setG = x => g = x;
// a harder to spot example:
const doSth = () => {
// we're calling an impure function, so this is also impure.
setG(2);
return g;
}
// exceptions are impure too
const square = x => {
if (x < 0) {
throw new Error('Negative numbers are not allowed');
}
return x * x;
}
// so is logging
console.log('I\'m impure');
那么等一下,你不能只用纯函数编写程序吗?
有时,我们需要副作用。大多数程序都无法避免打印到控制台、改变状态或抛出异常。
所以,我们不能只用纯函数来编写程序。我们能做的最好的事情就是在程序的纯函数和非纯函数部分之间划出一条清晰的界限,这样我们才能知道会发生什么。
FizzBuzz?
如果您知道 FizzBuzz 是什么,那么您可以跳过本节。
FizzBuzz 是一道经典的编程面试题。你需要编写一个程序,打印 1 到 100 之间的数字,但将 3 的倍数替换为“Fizz”,将 5 的倍数替换为“Buzz”,将 3 和 5 的倍数都替换为“FizzBuzz”。
这是“规范的” FizzBuzz 答案:
for (let i = 1; i <= 100; i++) {
if (i % 15 === 0) console.log('FizzBuzz');
else if (i % 3 === 0) console.log('Fizz');
else if (i % 5 === 0) console.log('Buzz');
else console.log(i);
}
在这篇文章中,我们将以功能性的方式重写此代码并探索其好处。
功能性 FizzBuzz
抽象函数
让我们从原始的 FizzBuzz 代码开始。你能看出哪些地方可以重构吗?
for (let i = 1; i <= 100; i++) {
if (i % 15 === 0) console.log('FizzBuzz');
else if (i % 3 === 0) console.log('Fizz');
else if (i % 5 === 0) console.log('Buzz');
else console.log(i);
}
我首先想到的是将可除性检查重构为一个函数。我们可以这样做:
const divisible = (x, y) => x % y === 0
for (let i = 1; i <= 100; i++) {
if (divisible(i, 15)) console.log('FizzBuzz');
else if (divisible(i, 3)) console.log('Fizz');
else if (divisible(i, 5)) console.log('Buzz');
else console.log(i);
}
现在它的可读性更强了,但仍然有改进的空间。我们可以
对这个函数进行柯里化:
const divisible = x => y => x % y === 0
for (let i = 1; i <= 100; i++) {
const divisibleI = divisible(i); // look ma, a new function with minimal code!
if (divisibleI(15)) console.log('FizzBuzz');
else if (divisibleI(3)) console.log('Fizz');
else if (divisibleI(5)) console.log('Buzz');
else console.log(i);
}
柯里化是一种函数返回另一个函数的技术。它是一种一次传递一个参数的方法。
以下是一个例子:
const add = x => y => x + y; add(1)(2) // 3
好处是我们现在可以利用 实现更多的功能
add
。const inc = add(1); inc(2) // 3
柯里化是一种强大的技术,可以让你的代码更加模块化。
i
这使得编写检查是否可以被另一个数字整除的函数变得非常简单。
删除命令式语句
在函数式编程中,不鼓励使用命令式语句。我们可以使用递归或其他方法来复制它们。
FizzBuzz 是数字到字符串的映射。这正是函数式编程的精髓:将一个值映射到另一个值。这里我们不需要循环,只需要将一个 1 到 100 的数组映射到一个“FizzBuzzes”(?)数组即可。
我们可以通过创建一个名为的实用函数来实现这一点range
,类似于 python 的range
函数。
const divisible = x => y => x % y === 0
const range = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => min + i)
range(1, 100).map(i => {
const divisibleI = divisible(i);
if (divisibleI(15)) console.log('FizzBuzz');
else if (divisibleI(3)) console.log('Fizz');
else if (divisibleI(5)) console.log('Buzz');
else console.log(i);
});
我们可以进一步雕琢一些函数:
const divisible = x => y => x % y === 0
const range = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => min + i)
const map = f => xs => xs.map(f)
const fizzbuzz = i => {
const divisibleI = divisible(i);
if (divisibleI(15)) console.log('FizzBuzz');
else if (divisibleI(3)) console.log('Fizz');
else if (divisibleI(5)) console.log('Buzz');
else console.log(i);
};
const mapFizzbuzz = map(fizzbuzz);
mapFizzbuzz(range(1, 100))
我们再次使用柯里化来实现可复用的函数。这使得 的定义变得mapFizzbuzz
极其简单清晰。
删除 if 语句
目前,使用的 if 语句非常相似:它们大多采用“如果 i 能被 n 整除,则输出必须包含 str”的形式。
我们可以将它们重构为一个对象,同时也摆脱所有的 if 语句!
const divisible = x => y => x % y === 0
const range = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => min + i)
const reduce = f => init => xs => xs.reduce(f, init)
const map = f => xs => xs.map(f)
const CANONICAL_FIZZBUZZ = [
{n: 3, str: 'Fizz'},
{n: 5, str: 'Buzz'},
// {n: 7, str: 'Duzz'} // try this out!
];
const fizzbuzz = keys => i => {
const divisibleI = divisible(i);
const reducer = reduce((acc, {n, str}) => acc + (divisibleI(n) ? str : ''))('');
console.log(reducer(keys) || i);
};
const canonFizzbuzz = fizzbuzz(CANONICAL_FIZZBUZZ);
const mapFizzbuzz = map(canonFizzbuzz);
mapFizzbuzz(range(1, 100))
现在,我们可以通过添加新项目来无限扩展我们的 FizzBuzz CANONICAL_FIZZBUZZ
。太棒了!
我们的 FizzBuzz 快完成了。但我们缺少一条规则……
分离纯净部分和不纯净部分
现在,不纯洁的东西就console.log
位于纯洁的东西的中间fizzbuzz
。
我们可以通过让 fizzbuzz 返回值并移动console.log
外部来将其切断。
这有两个好处:
- 纯净与不纯净将会被清晰地分开。
- 我们现在可以在代码的其他部分重用 fizzbuzz 函数,而不必记录值。
我们可以通过返回函数中的值来实现这一点fizzbuzz
,然后使用更多的函数实用程序来记录它们:
const divisible = x => y => x % y === 0
const range = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => min + i)
const reduce = f => init => xs => xs.reduce(f, init)
const map = f => xs => xs.map(f)
const forEach = f => xs => xs.forEach(f)
const CANONICAL_FIZZBUZZ = [
{n: 3, str: 'Fizz'},
{n: 5, str: 'Buzz'},
];
const fizzbuzz = keys => i => {
const divisibleI = divisible(i);
const reducer = reduce((acc, {n, str}) => acc + (divisibleI(n) ? str : ''))('');
return reducer(keys) || i;
};
const canonFizzbuzz = fizzbuzz(CANONICAL_FIZZBUZZ);
const mapFizzbuzz = map(canonFizzbuzz);
// IMPURE CODE STARTS HERE
const print = x => console.log(x)
const printEach = forEach(print);
printEach(mapFizzbuzz(range(1, 100)))
呼。
完成了!
就这样!希望你已经对函数式编程有了些许了解。你打算在下一个项目中使用函数式编程吗?还是会继续使用面向对象编程(或其他方言)?欢迎留言告诉我!
文章来源:https://dev.to/siddharthshyniben/understand-function-programming-basics-by-rewriting-fizzbuzz-okc