C++ 中的 Monad 是什么?

2025年3月24日 | 7分钟阅读

引言

C++ 中的Monad(源自 Haskell 等函数式编程语言)表示一种设计模式,它允许在受控的方式下管理值、上下文或副作用,同时进行操作的链式调用。在 C++ 中,Monad 并非原生内置,但可以通过设计遵循特定规则的类型来应用此概念,从而实现安全且模块化的计算组合。

Monad 背后的关键思想

Monad 本质上是一个值或计算的包装器,它允许链式转换和操作,同时维护一个上下文。这个上下文可以表示如下所示的内容:

  • 处理可选值(例如 C++ 中的 `std::optional`)。
  • 管理错误或异常(例如 `std::expected`,或未来的实现)。
  • 处理异步操作(例如 `std::future`、`std::promise`)。

方法 1:简单方法

实施

输出

 
Final Result: 30   

说明

std::optional

  • 这是一种可以包含值也可以为空的类型。它对于处理值可能缺失或无效的情况很有用。

函数

  • 我们定义了两个函数,一个用于加数,另一个用于乘数。每个 函数 接受一个整数并返回一个包含结果的 `std::optional`。

链式操作

  • 为了模拟链式操作(应用一系列函数),我们创建了一个辅助函数。这个函数检查 `std::optional` 是否包含值。如果包含,函数则执行下一步;如果不包含,它则直接传递值的缺失状态。

执行流程

  • 从包装在 `std::optional` 中的初始值开始。
  • 将第一个函数应用于该值。
  • 如果第一个函数的结果有效,则将第二个函数应用于该结果。
  • 检查最终结果是否有效并打印它。

它是如何工作的?

初始化

  • 您从一个要处理的值开始。该值被包装在 `std::optional` 中,这使您能够安全地处理它,即使该值可能不存在。

处理

  • 我们使用的自定义函数会检查 optional 是否包含值。如果包含,它会将给定的函数应用于该值。
  • 这个过程允许链式操作:一个函数的结果成为下一个函数的输入。

结果处理

  • 在应用所有函数后,您会检查是否有有效的结果。如果有,则显示它;如果没有,则处理缺少结果的情况。

复杂度分析

时间复杂度

初始化

将值包装在 `std::optional` 中是一个 O(1) 操作。这是因为它涉及使用给定值对 `std::optional` 对象进行简单的初始化。

函数应用

每个函数(`add_five` 和 `multiply_by_two`)都在常数时间内运行,O(1)。它们执行简单的算术运算并返回结果。

链式操作

链式操作涉及按顺序应用每个函数。由于每个函数只应用一次,并且检查 `std::optional` 中的值是否存在也是 O(1),因此每一步的总复杂度仍然是 O(1)。

总体复杂度

包括初始化、函数应用和链式操作在内的整个操作的时间复杂度为 O(1),因为每个操作(包装值、函数应用和检查结果)都是常数时间。

空间复杂度

std::optional 的空间

`std::optional<int>` 所需的空间是常数,O(1),因为它只存储一个整数值或一个空状态。optional 本身的大小不取决于整数值的大小。

函数的空间

函数 `add_five` 和 `multiply_by_two` 除了它们的输入和输出之外,不需要额外的空间。它们操作整数值并返回新的 `std::optional` 实例,这些实例的大小也都是常数。

链式操作的空间

链式操作的空间复杂度也为常数,O(1)。这是因为中间结果存储在 `std::optional` 实例中,并且创建的此类实例的最大数量是固定的。

总体复杂度

包括 `std::optional` 对象和函数存储在内的整个操作的空间复杂度为 O(1)。没有动态内存分配,也没有相对于输入大小的空间使用量的显著增长。

方法 2:使用函数对象进行链式操作

在此方法中,您定义函数对象(具有 `operator()` 的类)来执行计算。您通过组合来链式调用这些计算,将每个函数 对象 应用于前一个对象的結果。

实施

输出

 
Final Result: 30   

说明

定义函数对象

  • 函数对象是定义特定计算的类。每个 都包含一个 `operator()`,这是执行计算的方法。例如,一个函数对象可以加一个数,而另一个函数对象可以乘一个数。

链式操作

  • 要按顺序应用多个操作,您可以创建这些函数对象的实例并一个接一个地使用它们。首先,您将初始操作应用于该值,然后获取结果并将其应用于下一个操作。

处理结果

  • 应用所有操作后,您会检查最终结果是否包含值。如果包含,您可以使用它;否则,您会处理缺少结果的情况。

它是如何工作的?

创建函数对象

  • 您将操作定义为类。每个类都有一个执行特定任务(例如加数或乘数)的方法(`operator()`)。这些类允许您创建可重用、可自定义的操作。

应用函数

  • 您使用这些函数对象来处理可能存在也可能不存在的值。如果初始值可用,则第一个函数对象将被应用于该值。如果此操作成功并返回新值,则您将第二个函数对象应用于此结果。

检查最终结果

  • 应用所有操作后,您会检查最终结果是否有效。如果是,则根据需要使用结果;如果不是,则处理没有可用有效结果的情况。

复杂度分析

时间复杂度

初始化

将值包装在 `std::optional` 中是一个 O(1) 操作。这涉及到创建 `std::optional` 对象并存储该值。

函数对象

每个函数对象(`AddFive`、`MultiplyByTwo`)都在常数时间内执行其操作,O(1)。这意味着加法或乘法运算需要固定的时间。

链式操作

应用每个函数对象都涉及调用其 `operator()` 方法。由于每个函数对象都以常数时间执行其操作,并且链式操作是顺序的,因此每个函数应用的总链式操作时间复杂度也为 O(1)。

总体复杂度

包括初始化、应用函数对象和链式操作在内的整个过程的总时间复杂度为 O(1)。这是因为每个步骤(初始化 `std::optional`、应用函数和链式操作)都以常数时间发生。

空间复杂度

std::optional 的空间

`std::optional<int>` 所需的空间是常数,O(1),因为它要么存储整数值,要么指示没有值存在。`std::optional` 的大小不会随着其保存的值而增长。

函数对象的空间

函数对象本身(如 `AddFive` 和 `MultiplyByTwo`)除了它们的内部状态(通常很少,可能只有一个整数或没有)之外,不需要额外的空间。因此,每个函数对象使用 O(1) 空间。

链式操作的空间

链式操作的空间复杂度仍然是常数,O(1)。这是因为您处理的是固定数量的 `std::optional` 实例和函数对象,并且您没有 动态分配内存

总体复杂度

该过程的总空间复杂度为 O(1)。它包括 `std::optional` 对象和函数对象的空间。所需空间不取决于输入大小,保持恒定。