当前位置: 首页 > news >正文

CppCon 2015 学习:Functional programming: functors and monads

发现模式并将其转化为有用的抽象

这个想法围绕着 发现现有代码或系统中的模式,然后将其 泛化 成可重用的抽象,而不是强行将抽象应用于特定的类型或问题。

关键概念:
  1. 类型之间的共通操作
    • 类型像 智能指针optionalfuture 都有一个共同点:它们封装了值。尽管这些类型的用途不同(智能指针用于内存管理,optional用于可选值,future用于异步操作),它们都封装了一个可以访问或修改的值。
    • 认识到这种 共通行为 可以让我们创建一个通用的抽象,这个抽象可以应用于所有这些类型。
  2. 泛型编程(Genericity)
    • 泛型编程 的概念是写出能够处理任何类型的代码,只要该类型符合某些约束或具有共同的操作。
    • 在 C++ 中,这通常是通过 模板 和类型特性来实现的。与其为每种类型编写不同的代码(例如,为 smart_ptroptional 写一个专门的函数),我们写一个 通用的代码,它可以处理任何支持某些操作的类型(比如解引用或访问值)。
      例子:
    template <typename T>
    void print_value(const T& value) {std::cout << value << std::endl;
    }
    // 这适用于任何类型,不管是普通类型、智能指针还是 optional。
    print_value(42);            // 输出 42
    print_value(std::make_optional(42));  // 输出 42
    
  3. 设计抽象与发现抽象
    • 很多设计模式(例如 Gang of Four 中的设计模式)是通过 设计抽象 来通用化常见问题的解决方案。这些模式很有用,但通常是基于直觉或经验设计出来的。
    • 然而,真正 有用的抽象 是通过 发现 类型或操作的行为模式来实现的。通过识别现有的模式,我们可以创建 优雅、简洁且自然的抽象
    • 例如,迭代器模式(Iterator Pattern)并不是为了满足每个场景的需要而发明的,而是通过观察常见的序列操作(如列表、数组和其他容器)可以如何被抽象成一种统一的方式,从而 发现 的。
  4. 强迫模式与自然抽象
    • 开发者或设计人员往往试图将某些模式强加给特定类型或问题。这在很多设计模式中都能看到,比如 单例模式(Singleton)或 工厂模式(Factory Pattern),这些结构往往是在没有真正了解问题的情况下设计出来的。
    • 当你将一个抽象强行应用到一个问题上时,可能会导致 过度设计 或不必要的复杂性。
    • 相反,通过 发现现有系统中的模式,我们可以创建与问题 自然契合 的抽象,从而提高代码的清晰性、可维护性和可扩展性。

编程中的实际应用

  1. 识别现有代码中的抽象
    • 与其设计一个新的抽象,不如去看现有代码中是否有反复出现的模式。例如:
      • C++ 中的许多容器(如 std::vectorstd::liststd::map)都有相似的操作(插入、删除、访问)。通过识别这一点,你可以设计一个适用于所有容器的抽象。
      • 同样,许多类型都在封装一个值或资源。识别这一点可以让你设计适用于许多不同类型的抽象(例如 std::optional<T>std::shared_ptr<T>std::future<T>)。
  2. 使用现有的模式
    • 与其 重新发明轮子,不如寻找 已建立的模式框架。在 C++ 中,你可以找到已经存在的抽象,这些抽象已经在 标准库 和其他库中被发现并广泛使用。这些抽象是通过 发现 常见的类型操作模式而产生的。
  3. 创建更通用的解决方案
    • 一旦你识别了共通的模式,就可以创建 更通用的解决方案。例如,之前提到的 print_value 函数可以处理多种输入类型,类似地,你也可以创建一个 统一的操作,比如 map(),它可以适用于所有类型的容器或封装类型。

示例:识别包装模式

封装一个值(无论是为了内存管理、可选性还是异步操作)是一个反复出现的模式。我们可以识别出 C++ 中许多类型符合这一模式:

  • std::unique_ptr<T>:封装一个动态分配的对象。
  • std::shared_ptr<T>:封装一个具有共享所有权的对象。
  • std::optional<T>:封装一个可能不存在的值。
  • std::future<T>:封装一个异步结果,稍后会有值。
    通过识别这个模式,我们可以创建适用于任何封装值类型的抽象。
    例如,一个处理封装值的通用函数可能是这样的:
