通过(重写)FizzBu​​zz 来理解函数式编程基础知识

2025-06-07

通过(重写)FizzBu​​zz 来理解函数式编程基础知识

函数式编程是一种通过组合纯函数来思考程序的方式。它试图避免共享状态、可变性和副作用。这使得代码更容易推理,也更容易拆分并用于
其他用途。

函数式编程是声明式的,也就是说,它描述的是什么,而不是怎么做。这对我们来说更容易理解。(如果你想了解更多关于命令式编程和声明式编程的区别,请查看这篇文章)。

函数编程也有点难学,因为大多数与函数编程相关的文献都有点数学化(因为 FP 基于 lambda 演算)。

让我们通过以更函数式的方式重写经典的 FizzBu​​zz 来看看函数式编程。

等一下,纯函数?

纯函数是

  1. 给定相同的输入,给出相同的输出
  2. 无副作用
/// 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');
Enter fullscreen mode Exit fullscreen mode

那么等一下,你不能只用纯函数编写程序吗?

有时,我们需要副作用。大多数程序都无法避免打印到控制台、改变状态或抛出异常。

所以,我们不能只用纯函数来编写程序。我们能做的最好的事情就是在程序的纯函数和非纯函数部分之间划出一条清晰的界限,这样我们才能知道会发生什么。

FizzBu​​zz?

如果您知道 FizzBu​​zz 是什么,那么您可以跳过本节。

FizzBu​​zz 是一道经典的编程面试题。你需要编写一个程序,打印 1 到 100 之间的数字,但将 3 的倍数替换为“Fizz”,将 5 的倍数替换为“Buzz”,将 3 和 5 的倍数都替换为“FizzBu​​zz”。

这是“规范的” FizzBu​​zz 答案:

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);
}
Enter fullscreen mode Exit fullscreen mode

在这篇文章中,我们将以功能性的方式重写此代码并探索其好处。

功能性 FizzBu​​zz

抽象函数

让我们从原始的 FizzBu​​zz 代码开始。你能看出哪些地方可以重构吗?

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);
}
Enter fullscreen mode Exit fullscreen mode

我首先想到的是将可除性检查重构为一个函数。我们可以这样做:

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);
}
Enter fullscreen mode Exit fullscreen mode

现在它的可读性更强了,但仍然有改进的空间。我们可以
对这个函数进行柯里化:

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);
}
Enter fullscreen mode Exit fullscreen mode

柯里化是一种函数返回另一个函数的技术。它是一种一次传递一个参数的方法。

以下是一个例子:

const add = x => y => x + y;

add(1)(2) // 3

好处是我们现在可以利用 实现更多的功能add

const inc = add(1);

inc(2) // 3

柯里化是一种强大的技术,可以让你的代码更加模块化。

i这使得编写检查是否可以被另一个数字整除的函数变得非常简单。

删除命令式语句

在函数式编程中,不鼓励使用命令式语句。我们可以使用递归或其他方法来复制它们。

FizzBu​​zz 是数字到字符串的映射。这正是函数式编程的精髓:将一个值映射到另一个值。这里我们不需要循环,只需要将一个 1 到 100 的数组映射到一个“FizzBu​​zzes”(?)数组即可。

我们可以通过创建一个名为的实用函数来实现这一点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);
});
Enter fullscreen mode Exit fullscreen mode

我们可以进一步雕琢一些函数:

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))
Enter fullscreen mode Exit fullscreen mode

我们再次使用柯里化来实现可复用的函数。这使得 的定义变得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))
Enter fullscreen mode Exit fullscreen mode

现在,我们可以通过添加新项目来无限扩展我们的 FizzBu​​zz CANONICAL_FIZZBUZZ。太棒了!

我们的 FizzBu​​zz 快完成了。但我们缺少一条规则……

分离纯净部分和不纯净部分

现在,不纯洁的东西就console.log位于纯洁的东西的中间fizzbuzz

我们可以通过让 fizzbuzz 返回值并移动console.log外部来将其切断。

这有两个好处:

  1. 纯净与不纯净将会被清晰地分开。
  2. 我们现在可以在代码的其他部分重用 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)))
Enter fullscreen mode Exit fullscreen mode

呼。

完成了!

就这样!希望你已经对函数式编程有了些许了解。你打算在下一个项目中使用函数式编程吗?还是会继续使用面向对象编程(或其他方言)?欢迎留言告诉我!

文章来源:https://dev.to/siddharthshyniben/understand-function-programming-basics-by-rewriting-fizzbuzz-okc
PREV
维护热门开源项目的 6 个经验教训
NEXT
让我们改进文本区域!