问题

类似于继承这样的方法在函数式编程里如何解决?

回答
在函数式编程中,“继承”这个概念,如果严格按照面向对象中类与类之间血缘关系那种方式来理解,确实是没有直接对等的机制的。函数式编程更强调组合和转换,而不是“是什么”的层层递进。那么,当我们在面向对象的世界里需要“复用”和“扩展”某个对象的功能时,函数式编程是怎么做到的呢?

想象一下,我们有一个基础的“行为”或者说“能力”。在面向对象里,我们可能会创建一个基类,然后让其他类继承它,从而获得这些能力。在函数式编程里,我们会把这些“能力”看作是独立的、可组合的函数。

1. 高阶函数:包装与增强

最核心的方式就是利用高阶函数。高阶函数就是能够接受函数作为参数,或者返回函数的函数。

举个例子,假设我们想创建一个“能说话”的对象。在面向对象里,我们可能会有一个`Speaker`类,里面有一个`speak`方法。

在函数式编程里,我们可以有一个`speak`函数,它接收一个人的名字,然后返回一个表示说话的字符串:

```javascript
const speak = (name) => `Hello, my name is ${name}.`;
```

现在,如果我们想创建一个“能用特定语言说话”的变体,比如用法语说:

在面向对象里,我们可能会创建一个`FrenchSpeaker`类,继承`Speaker`,然后重写`speak`方法。

在函数式编程里,我们可以写一个高阶函数,它接收一个基础的“说话”函数,然后返回一个新的、经过修改的“说话”函数:

```javascript
const createFrenchSpeaker = (baseSpeakFunction) => {
return (name) => {
const englishGreeting = baseSpeakFunction(name); // 调用基础的说话函数
// 做一些转换,这里简单地替换一下
return englishGreeting.replace("Hello", "Bonjour");
};
};

const englishSpeaker = (name) => `Hello, my name is ${name}.`;
const frenchSpeaker = createFrenchSpeaker(englishSpeaker);

console.log(englishSpeaker("Alice")); // 输出: Hello, my name is Alice.
console.log(frenchSpeaker("Alice")); // 输出: Bonjour, my name is Alice.
```

这里的`createFrenchSpeaker`就是一个高阶函数。它“接收”了`englishSpeaker`这个函数(这就是一种“组合”),然后“返回”了一个新的函数`frenchSpeaker`,这个新函数拥有了基础的说话能力,并且增加了“用法语说话”的特定行为。

这就像是给一个已有的函数“穿上一件外衣”,这件外衣增加了新的功能,或者改变了原有的行为。这种模式在函数式编程中非常常见,它被称为装饰器(Decorator)或者包装器(Wrapper)。

2. 函数组合:链式调用与功能叠加

如果我们希望一个实体同时拥有多种能力,比如“能说话”和“能唱歌”。

面向对象里,一个类可以继承多个基类(在一些语言中),或者通过接口实现多重能力。

在函数式编程里,我们可以将这些能力写成独立的函数,然后通过函数组合将它们“串联”起来。

假设我们有一个`sing`函数:

```javascript
const sing = (name) => `${name} starts singing a beautiful song.`;
```

现在,我们想让一个人既能说话又能唱歌。我们可以创建一个新的函数,它调用这两个独立的函数,并将结果组合起来:

```javascript
const speakAndSing = (name) => {
const speech = englishSpeaker(name);
const song = sing(name);
return `${speech} And ${song}`;
};

console.log(speakAndSing("Bob"));
// 输出: Hello, my name is Bob. And Bob starts singing a beautiful song.
```

更进一步,我们可以抽象这个组合的过程。有许多库提供了专门的函数组合工具,比如Lodash的`flow`或`pipe`(虽然通常用于数据流,但底层思想一致):

```javascript
// 假设存在一个函数组合工具 `compose`
// compose(f, g) = (...args) => f(g(...args))
// pipe(f, g) = (...args) => g(f(...args))

// 让我们模拟一个简单的 pipe
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);

const speakThenSing = pipe(englishSpeaker, sing);

console.log(speakThenSing("Charlie"));
// 注意:这里 pipe 默认是把前一个函数的输出作为后一个函数的输入
// englishSpeaker("Charlie") 返回一个字符串 "Hello, my name is Charlie."
// sing("Charlie") 需要一个名字,而不是一个字符串。
// 所以需要调整一下:

// 让我们重新思考:如果我们要让一个“实体”拥有两种能力,
// 并且这些能力都基于同一个“上下文”(比如名字)
// 我们可以这样:

const performActions = (name) => {
const speechResult = englishSpeaker(name);
const singingResult = sing(name);
return {
name: name,
speech: speechResult,
song: singingResult
};
};

// 或者,更函数式地,将多个返回不同值的函数组合起来,
// 并且确保它们都接收相同的参数:

const getCapabilities = (name) => ({
speech: englishSpeaker(name),
song: sing(name)
});

console.log(getCapabilities("David"));
// 输出: { name: 'David', speech: 'Hello, my name is David.', song: 'David starts singing a beautiful song.' }
```