template <typename T>
void process_wrapped_value(const T& value) {if constexpr (std::is_same<T, std::optional<int>>::value) {// 针对 std::optional<int> 的特殊逻辑}else if constexpr (std::is_same<T, std::shared_ptr<int>>::value) {// 针对 std::shared_ptr<int> 的特殊逻辑}else {// 针对其他类型的通用逻辑}
}

总结

  • 识别模式 在现有系统和类型中是创建抽象的更有效方式,而不是强行将抽象应用于特定问题。
  • 通过观察共同的操作(如包装值、遍历集合等),我们可以 泛化 解决方案,写出更加通用和可维护的代码。
  • 有效的抽象并不是 设计 出来的,而是通过 发现 现有的模式,创建适合问题的自然抽象。

这个类比在解释编程中的 函子(Functor) 时是有帮助的,尤其是在函数式编程中。函子可以看作是一个 装有值的盒子,这个盒子有一些特性。我们可以一步步解析这个类比,理解其中的概念:

函子是一个装有值的盒子

  1. 函子是一个包含值的盒子
    • 函子 就像一个 容器 或者 盒子,它包含一个值(或多个值)。通过这个盒子,我们可以操作盒子里面的值,而无需直接接触它。这就像你不需要直接操作盒子里的内容,而是通过盒子的接口来操作里面的值。
    • C++ 示例
      std::optional<int> box = 5;  // 一个装有 int 值的盒子
      
  2. 或没有值
    • 有些函子可能不包含任何值。例如,optional 可能是空的,或者 future 还没有计算结果。这个“空”状态就代表了“没有值”。
    • C++ 示例
      std::optional<int> box;  // 一个没有值的盒子(空的)
      
  3. 或包含多个值
    • 函子不仅可以包含一个值,它还可以包含多个值。比如我们常见的 listvectortuple 等,它们可以作为一个函子来容纳多个值。
    • C++ 示例
      std::vector<int> box = {1, 2, 3, 4};  // 一个包含多个值的盒子(列表)
      
  4. 多个值是否有相同的类型?
    • 盒子里的值通常是 相同类型 的,但有时也可以是 不同类型 的。比如 tuple 就可以包含多种类型的值。
    • C++ 示例
      std::tuple<int, double, std::string> box = {1, 3.14, "hello"};  // 一个包含不同类型值的盒子
      
  5. 盒子允许你查看里面的内容并对其调用函数
    • 函子的关键特性是,它允许你对里面的值应用一个 函数(或者是变换)。这通常通过一个方法来完成,通常叫做 maptransform
    • C++ 示例(std::optional
      std::optional<int> box = 5;
      auto result = box.map([](int value) { return value * 2; }); // 对盒子里的值应用一个函数
      
  6. 或者当盒子为空时不调用任何函数
    • 如果盒子是空的(即里面没有值),那么函数不会被调用。例如,在 C++ 中的 std::optional,如果盒子没有值,调用函数就不会做任何事情。
    • C++ 示例(std::optional
      std::optional<int> emptyBox;
      auto result = emptyBox.map([](int value) { return value * 2; });  // 不会做任何事情,因为盒子是空的
      
  7. 或者对多个值调用函数
    • 有些函子,比如 listvector,可以对里面的每个值都调用一次函数。这是因为它们包含多个值。
    • C++ 示例(std::vector
      std::vector<int> box = {1, 2, 3};
      std::transform(box.begin(), box.end(), box.begin(), [](int value) { return value * 2; });
      // 对 vector 中的每个元素都应用函数
      
  8. 或者对不同类型的值调用不同的函数
    • 如果函子包含不同类型的值,我们可以根据类型来调用不同的函数。这通常通过 模式匹配类型分发 来实现。
    • C++ 示例(std::variant
      std::variant<int, double> box = 3.14;
      std::visit([](auto&& arg) { std::cout << arg; }, box);  // 根据类型来应用不同的函数
      

总结

函子 比作一个 盒子 是一个简化的类比,帮助我们理解其概念:

  • 函子是一个容器(或盒子),它可以装一个值或多个值
  • 它允许我们 对盒子中的值应用函数(如果有值)。
  • 它可以是 空的包含多个值,甚至是 包含不同类型的值
  • 函子的主要特性是,它提供了一种 统一的方式 来操作盒子中的值,这就是函数式编程中 maptransform 操作的核心。

“单子是一个墨西哥卷饼” 这样的比喻,和类似的比喻比如“函子是一个盒子”,通常被用来简化或让复杂的概念更容易理解,但它们往往会带来更多的误导而非帮助。让我们分解一下这些概念,了解为什么这些比喻不太有用,并如何更好地理解这些概念:

单子和墨西哥卷饼:为什么比喻不起作用

  1. 过度简化
    • 比如“单子是一个墨西哥卷饼”这样的比喻,虽然乍一听可能让人觉得容易理解,但它过于简化了单子的概念,反而让人更迷惑。
    • 单子的核心思想是提供一种结构化的方式来处理可能涉及副作用(例如状态、IO、异常等)的计算。它是为了组合操作,同时保持可预测的上下文(例如处理副作用或空值)。
  2. 单子是组合模式,而非对象
    • 单子不是一个墨西哥卷饼,因为单子的概念本质上是关于组合和如何链式地组织计算,而不是一个简单的容器(比如墨西哥卷饼装满某些东西)。
    • 它更应该被理解为一个设计模式,用于以特定的方式处理计算,而不仅仅是一个容器,里面装着某个值。

为什么“函子是盒子”也有误导性

  • 函子是盒子这个比喻也没有很好地解释函子的含义。
  • 函子是一个类型类(在Haskell或类似语言中),它允许对一个包含的值应用映射操作。它是一个容器,允许对容器内的值应用函数,但这里有更多的内容:
    • 函子不仅仅是一个容器,它更重要的是一个对结构进行映射的方式,保持结构的形状不变,而这个映射的本质是变换。因此,单纯的“盒子”比喻无法传达函子的变换性质。

如何更好地理解单子和函子

  1. 单子
    • 单子是一个设计模式,用于以特定的方式链式地处理计算,同时保持上下文(例如,处理副作用或失败的计算)。它使得你能够在保持一致性和控制副作用的同时,进行复杂的计算。
    • 例如,在Haskell中,Maybe单子可以用于处理可能失败的计算(例如返回Nothing)。单子操作符(bind>>=)帮助我们传递失败的上下文,确保我们不需要在每一步中手动检查空值。
  2. 函子
    • 函子是一个实现了map操作的类型,允许你对结构内的值应用函数,而不改变结构本身。
    • 例如,C++中的std::optional<T>就是一个函子,你可以在其中存在值时应用映射函数,而不改变optional容器本身。

为什么比喻常常不准确

  • 比喻可以帮助初步理解,但由于它们的过度简化,往往会限制对深层概念的理解。
  • 单子函子抽象的数学概念,它们的作用是提供通用的计算结构,但这种抽象很难通过物理对象的直观比喻来捕捉。

更数学化的理解方式

  • 函子:函子是范畴论中的一个概念,它定义了从一个范畴到另一个范畴的映射。这个映射作用于对象(比如值),并且保持它们之间关系的结构。
  • 单子:单子是一个组合工具,它为链式应用函数提供了操作,并保持计算的上下文(例如,管理状态、副作用、错误等)。

总结

  • 与其依赖过度简化的比喻,不如专注于数学原理来理解单子和函子。
  • 单子是关于以可组合的方式结构化计算,函子则是将函数应用于容器内的值。
  • 虽然像“单子是墨西哥卷饼”这样的比喻看似有趣,但它们通常导致误解,掩盖了核心思想。更好的方法是通过实际的例子、代码和范畴论的学习来深入理解这些概念。

optional<T> 是 C++ 中用于表示可能存在或不存在的值的容器类型。它要么包含一个值,要么为空。这种类型非常有用,尤其是在函数的返回值可能没有有效结果时。

optional<T> 的主要概念:

  1. 包含零个或一个值
    • optional<T> 要么包含一个类型为 T 的值,要么不包含任何值(即为空)。
    • 这对于可能不返回有效结果的函数非常有用(例如,由于错误或某些条件下值不存在)。
  2. 检查值是否存在
    • 可以使用 has_value()operator bool() 来检查 optional<T> 是否包含值。
      std::optional<int> opt = 42;
      if (opt) { // 或者 if (opt.has_value())std::cout << "Value: " << *opt << std::endl; // 解引用获取值
      }
      
  3. 创建空的或包含值的 optional
    • 可以使用 std::nullopt(或 Boost 中的 boost::none)来创建一个空的 optional
      std::optional<int> opt1 = std::nullopt; // 空的
      std::optional<int> opt2 = 42;          // 包含值 42
      
  4. 有条件地应用函数
    • 可以在 optional 内部的值上应用函数,但仅在它包含有效值时。通常的方法是先检查值是否存在,然后再有条件地应用函数。
    • 典型的模式是使用 三元运算符std::transform/std::map
      auto transformed = opt ? boost::make_optional(f(*opt)) : boost::none;
      
      在这段代码中:
      • *opt 解引用 optional 来获取其内部的值。
      • boost::make_optional(f(*opt)) 对值应用函数 f,并将结果封装到一个新的 optional 中。
      • boost::none 表示如果原 optional 为空,则返回空的 optional

实际示例:

假设我们有一个可能返回值的函数,但如果分母为零,它也可能不返回值:

std::optional<double> safeDivide(double numerator, double denominator) {if (denominator == 0) {return std::nullopt;  // 如果分母为零,返回空的 optional} else {return numerator / denominator;  // 返回一个有效的结果}
}
int main() {auto result = safeDivide(10, 2);if (result) {  // 检查是否有有效的结果std::cout << "Result: " << *result << std::endl;} else {std::cout << "Division by zero!" << std::endl;}auto emptyResult = safeDivide(10, 0); // 分母为零if (!emptyResult) { // 检查没有结果的情况std::cout << "No result!" << std::endl;}
}

在这个例子中:

  • 如果除法成功,optional 包含结果。
  • 如果有错误(如分母为零),则返回空的 optional

转换 optional 内部的值:

可以使用函数来转换 optional 中的值。假设你想对一个 optional 中的值进行两倍操作(如果值存在):

std::optional<int> opt = 10;
auto doubled = opt ? boost::make_optional(*opt * 2) : boost::none;
if (doubled) {std::cout << "Doubled: " << *doubled << std::endl;
}

这里:

  • *opt 解引用 optional 获取值。
  • 三元运算符检查 optional 是否为空,并对其进行转换。

为什么使用 optional<T>

  • 避免空指针异常:相比于使用指针来表示可选值,optional<T> 更安全,因为你不需要手动检查指针是否为 nullptr
  • 更好的语义表达optional<T> 明确表达了值可能存在或不存在,提升了代码的可读性和理解性。
  • 表示缺失数据:它是表示数据缺失的绝佳工具(类似于指针中的 NULLnullptr),但提供了更好的安全性和清晰度。

C++17 及以后版本(std::optional):

  • 在 C++17 中,std::optional<T> 已经成为标准库的一部分,提供与 Boost 的 optional 相同的功能。它可以像以下代码一样使用:
    std::optional<int> opt = 42;
    auto transformed = opt ? std::make_optional(*opt * 2) : std::nullopt;
    

总结:

  • optional<T> 是一种容器类型,可以包含一个 T 类型的值,也可以为空,非常适合处理可能没有有效结果的计算。
  • 你可以创建空的或包含值的 optional,检查其是否包含值,并有条件地对其内容应用函数。
  • 上述代码示例展示了如何在 optional 内部的值存在时应用转换操作,并使用 boost::none 表示空值。

std::vector<T> 的理解

std::vector<T> 是 C++ 标准库中的一个容器类型,用于存储零个或多个相同类型(T)的元素。它是一个动态数组,能够在程序运行时根据需要自动调整其大小。

std::vector<T> 的主要特点:

  1. 包含零个或多个值
    • std::vector<T> 可以存储任意数量的元素(取决于可用的内存),甚至是零个元素。
    • 这是一个动态大小的容器,可以在运行时根据需要增加或减少元素数量。
  2. 检查元素数量
    • 你可以使用 size() 方法来检查 vector 中的元素个数。
      std::vector<int> vec = {1, 2, 3};
      std::cout << "Size of vector: " << vec.size() << std::endl; // 输出 3
      
  3. 创建包含任意数量元素的 vector
    • vector 容器的大小是动态可变的,可以随时增加或减少元素的数量。它的最大大小仅受可用内存的限制。
      std::vector<int> vec;  // 空 vector
      vec.push_back(10);     // 向 vector 添加元素
      vec.push_back(20);
      
  4. 对所有值调用函数
    • std::vector<T> 提供了一个高效的方式来遍历所有元素并对它们执行操作。你可以使用算法如 std::transform 来将函数应用到所有元素。
    • 在语义上,虽然 std::transform 允许你修改或转换 vector 中的元素,但是这种方式可能不是最优的,因为它会在每次迭代时创建一个新的容器来存储结果。

示例:如何在 vector 中应用函数

假设你有一个整数 vector,并且你想将每个元素乘以 2。使用 std::transform 可以轻松实现:

#include <iostream>
#include <vector>
#include <algorithm>
int main() {// 创建一个包含元素的 vectorstd::vector<int> vec = {1, 2, 3, 4};// 准备一个 transformed vector 来存储转换后的结果std::vector<int> transformed;transformed.reserve(vec.size());  // 预分配空间以提高效率// 使用 std::transform 将每个元素乘以 2std::transform(vec.begin(), vec.end(), std::back_inserter(transformed), [](int x) {return x * 2;});// 输出 transformed vectorfor (int x : transformed) {std::cout << x << " ";  // 输出:2 4 6 8}return 0;
}

代码分析:

  • 创建 transformed 向量transformed.reserve(vec.size()) 预先为 transformed 向量分配足够的内存空间,以避免每次插入新元素时都进行内存重新分配。
  • std::transform:该函数将一个函数 f 应用于 vec 中的每个元素,并将结果插入到 transformed 中。我们用 std::back_inserter 来自动将结果插入到 transformed 向量中。
    std::transform 的原型是:
    std::transform(begin, end, output_begin, func);
    
    • beginend 是原始容器的迭代器(在此为 vec.begin()vec.end())。
    • output_begin 是目标容器的插入点(在此是 std::back_inserter(transformed))。
    • func 是我们对每个元素应用的操作(在此是乘以 2)。

性能优化:

  • 避免不必要的拷贝:通过 reserve 方法来提前为 transformed 分配空间,可以避免每次插入时发生重复的内存分配和拷贝。
  • std::transform 语义std::transform 是一个强大的算法,它可以在一个容器的范围内对每个元素应用函数,并将结果写入另一个容器。虽然它在语义上直观且高效,但它会创建一个新的容器来存储转换后的结果。如果你希望就地修改原始 vector 中的元素,可以直接使用 std::for_eachstd::transform 的原地修改版本。

总结:

  • std::vector<T> 是一个非常灵活且高效的容器,用于存储多个相同类型的元素。
  • 使用 std::transform 可以方便地对 vector 中的所有元素应用函数,并生成一个新的容器来存储转换后的结果。
  • 在实际编程中,通过合理使用 reservestd::back_inserter 等技巧,能够提高 vector 操作的性能。

std::future<T> 的理解

std::future<T> 是 C++ 标准库中的一个类模板,用于表示某个操作的结果,这个结果可能会在未来某个时刻可用,或者根本不可用。它通常与 std::promise<T> 配合使用,以便在异步操作完成后提供结果。

std::future<T> 的主要特点:

  1. 包含将来可能出现的值(或根本没有值)
    • std::future<T> 表示一个值,这个值将在未来某个时刻变得可用,或者可能根本不会有值(例如,如果计算失败或者被取消)。
    • 它通常用于异步编程中,表示一个异步操作的结果。
  2. 检查值是否存在
    • 你可以使用 std::future<T>::valid() 来检查 future 是否持有有效的值。
    • 你还可以使用 std::future<T>::get() 来获取结果。如果结果尚未准备好,它会阻塞当前线程直到值可用。
  3. 创建已准备好的 future
    • 你可以通过 std::promise<T>std::make_ready_future() 来创建一个已准备好的 future,即表示操作已经完成并且结果已经存在。
    • 你还可以通过类似 std::make_exceptional_future() 创建一个包含异常的 future
  4. 调用函数处理值
    • std::future<T> 提供了 .then() 方法(在某些实现中可能是类似的),可以用来在异步操作完成后对结果进行处理。
    • 如果没有 .then(),可以通过创建一个线程并等待 future 结果的方式来处理它,这样做虽然有效,但通常不推荐。

示例:如何使用 std::future<T> 进行异步操作

假设你正在进行一个耗时的计算,并且希望在计算完成后执行某些操作。你可以通过 std::future<T>std::promise<T> 来实现。

#include <iostream>
#include <future>
#include <thread>
// 一个简单的函数,模拟异步计算
int calculate_square(int x) {std::this_thread::sleep_for(std::chrono::seconds(1));  // 模拟延时return x * x;
}
int main() {// 创建一个 promise 和 futurestd::promise<int> promise;std::future<int> fut = promise.get_future();// 启动一个线程来执行计算,并通过 promise 设置结果std::thread t([&promise] {int result = calculate_square(10);  // 计算 10 的平方promise.set_value(result);           // 设置计算结果});// 在 main 线程中使用 future 获取计算结果std::cout << "Waiting for result...\n";int result = fut.get();  // 阻塞直到 future 获取到值std::cout << "Result: " << result << std::endl;// 等待线程完成t.join();return 0;
}

代码分析:

  1. std::promise<int> promise;
    • std::promise 用于设置一个未来值。在此示例中,我们创建了一个类型为 intpromise,表示将来会提供一个整数值。
  2. std::future<int> fut = promise.get_future();
    • 通过 promise.get_future() 获取一个与 promise 相关联的 future。这个 future 会在将来被填充上结果。
  3. 异步计算和设置值
    • 我们创建了一个新线程,在线程内执行 calculate_square(10) 计算 10 的平方,并使用 promise.set_value(result) 设置计算结果。
  4. 阻塞直到结果可用
    • 在主线程中,使用 fut.get() 阻塞并等待直到 future 中有了结果。一旦结果准备好,fut.get() 会返回该值。
  5. t.join();
    • 使用 join() 等待线程执行完成,确保主线程在退出前等待异步线程结束。

示例:如何使用 .then()(假设支持)

在某些 C++ 实现中(例如 C++20 及以后),你可能会看到 std::future 支持 .then() 方法,它可以让你在异步操作完成后直接应用一个函数:

auto transformed = fut.then([](std::future<int> fut) {return fut.get() * 2;  // 获取值并对结果进行转换
});
std::cout << "Transformed result: " << transformed.get() << std::endl;

在这种情况下,.then() 会返回一个新的 future,你可以链式调用它,等待异步操作的结果并继续操作。

性能和注意事项:

  • 阻塞fut.get() 会阻塞当前线程,直到异步操作完成。虽然阻塞是必要的,但在某些情况下,可能会有性能影响。你可以使用 std::async 等更复杂的机制来避免完全阻塞。
  • 异常处理:如果异步操作抛出异常,你可以在 fut.get() 上捕获该异常,或使用 .then() 来处理。
  • 线程安全std::future<T> 是线程安全的,但你只能在一个线程中调用 get() 方法。如果在多个线程中调用 get(),会导致未定义的行为。

总结:

  • std::future<T> 是一个用于表示将来可能会出现的值的类模板,通常与 std::promise<T> 配合使用。
  • 它可以用来处理异步操作的结果,通过 .get() 阻塞直到结果可用,或使用 .then() 等方法来处理异步操作的结果。
  • std::future 在多线程编程中非常有用,能够避免线程之间的直接同步和等待问题。

Functors(函子)

在编程中,函子是一个类型,它封装了另一个类型(或多个类型),并提供一个函数来将某个函数应用于该类型中包含的值。

函子的定义

函子这个概念来源于范畴论,但在编程中我们并不深入探讨范畴论的数学背景。可以简单地理解为,函子是一个容器(或包装器),它包含一个值(或者多个值)。这个容器提供了一个叫做 map(或者在 Haskell 中叫做 fmap)的函数,允许你将一个函数应用于容器内的值,并生成一个新的容器。

函子的类型签名

Haskell 中,fmap 的签名如下:

fmap :: (a -> b) -> f a -> f b

这意味着 fmap 接受一个从 ab 的函数,并且接受一个包含 a 的函子,返回一个包含 b 的函子。

  • ab 是函子内部值的类型。
  • f 是函子本身,它可以是类似 MaybeListOptional 等的容器类型。
  • fmap 将函数应用于函子中的值,返回一个包含新值的函子。

Haskell 中的 fmap 示例

这里是一个 Haskell 中的例子:

data Maybe a = Nothing | Just a
instance Functor Maybe wherefmap _ Nothing  = Nothingfmap f (Just x) = Just (f x)
  • 如果你对 Just 5 使用 fmap 并应用 (*2) 函数,结果将是 Just 10
  • 如果你对 Nothing 使用 fmap,它仍然是 Nothing,因为里面没有值可以应用函数。

C++ 中的函子

在 C++ 中,创建一个函子比在像 Haskell 这样支持函数式编程的语言要复杂一些。但你仍然可以通过模板Lambda 函数来实现函子。

C++ 中的函子示例
#include <iostream>
#include <vector>
#include <algorithm>
// 定义一个函子类型
template<typename T>
class MyFunctor {
public:// 函子的 map 函数(在 C++ 中用 operator() 来实现)T operator()(const T& x) const {return x * 2;  // 示例:将值翻倍}
};
int main() {std::vector<int> numbers = {1, 2, 3, 4, 5};// 创建函子实例MyFunctor<int> fmap;// 使用 std::transform 对 vector 中的每个元素应用函子std::transform(numbers.begin(), numbers.end(), numbers.begin(), fmap);// 输出转换后的值for (int num : numbers) {std::cout << num << " ";  // 输出: 2 4 6 8 10}return 0;
}

解释:

  • MyFunctor: 这是一个将输入整数值翻倍的函子,它通过 operator() 方法来实现。
  • std::transform: 这个标准算法将函子应用于容器中的每个元素(在这个例子中是一个 std::vector<int>)。
  • 结果是一个修改后的 vector,其中每个元素都被函子转换了(翻倍)。

函子的应用场景:

  • 容器: 函子可以封装数据类型(例如容器),并允许你对容器中的每个元素应用函数。
  • std::optionalstd::vectorstd::future: 这些类型可以看作是函子,因为它们封装了值,并且允许你对值进行转换(例如 maptransform)。
  • C++ 中的函数式编程: 尽管 C++ 不是一个纯粹的函数式编程语言,但它支持函子等函数式编程范式,这帮助我们更声明式地编写代码。

总结:

  • 函子(Functor): 是一个类型,它封装一个值或一组值,并允许你应用一个函数来转换这些值。
  • mapfmap(在 Haskell 中)是应用于函子中的值并返回一个新值的函数。
  • C++ 中的函子:C++ 中的函子通常是实现了 operator() 方法的类型,这使得它们在应用到值时表现得像函数一样。它们通常与 std::transform 等算法或容器类型(如 std::vectorstd::optional)一起使用。

Functors pt. 2

在这一部分,我们继续探讨 函子 的概念,并且深入理解它在不同类型中的应用。

我们已经见过 fmap

在之前的幻灯片中,实际上我们已经接触到过 fmap(即映射函数)。我们讨论的许多代码片段,实际上都在实现 fmap。例如,针对不同类型(如 std::optionalstd::vector 等)使用了 maptransform 操作,这些都可以视作是 函子 的应用。

例子

对于 std::optional 类型,fmap 会将一个函数应用于容器内部的值(如果有的话),并返回一个新的 optional 容器。

#include <iostream>
#include <boost/optional.hpp>
int main() {boost::optional<int> opt = 10;auto transformed = opt ? boost::make_optional(*opt + 5) : boost::none;if (transformed) {std::cout << "Transformed value: " << *transformed << std::endl;  // 输出: Transformed value: 15} else {std::cout << "No value!" << std::endl;}return 0;
}

在这个例子中,boost::optional 就是一个函子,我们使用 fmap 操作符将值转换为另一个类型。

Variant 类型作为 vector 的应用

我们可以将 boost::variant 也看作是一个特殊的容器类型(类似于 std::vector),因为它允许将一个函数应用于其包含的多种类型的值。

  • std::variantboost::variant 是容器类型,它们的内部可以包含不同类型的值。
  • 对这种类型的函数对象进行 fmap 操作时,可以根据类型不同对每个值应用不同的操作。
    这种方式在 Haskell 中并不容易实现,因为 Haskell 更加严格地要求类型的统一,而 C++ 通过模板和多态(尤其是函数重载)提供了更灵活的方式来处理这种情况。

Haskell 中的 Bifunctor

Haskell 中,除了标准的 Functor 类型类,还存在一个名为 Bifunctor 的类型类,它允许你对 双值 的容器(例如包含两个值的容器)进行操作。
bimapBifunctor 中的映射函数,它允许你对容器中的两个值分别应用不同的函数。

bimap 的类型签名
bimap :: (a -> b) -> (c -> d) -> f a c -> f b d
  • bimap 接受两个函数:
    • 第一个函数 a -> b 将第一个值从类型 a 转换为类型 b
    • 第二个函数 c -> d 将第二个值从类型 c 转换为类型 d
  • f a c 是一个包含两个值的容器(如一个二元组)。
  • f b d 是转换后的容器,包含转换后的值。
示例:Bifunctor 使用
import Data.Bifunctor
-- 定义一个二元组类型,Bifunctor 实现
instance Bifunctor (,) wherebimap f g (x, y) = (f x, g y)
-- 使用 bimap 函数
main = print $ bimap (+1) (*2) (3, 4)
-- 输出: (4, 8)
  • 在这个例子中,bimap(3, 4) 转换成了 (4, 8),即对第一个值应用 (+1),对第二个值应用 (*2)

总结

  • fmap 在 C++ 中常见的应用方式,如 std::optionalstd::vector 类型,都可以看作是函子的实现,它们通过 map 将函数应用于其包含的值。
  • std::variant 在 C++ 中也表现出类似函子的特性,通过重载函数来处理其内部不同类型的值。
  • Haskell 中的 bimap 允许你同时对双值容器进行操作,并且将两个不同类型的函数应用到容器的不同值上。

函子的应用场景

  • 封装和转换:我们可以用函子对不同类型的数据进行统一的操作。
  • 类型安全:通过封装类型和转换逻辑,函子为我们提供了类型安全的操作方式。
  • 函数式编程:C++ 和其他语言通过支持函子,增强了函数式编程的能力,使得我们能够更高效、更灵活地处理数据。

Functors pt. 3

这一部分继续讨论 函子 的概念,重点讲解了 fmap 的特性及其在函数式编程中的作用。

fmap 是一个伟大的工具
  • fmap 是一个非常有用的工具,它允许我们对一个函子中的值进行轻松的函数调用。它遵循 值语义(value semantics),即使容器中的值被封装在容器里,我们仍然可以以一致的方式对其进行操作。
fmap 定义
  • fmap 的定义有一个关键的特性:它返回一个同类型的 函子。也就是说,应用 fmap 后,得到的结果还是同一种类型的容器,内部包含经过函数转换后的值。
    举个例子,假设我们有一个 std::optional<int> 类型,如果我们对其应用 fmap,那么它依然是一个 std::optional<int> 类型的容器,只不过其中的值已经被应用了某个函数(比如加一)。
Endofunctors(自函子)
  • 由于 fmap 保证了返回的函子是同类型的容器,所以它们被称为 endofunctors(自函子)。Endofunctor 是指那些能够映射到它们自身的函子。
    • 简单来说,endofunctor 就是一个能够接受自己类型作为输入并返回自己类型的函子。对容器应用 fmap 后,得到的还是同类型的容器(即类型保持不变)。
为什么 endofunctor 很有用?
  • endofunctor 这个概念在很多领域都非常重要,尤其是在函数式编程中,它为我们提供了一种一致的方式来操作容器中的值,同时保持容器的类型不变。
  • 这个概念非常有用,尤其是在组合复杂操作时,可以确保容器的类型始终保持一致,并且可以通过不同的函数组合构造出新的操作。
举个简单的例子:
#include <iostream>
#include <optional>
int main() {std::optional<int> opt = 10;// 应用 fmap(通过lambda将值加1)auto transformed = opt ? std::make_optional(*opt + 1) : std::nullopt;if (transformed) {std::cout << "Transformed value: " << *transformed << std::endl;  // 输出: 11} else {std::cout << "No value!" << std::endl;}return 0;
}

在这个例子中,std::optional 是一个 endofunctor,因为应用 fmap 后,它的类型 std::optional<int> 并没有变化。

总结
  • fmap 是一个非常有用的工具,它遵循值语义并允许对容器中的值进行函数调用。
  • endofunctors 是一种特殊的函子,它映射到自己本身,即操作后的容器仍然是同类型的。
  • 函子使得我们可以对不同容器进行一致的操作,同时保持类型安全,并且方便地组合不同的操作。

A Quick Shortcut without Mentioning [ … ]

在这一部分,讲解了 Applicative Functors 的概念,并澄清了之前的解释。接下来,我们将会讨论 Applicative Functors 和它们在 C++ 中的一些实际应用。

I lied again

讲者在这里自嘲,之前的解释可能并不完全准确,因为 Applicative Functors 其实是比普通的 Functor 更强大的工具。通过 Applicative Functors,我们能够将函数本身也包装成一个 Functor,这给我们提供了更多的功能和灵活性。

Applicative Functors 允许你将函数本身包装成 Functor
  • Applicative Functors 的一个重要特点是,你可以将一个 函数 本身也包装成一个 Functor。这意味着你可以像对待普通值一样对待函数,即使这个函数是可选的(如 std::optional),这就为组合函数和容器提供了更多的灵活性。
使用场景:可选的函数和可选的值

举个简单的例子:假设我们有一个 可选函数(比如一个返回 std::optional 的函数)和一个 可选值std::optional<int>),我们可以通过 Applicative Functors 来应用这个函数:

std::optional<int> value = 10;
std::optional<std::function<int(int)>> function = [](int x) { return x + 5; };
// 使用 Applicative Functor 来应用函数
auto result = (function && value) ? std::make_optional((*function)(*value)) : std::nullopt;

在上面的代码中,functionvalue 都是 std::optional 类型的容器。应用 Applicative Functor 后,如果它们都包含有效值,我们就能将函数应用于这个值。否则,结果就是 std::nullopt

从这次讨论的角度来看,Applicative Functors 并没有那么有趣
  • 讲者在这里表示,尽管 Applicative Functors 很强大,但在本次讨论的上下文中,它们可能没有 Functor 那么引人注目,因此没有深入探讨其细节。
总结
  • Applicative Functors 是一种更强大的 Functor 类型,它允许我们将函数本身也包装成 Functor,从而支持对可选的函数和值进行组合操作。
  • 在实际应用中,Applicative Functors 非常有用,尤其是在处理 可选值(如 std::optional)时,可以轻松组合函数和容器。

Monads: join

在这部分,讲者介绍了 Monads 中的一个重要概念:join。我们先来看一下这个概念和代码示例的含义。

什么是 join

在某些情况下,我们可以“扁平化”(flatten)一个 Functor,这就是所谓的 join 操作。对于一些容器类型,如 std::optional,我们有一个嵌套的容器(即一个容器中的元素本身也是一个容器)。通过 join 操作,我们可以将这种嵌套的结构“合并”为一个单一的容器。
换句话说,join 的作用是将 容器中的容器 展开成 单一的容器,从而简化结构。

示例代码:
template<typename T>
boost::optional<T> join(boost::optional<boost::optional<T>> opt)
{return opt ? std::move(*opt) : boost::none;
}

在这段代码中,join 函数的作用是:

  • 输入:一个 boost::optional<boost::optional<T>> 类型,即一个嵌套的 optional
  • 输出:一个 boost::optional<T> 类型,即扁平化后的 optional
    如果外层的 optional 包含值(即 opt 为真),那么 join 就会提取出内层的 optional 并返回它的值。如果外层 optional 是空的(即 optboost::none),那么函数返回一个空的 optionalboost::none)。
理解这个例子的关键点:
  • “扁平化”:通过 join,我们将一个嵌套的容器(例如 boost::optional<boost::optional<T>>)转变成一个单一的容器(boost::optional<T>)。这就像把多个层次的包装去掉,只留下真正的内容。
  • joinMonad:在 Monad 的上下文中,join 是一个非常重要的操作,它用于将嵌套的 Monad 扁平化。通过 join,我们能够将多个层次的 容器(比如 optionalfuture 等)简化成一个层次,使得我们可以更轻松地进行链式操作。
总结:
  • join 是一个操作,它将嵌套的容器结构“合并”或“扁平化”,使得我们能够更直接地访问内层的值。
  • 这个操作在 Monads 中非常常见,它简化了多个层级的结构,使得处理变得更加方便。

Monads: join for std::vector

这段代码演示了如何为 std::vector 类型实现类似于 join 的操作,使得我们能够“扁平化”一个嵌套的 vector

join 的作用:
  • 在这个例子中,join 的作用是将一个嵌套的 std::vector<std::vector<T>>(即一个包含多个 std::vectorvector)展平,变成一个单一的 std::vector<T>,使得原来嵌套的结构成为一维的。
  • 这意味着,原来每个子 vector 中的元素都会被提取出来,全部放入到一个大的 vector 中。
代码分析:
template<typename T>
std::vector<T> join(std::vector<std::vector<T>> vec)
{std::vector<T> ret;// reserve enough space for all elementsfor (auto && v : vec){std::move(v.begin(), v.end(), std::back_inserter(ret));}return ret;
}
  1. 输入
    • vec 是一个 std::vector<std::vector<T>>,即一个包含多个 std::vector<T> 的容器。
  2. 输出
    • ret 是一个扁平化后的 std::vector<T>,它包含了所有子 vector 中的元素。
  3. std::movestd::back_inserter
    • std::move(v.begin(), v.end(), std::back_inserter(ret)) 通过 std::move 将每个子 vector 中的元素移动到 ret 中,而不是复制它们。这是为了提高性能,避免不必要的复制。
    • std::back_inserter(ret) 将元素插入到 ret 的末尾。
  4. reserve
    • 代码中提到的 reserve(通过注释)表示我们可以预先为 ret 分配足够的空间来容纳所有的元素,这样可以避免在插入过程中进行多次内存分配,提高效率。
  5. return
    • 最后,join 返回一个扁平化后的 std::vector<T>,包含了所有子 vector 中的元素。
示例:

假设我们有以下嵌套的 vector

std::vector<std::vector<int>> vec = {{1, 2, 3},{4, 5},{6, 7, 8}
};

通过调用 join

auto result = join(vec);

result 将会是一个扁平化的 std::vector<int>

{1, 2, 3, 4, 5, 6, 7, 8}
总结:
  • join 操作不仅适用于 optional 类型,也适用于 std::vector 类型,它的作用是将嵌套的容器扁平化。
  • 在这个例子中,join 通过移动嵌套 vector 中的元素来创建一个新的、扁平的 vector
  • 这种方式有助于简化数据结构,使得后续的操作变得更加直观和高效。

Monads: join 详解

概念
  • join 操作的基本概念是:将一个嵌套的 m (m a) 类型(即一个包含 m a 的容器)“展平”成一个单一的 m a 类型。
    • 形式化定义:
      join :: m (m a) -> m a
      
    这里,m 代表一个容器(例如 optional, vector 等),而 a 是容器内部的元素类型。
join 的作用
  • join 的主要目的是将一个嵌套容器展平到单一容器。这个操作在许多情况中都非常有用,比如:
    • 对于 optional<optional<T>> 类型,调用 join 可以将其展平为 optional<T>
    • 对于 vector<vector<T>> 类型,调用 join 可以将其展平为 vector<T>
不同容器的 join 实现
  • 对于不同类型的容器,join 执行的操作可能有所不同,并且对于某些类型,join 可能并没有太大意义。
    例如
    • 对于 optional 类型,join 会消除嵌套的 optional,将 optional<optional<T>> 转换为 optional<T>,这是有意义的。
    • 对于某些其他容器类型,join 可能不那么直观或不具备实用意义。
如何使用 joinfmap

假设我们有一系列的计算步骤,每个步骤返回的是一个 optional 类型,形成了嵌套的 optional<optional<T>>

  1. fmap 操作:首先,使用 fmap 将函数应用到 optional 中的值。此时,返回的将是 optional<optional<T>>
    boost::optional<boost::optional<int>> opt1 = ...;
    auto result = opt1 ? boost::make_optional(boost::make_optional(f(*opt1))) : boost::none;
    
  2. join 操作:随后,可以使用 join 操作将嵌套的 optional 展平为 optional<T>,使得结构更加简单。
    boost::optional<int> flatOpt = join(opt1);
    
  3. 再次应用 fmap:最后,您可以在展平后的 optional<T> 上再次应用 fmap,进行进一步的操作。
    auto transformed = flatOpt ? boost::make_optional(f(*flatOpt)) : boost::none;
    
例子说明

假设我们有一个计算步骤 f,它接受一个整数并返回一个 optional<int>

auto f = [](int x) {return x > 0 ? boost::make_optional(x * 2) : boost::none;
};

我们先执行 f,然后使用 join 操作将返回的 optional<optional<int>> 展平为 optional<int>

boost::optional<boost::optional<int>> opt1 = boost::make_optional(boost::make_optional(10));
auto flattened = join(opt1);  // Now flattened is boost::optional<int> with value 10
auto transformed = flattened ? boost::make_optional(f(*flattened)) : boost::none; // f(10) = 20

通过这种方式,您可以轻松地处理包含多个嵌套层次的容器,最终得到一个简单的结构。

总结
  • join 操作的目的是将嵌套的容器展平,使得后续的操作更加简洁。
  • joinfmap 配合使用,可以实现类似于链式计算的过程,每一步都返回一个容器类型,最终通过 join 展平嵌套结构。
  • 对于不同的容器类型,join 的具体实现和意义可能有所不同,但它总是帮助我们将容器中的元素提取并简化。

Monads: mbind 详解

mbind 的基本概念

mbindmonad 中的一个操作符,在 Haskell 中它通常表示为 >>=,其基本功能是将一个 monad 类型的值传递给一个返回 monad 的函数,并返回最终的 monad。

  • Haskell 版本
    (>>=) :: m a -> (a -> m b) -> m b
    x >>= f = join (fmap f x)
    
    这里的 x 是一个包含类型 a 的 monad,而 f 是一个函数,它接受一个 a 类型的值并返回一个 m b 类型的 monad。
如何理解 mbind
  • 通过 >>=,我们能够将一个 monad 的值传递给一个函数,函数的返回值本身也是一个 monad。通过 joinfmap 结合使用,我们可以将嵌套的 monad 展平并获得最终的结果。
    例如,fmap f x 会将 x 中的值传递给 f,这会产生一个新的 monad(通常是嵌套的)。接着,我们通过 join 将嵌套的 monad 展平,从而得到最终的 monadic 值。
在 C++ 中实现 mbind
  • 在 C++ 中,mbind(即 >>=)的行为和 Haskell 中的实现类似,但存在一个问题:C++ 中的操作符 >>= 的结合性与 Haskell 的行为不一样。
    在 C++ 中,如果你写 a >>= b >>= c,它的执行顺序是 (a >>= b) >>= c,而不是 a >>= (b >>= c),这会导致不同的结果。由于 C++ 中的结合性问题,可能需要手动调整括号或修改实现来避免错误。
    • 示例:
      auto x = a >>= b; // This is (a >>= b), not a >>= (b >>= c)
      
mbindjoin 的关系
  • mbindjoin:有时,我们需要实现 mbind 而不是 join,因为 join 实际上是将嵌套的 monad 展平,而 mbind 允许我们通过应用一个函数来链接 monadic 操作。如果你选择实现 mbind,那么 join 实际上可以通过将恒等函数传递给 mbind 来间接实现:
    template <typename T>
    auto join(const std::optional<std::optional<T>>& opt) {return opt ? *opt : std::nullopt;
    }
    // Equivalent to: mbind(x) = join(fmap(identity, x))
    
    • 在这里,identity 函数就是一个将输入值原样返回的函数,fmap 将其应用到 x 上,而 join 则展平嵌套的结果。
C++ 中的实现示例

假设我们有一个 std::optional<T> 类型的 monad,想要实现类似 Haskell 中 >>= 的功能:

template<typename T, typename F>
auto mbind(const std::optional<T>& opt, F&& f) {if (opt) {return f(*opt);  // f returns an std::optional<U>} else {return std::optional<decltype(f(*opt))>();  // Return an empty std::optional}
}

这个 mbind 操作符将一个 std::optional<T> 传递给一个返回 std::optional<U> 类型的函数 f,并返回最终的结果。

总结
  • mbind>>= 是 monad 中的一个重要操作符,用于将一个值(可能包含在 monad 中)传递给一个返回 monad 的函数。它依赖于 joinfmap 的组合。
  • 在 C++ 中实现 mbind 时,需要特别注意操作符的结合性问题,可能需要调整实现或使用额外的括号来确保正确的计算顺序。
  • 有时,使用 mbindjoin 更加直接和清晰,因为它允许我们在 monad 内部进行函数应用,同时保持 monadic 结构不变。

Monads 解析

Monads 是 Functors
  • Functors 是一种可以在其中应用函数的容器,而 Monads 是 Functors 的一种特殊形式。与 Functors 一样,Monads 也能封装一个值并提供一种将函数应用于该值的方式,但它们还提供了更强大的组合和控制机制。
Monads 表示计算的步骤序列
  • Monads 用于描述一系列计算步骤,每一步都可能依赖于前一步的结果。它们能够将计算过程“链式”地连接起来,每个步骤的结果都被封装在一个 monadic 容器中。通过这种方式,Monads 使得复杂的计算变得更加易于组合与管理,尤其是在涉及副作用或异步操作的场景中。
Monads 是“可编程的分号”
  • 这个比喻说明了 Monads 在控制流中的作用。就像分号(;)在程序中表示一个语句的结束和下一个语句的开始一样,Monads 也能表示计算流程中的步骤。具体来说,Monads 能够管理和控制这些计算步骤的顺序,尤其是涉及副作用(比如输入输出操作、状态修改等)时。它们不仅是计算流的“分隔符”,还可以在其中插入特定的逻辑,比如错误处理、状态传递等。

Monads 的关键特点

  1. 封装计算:每个计算步骤的结果都被封装在 monadic 类型中(如 Optional, Future 等),确保计算的一致性和可靠性。
  2. 顺序化计算:通过 bind(Haskell 中是 >>= 操作符),Monads 可以将多个计算步骤串联起来,允许后续步骤依赖于前一步的结果。
  3. 处理副作用:Monads 特别适合处理有副作用的计算,如 IO 操作、状态修改、错误处理等。例如,IO Monad 允许我们在一个函数式程序中处理输入输出。
  4. 统一接口:Monads 提供了统一的接口(如 fmapbindjoin 等),使得不同类型的计算(无论是同步的、异步的、包含副作用的等)都可以通过类似的方式进行组合。

总结

  • Monads 是 Functors 的一种扩展,提供了更强的功能,允许我们定义和组合复杂的计算步骤。
  • Monads 描述计算过程中的每个步骤,它们能够链式地组织计算,保证每个步骤的输出可以作为下一个步骤的输入。
  • Monads 是“可编程的分号”,它们帮助我们控制计算流程,特别是在处理副作用时,让程序更加可控和可组合。

do-notation 解析

do-notation 是 monadic bind 的简写
  • do-notation 是一种用于简化 monadic bind>>=)的语法。它允许你以更直观和易读的方式编写基于 monads 的计算序列,特别是在函数式编程语言中,像 Haskell 就广泛使用 do-notation 来简化代码结构。
如何使用 do-notation
  • do 语法可以帮助你编写多个 monadic 操作,而无需显式地使用 >>=。它的工作方式是,将一个步骤的结果(如 something <- foo)解包并传递到后续的计算中。
  • 例如,下面是 do-notation 的使用示例:
dosomething <- foobarbaz something
  • 上述代码可以转换成标准的 monadic bind 形式:
foo >>= \something -> bar >>= \_ -> baz something
解释
  • 这里的 foo 是一个可能返回 Maybe(类似于 C++ 中的 Optional)的操作。如果 foo 成功返回一个值,则将该值绑定到 something 并继续执行 bar 操作。如果 bar 成功,那么接着执行 baz something
  • do-notation 就是简化了这些重复的 >>= 链接,使得代码更加易于阅读。
举个具体例子
  • 假设我们有一个 Maybe monad(类似于 C++ 的 Optional 类型),并且我们想要按照顺序执行多个可能失败的操作:
dox <- foo  -- foo 返回 Maybey <- bar  -- bar 返回 Maybereturn (x + y)  -- 如果 foo 和 bar 都成功,返回它们的和

这相当于:

foo >>= \x ->bar >>= \y ->return (x + y)
为什么它看起来很熟悉?
  • 其实在 C++ 中,我们也有类似的操作——例如,std::optionalstd::future 都可以类似地链式调用多个操作。虽然 C++ 没有直接的 do-notation,但是我们也可以通过嵌套的函数调用来处理这种依赖计算:
auto result = foo().then([](auto x) {return bar().then([x](auto y) {return x + y;});
});
  • do-notation 的优势在于,它让你可以用更简洁、可读的方式表达这些连续的、依赖的操作,而不必明确每一步的 bind 操作。

总结

  • do-notation 是一种简化 monadic bind 的语法,使得我们在进行多个依赖计算时更加高效和清晰地表达逻辑。
  • 它可以看作是将一系列 >>= 操作包装起来,提供一个更自然的结构,特别适用于处理可能失败或有副作用的计算(如 MaybeFuture)。

do-notation 和 C++ 代码的对比

do-notation 和 C++ try-catch 的相似性
  • do-notation 本质上是一种用于表达一系列依赖操作的语法,它类似于 C++ 中的异常处理机制,尤其是 try-catch 语句。它们都强调“顺序执行”多个操作,并在某些情况下中断执行。
  • 在 Haskell 或其他函数式编程语言中,do-notation 让我们以更直观的方式写出具有依赖关系的计算序列。如果某一步失败(比如值为 NothingFailure),则后续的操作将不会继续执行。
  • 这与 C++ 中的 try-catch 机制非常相似,后者也会在遇到异常时停止当前的代码执行,转而执行异常处理逻辑。
对比示例

假设你有一系列操作,需要检查每个操作是否成功:
Haskell 代码 (使用 do-notation)

dosomething <- foobarbaz something

这在 C++ 中类似于:
C++ 代码 (使用 try-catch)

try {auto something = foo();bar();baz(something);
} catch ( /* ... */ ) {// 处理错误
}
  • Haskell 中的 do 语法将多个操作链接在一起。如果任何一个操作失败,后续的操作就不会被执行,就像是抛出了一个“失败”信号。
  • C++ 中的 try-catch 也有类似的效果:如果 foo()bar() 发生异常,那么程序会跳到 catch 语句中,而不执行 baz(something)
幽默:Do or Do Not; There Is No Try
  • 这句话出自《星球大战》中的尤达大师,他在教卢克如何使用原力时说:“Do or do not; there is no try”(要么做,要么不做,别说‘试试’)。这正是对 do-notation 的一个有趣比喻:在 do-notation 中,我们没有“试试”,我们只做了。如果某个操作失败了,它就不会继续执行下去。
http://www.lqws.cn/news/180253.html

相关文章:

  • git commit 执行报错 sh: -/: invalid option
  • FFmpeg 低延迟同屏方案
  • 局域网聊天室系统的设计与实现【源码+文档】
  • NSSCTF-WEB
  • AI量化透视:金银比突破94阈值,黄金触及4周高点+白银13年新高的联动效应建模
  • 集成电路设计:从概念到实现的完整解析优雅草卓伊凡
  • NLP学习路线图(二十九):BERT及其变体
  • 护网行动面试试题(2)
  • 去除Word文档多余的回车键
  • 如何轻松、安全地管理密码(新手指南)
  • 重构城市应急指挥布控策略 ——无人机智能视频监控的破局之道
  • 基于深度学习的无人机轨迹预测
  • Android动态广播注册收发原理
  • Android设备推送traceroute命令进行网络诊断
  • Ubuntu 系统通过防火墙管控 Docker 容器
  • Linux缓冲区与glibc封装:入门指南
  • 小黑一层层削苹果皮式大模型应用探索:langchain中智能体思考和执行工具的demo
  • 什么是权威解析服务器?权威解析服务器哪些作用?
  • ​​高频通信与航天电子的材料革命:猎板PCB高端压合基材技术解析​​
  • 利用NVivo进行数据可视化,重塑定性研究
  • AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
  • 刷题记录(7)二叉树
  • 使用 Coze 工作流一键生成抖音书单视频:全流程拆解与技术实现
  • scss(sass)中 的使用说明
  • AI生成的基于html+marked.js实现的Markdown转html工具,离线使用,可实时预览 [
  • (转)什么是DockerCompose?它有什么作用?
  • 网络安全逆向分析之rust逆向技巧
  • [论文阅读]TrustRAG: Enhancing Robustness and Trustworthiness in RAG
  • Inxpect安全雷达传感器与控制器:动态检测 + 抗干扰技术重构工业安全防护体系
  • figma 和蓝湖 有什么区别