这里的`getCapabilities`函数,虽然不是通过“继承”得到的能力,但它通过组合了`englishSpeaker`和`sing`这两个函数的调用结果,将这些能力“聚合”在了一个地方,并且是以一种数据结构的形式呈现。这提供了类似“拥有”多种能力的表现。

3. 闭包:封装与状态保持

前面提到高阶函数可以返回新的函数。当这个返回的函数“记住了”它被创建时所处的环境(变量),这就是闭包。

这在某种程度上可以模拟面向对象中对象实例的状态。

比如,我们想创建一个计数器,每次调用都能返回一个递增的数字。

面向对象会是这样:

```javascript
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
return this.count;
}
}
const myCounter = new Counter();
console.log(myCounter.increment()); // 1
console.log(myCounter.increment()); // 2
```

在函数式编程里,我们可以用闭包来实现:

```javascript
const createCounter = () => {
let count = 0; // 这是闭包的“状态”
return () => {
count++;
return count;
};
};

const myCounterFunc = createCounter();
console.log(myCounterFunc()); // 1
console.log(myCounterFunc()); // 2

const anotherCounterFunc = createCounter(); // 创建另一个独立的计数器
console.log(anotherCounterFunc()); // 1
```

这里的`createCounter`函数创建了一个私有的`count`变量,然后返回了一个新的函数。这个返回的函数(`myCounterFunc`)“捕获”了外部作用域中的`count`变量。每次调用`myCounterFunc`时,它都在访问和修改同一个`count`变量。这提供了类似对象实例的状态管理能力。

当我们想“扩展”这个计数器的行为,比如增加一个“重置”功能,我们可以用高阶函数和闭包结合:

```javascript
const createResettableCounter = () => {
let count = 0;
const increment = () => {
count++;
return count;
};
const reset = () => {
count = 0;
return 0; // 或者返回当前count,这里返回0
};
return {
increment,
reset
};
};

const resettableCounter = createResettableCounter();
console.log(resettableCounter.increment()); // 1
console.log(resettableCounter.increment()); // 2
console.log(resettableCounter.reset()); // 0
console.log(resettableCounter.increment()); // 1
```

这个`createResettableCounter`返回一个对象,其中包含两个函数(`increment`和`reset`),它们共享同一个`count`状态。这非常接近面向对象中创建一个包含多个方法的对象实例。

总结一下,函数式编程里解决“继承”类似需求的核心思想是:

将能力拆分成独立的、可复用的函数。
利用高阶函数来组合、包装和增强现有函数的功能。 这就像是给一个函数“穿上”更多的“行为外衣”,或者根据需要“定制”它的行为。
使用函数组合来将多个独立的功能“连接”起来,形成更复杂的行为。
利用闭包来封装状态,使得函数能够保持自己的“记忆”,这在模拟对象实例的行为时非常有用。

这是一种“组合优于继承”的思路。我们不是在构建一个有层层血缘的“物种”,而是在“拼装”和“编织”一系列独立的“行动”和“能力”,根据需要将它们巧妙地组合在一起,以达到我们想要的效果。最终的产物可能是一个函数,一个返回函数的函数,或者一个包含多个函数的对象,这些都比简单的类继承提供了更灵活的复用和扩展方式。

网友意见

user avatar

题主的问题在于混淆了目的和手段。

继承是一个手段而不是目的,所以你要问fp里面怎么搞继承,首先思路就是错的。

就和不能问枪怎么砍人是一样的。同样是杀人这个目的,枪和刀的手段天生是不同的。(即使你用有刺刀的步枪也别因为以前用刀顺手就真把它当刀用)

而继承用来解决什么问题,最简单的就是看哪些设计模式主要靠继承。

我自己在学fp之后有回头去再看过设计模式,很多设计模式在fp中都消失了--变成了语言基本功能。最典型的莫过于策略模式,放在fp里面就是把算法函数定义成一个参数就可以了要个屁的模式啊。

对了,虽然书很久没有翻了,我还记得虽然用的java,但是作者有提到过一点,“多用组合,少用继承”。也就是说即使在OO里面继承也算不上是受推荐的手段,题主就别念念不忘了。

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有