这个想法围绕着 发现现有代码或系统中的模式,然后将其 泛化 成可重用的抽象,而不是强行将抽象应用于特定的类型或问题。
智能指针
、optional
和 future
都有一个共同点:它们封装了值。尽管这些类型的用途不同(智能指针用于内存管理,optional用于可选值,future用于异步操作),它们都封装了一个可以访问或修改的值。smart_ptr
或 optional
写一个专门的函数),我们写一个 通用的代码,它可以处理任何支持某些操作的类型(比如解引用或访问值)。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
std::vector
、std::list
、std::map
)都有相似的操作(插入、删除、访问)。通过识别这一点,你可以设计一个适用于所有容器的抽象。std::optional
、std::shared_ptr
、std::future
)。print_value
函数可以处理多种输入类型,类似地,你也可以创建一个 统一的操作,比如 map()
,它可以适用于所有类型的容器或封装类型。封装一个值(无论是为了内存管理、可选性还是异步操作)是一个反复出现的模式。我们可以识别出 C++ 中许多类型符合这一模式:
std::unique_ptr
:封装一个动态分配的对象。std::shared_ptr
:封装一个具有共享所有权的对象。std::optional
:封装一个可能不存在的值。std::future
:封装一个异步结果,稍后会有值。template <typename T>
void process_wrapped_value(const T& value) {
if constexpr (std::is_same<T, std::optional<int>>::value) {
// 针对 std::optional 的特殊逻辑
}
else if constexpr (std::is_same<T, std::shared_ptr<int>>::value) {
// 针对 std::shared_ptr 的特殊逻辑
}
else {
// 针对其他类型的通用逻辑
}
}
std::optional<int> box = 5; // 一个装有 int 值的盒子
std::optional<int> box; // 一个没有值的盒子(空的)
std::vector<int> box = {1, 2, 3, 4}; // 一个包含多个值的盒子(列表)
std::tuple<int, double, std::string> box = {1, 3.14, "hello"}; // 一个包含不同类型值的盒子
map
或 transform
。std::optional
):std::optional<int> box = 5;
auto result = box.map([](int value) { return value * 2; }); // 对盒子里的值应用一个函数
std::optional
,如果盒子没有值,调用函数就不会做任何事情。std::optional
):std::optional<int> emptyBox;
auto result = emptyBox.map([](int value) { return value * 2; }); // 不会做任何事情,因为盒子是空的
std::vector
):std::vector<int> box = {1, 2, 3};
std::transform(box.begin(), box.end(), box.begin(), [](int value) { return value * 2; });
// 对 vector 中的每个元素都应用函数
std::variant
):std::variant<int, double> box = 3.14;
std::visit([](auto&& arg) { std::cout << arg; }, box); // 根据类型来应用不同的函数
将 函子 比作一个 盒子 是一个简化的类比,帮助我们理解其概念:
Nothing
)。单子操作符(bind
或 >>=
)帮助我们传递失败的上下文,确保我们不需要在每一步中手动检查空值。map
操作的类型,允许你对结构内的值应用函数,而不改变结构本身。std::optional
就是一个函子,你可以在其中存在值时应用映射函数,而不改变optional
容器本身。optional
是 C++ 中用于表示可能存在或不存在的值的容器类型。它要么包含一个值,要么为空。这种类型非常有用,尤其是在函数的返回值可能没有有效结果时。optional
的主要概念:optional
要么包含一个类型为 T
的值,要么不包含任何值(即为空)。has_value()
或 operator bool()
来检查 optional
是否包含值。std::optional<int> opt = 42;
if (opt) { // 或者 if (opt.has_value())
std::cout << "Value: " << *opt << std::endl; // 解引用获取值
}
optional
:
std::nullopt
(或 Boost 中的 boost::none
)来创建一个空的 optional
。std::optional<int> opt1 = std::nullopt; // 空的
std::optional<int> opt2 = 42; // 包含值 42
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
?optional
更安全,因为你不需要手动检查指针是否为 nullptr
。optional
明确表达了值可能存在或不存在,提升了代码的可读性和理解性。NULL
或 nullptr
),但提供了更好的安全性和清晰度。std::optional
):std::optional
已经成为标准库的一部分,提供与 Boost 的 optional
相同的功能。它可以像以下代码一样使用:std::optional<int> opt = 42;
auto transformed = opt ? std::make_optional(*opt * 2) : std::nullopt;
optional
是一种容器类型,可以包含一个 T
类型的值,也可以为空,非常适合处理可能没有有效结果的计算。optional
,检查其是否包含值,并有条件地对其内容应用函数。optional
内部的值存在时应用转换操作,并使用 boost::none
表示空值。std::vector
的理解std::vector
是 C++ 标准库中的一个容器类型,用于存储零个或多个相同类型(T
)的元素。它是一个动态数组,能够在程序运行时根据需要自动调整其大小。
std::vector
的主要特点:std::vector
可以存储任意数量的元素(取决于可用的内存),甚至是零个元素。size()
方法来检查 vector
中的元素个数。std::vector<int> vec = {1, 2, 3};
std::cout << "Size of vector: " << vec.size() << std::endl; // 输出 3
vector
:
vector
容器的大小是动态可变的,可以随时增加或减少元素的数量。它的最大大小仅受可用内存的限制。std::vector<int> vec; // 空 vector
vec.push_back(10); // 向 vector 添加元素
vec.push_back(20);
std::vector
提供了一个高效的方式来遍历所有元素并对它们执行操作。你可以使用算法如 std::transform
来将函数应用到所有元素。std::transform
允许你修改或转换 vector
中的元素,但是这种方式可能不是最优的,因为它会在每次迭代时创建一个新的容器来存储结果。vector
中应用函数假设你有一个整数 vector
,并且你想将每个元素乘以 2。使用 std::transform
可以轻松实现:
#include
#include
#include
int main() {
// 创建一个包含元素的 vector
std::vector<int> vec = {1, 2, 3, 4};
// 准备一个 transformed vector 来存储转换后的结果
std::vector<int> transformed;
transformed.reserve(vec.size()); // 预分配空间以提高效率
// 使用 std::transform 将每个元素乘以 2
std::transform(vec.begin(), vec.end(), std::back_inserter(transformed), [](int x) {
return x * 2;
});
// 输出 transformed vector
for (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);
begin
和 end
是原始容器的迭代器(在此为 vec.begin()
和 vec.end()
)。output_begin
是目标容器的插入点(在此是 std::back_inserter(transformed)
)。func
是我们对每个元素应用的操作(在此是乘以 2)。reserve
方法来提前为 transformed
分配空间,可以避免每次插入时发生重复的内存分配和拷贝。std::transform
语义:std::transform
是一个强大的算法,它可以在一个容器的范围内对每个元素应用函数,并将结果写入另一个容器。虽然它在语义上直观且高效,但它会创建一个新的容器来存储转换后的结果。如果你希望就地修改原始 vector
中的元素,可以直接使用 std::for_each
或 std::transform
的原地修改版本。std::vector
是一个非常灵活且高效的容器,用于存储多个相同类型的元素。std::transform
可以方便地对 vector
中的所有元素应用函数,并生成一个新的容器来存储转换后的结果。reserve
和 std::back_inserter
等技巧,能够提高 vector
操作的性能。std::future
的理解std::future
是 C++ 标准库中的一个类模板,用于表示某个操作的结果,这个结果可能会在未来某个时刻可用,或者根本不可用。它通常与 std::promise
配合使用,以便在异步操作完成后提供结果。
std::future
的主要特点:std::future
表示一个值,这个值将在未来某个时刻变得可用,或者可能根本不会有值(例如,如果计算失败或者被取消)。std::future::valid()
来检查 future
是否持有有效的值。std::future::get()
来获取结果。如果结果尚未准备好,它会阻塞当前线程直到值可用。future
:
std::promise
或 std::make_ready_future()
来创建一个已准备好的 future
,即表示操作已经完成并且结果已经存在。std::make_exceptional_future()
创建一个包含异常的 future
。std::future
提供了 .then()
方法(在某些实现中可能是类似的),可以用来在异步操作完成后对结果进行处理。.then()
,可以通过创建一个线程并等待 future
结果的方式来处理它,这样做虽然有效,但通常不推荐。std::future
进行异步操作假设你正在进行一个耗时的计算,并且希望在计算完成后执行某些操作。你可以通过 std::future
和 std::promise
来实现。
#include
#include
#include
// 一个简单的函数,模拟异步计算
int calculate_square(int x) {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟延时
return x * x;
}
int main() {
// 创建一个 promise 和 future
std::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;
}
std::promise promise;
:
std::promise
用于设置一个未来值。在此示例中,我们创建了一个类型为 int
的 promise
,表示将来会提供一个整数值。std::future fut = promise.get_future();
:
promise.get_future()
获取一个与 promise
相关联的 future
。这个 future
会在将来被填充上结果。calculate_square(10)
计算 10 的平方,并使用 promise.set_value(result)
设置计算结果。fut.get()
阻塞并等待直到 future
中有了结果。一旦结果准备好,fut.get()
会返回该值。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
是线程安全的,但你只能在一个线程中调用 get()
方法。如果在多个线程中调用 get()
,会导致未定义的行为。std::future
是一个用于表示将来可能会出现的值的类模板,通常与 std::promise
配合使用。.get()
阻塞直到结果可用,或使用 .then()
等方法来处理异步操作的结果。std::future
在多线程编程中非常有用,能够避免线程之间的直接同步和等待问题。在编程中,函子是一个类型,它封装了另一个类型(或多个类型),并提供一个函数来将某个函数应用于该类型中包含的值。
函子这个概念来源于范畴论,但在编程中我们并不深入探讨范畴论的数学背景。可以简单地理解为,函子是一个容器(或包装器),它包含一个值(或者多个值)。这个容器提供了一个叫做 map
(或者在 Haskell 中叫做 fmap
)的函数,允许你将一个函数应用于容器内的值,并生成一个新的容器。
在 Haskell 中,fmap
的签名如下:
fmap :: (a -> b) -> f a -> f b
这意味着 fmap
接受一个从 a
到 b
的函数,并且接受一个包含 a
的函子,返回一个包含 b
的函子。
a
和 b
是函子内部值的类型。f
是函子本身,它可以是类似 Maybe
、List
、Optional
等的容器类型。fmap
将函数应用于函子中的值,返回一个包含新值的函子。fmap
示例这里是一个 Haskell 中的例子:
data Maybe a = Nothing | Just a
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)
Just 5
使用 fmap
并应用 (*2)
函数,结果将是 Just 10
。Nothing
使用 fmap
,它仍然是 Nothing
,因为里面没有值可以应用函数。在 C++ 中,创建一个函子比在像 Haskell 这样支持函数式编程的语言要复杂一些。但你仍然可以通过模板、类和Lambda 函数来实现函子。
#include
#include
#include
// 定义一个函子类型
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
)。vector
,其中每个元素都被函子转换了(翻倍)。std::optional
,std::vector
,std::future
: 这些类型可以看作是函子,因为它们封装了值,并且允许你对值进行转换(例如 map
或 transform
)。map
或 fmap
(在 Haskell 中)是应用于函子中的值并返回一个新值的函数。operator()
方法的类型,这使得它们在应用到值时表现得像函数一样。它们通常与 std::transform
等算法或容器类型(如 std::vector
和 std::optional
)一起使用。在这一部分,我们继续探讨 函子 的概念,并且深入理解它在不同类型中的应用。
fmap
!在之前的幻灯片中,实际上我们已经接触到过 fmap
(即映射函数)。我们讨论的许多代码片段,实际上都在实现 fmap
。例如,针对不同类型(如 std::optional
、std::vector
等)使用了 map
或 transform
操作,这些都可以视作是 函子 的应用。
对于 std::optional
类型,fmap
会将一个函数应用于容器内部的值(如果有的话),并返回一个新的 optional
容器。
#include
#include
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
操作符将值转换为另一个类型。我们可以将 boost::variant
也看作是一个特殊的容器类型(类似于 std::vector
),因为它允许将一个函数应用于其包含的多种类型的值。
std::variant
和 boost::variant
是容器类型,它们的内部可以包含不同类型的值。fmap
操作时,可以根据类型不同对每个值应用不同的操作。在 Haskell 中,除了标准的 Functor 类型类,还存在一个名为 Bifunctor 的类型类,它允许你对 双值 的容器(例如包含两个值的容器)进行操作。
bimap
是 Bifunctor 中的映射函数,它允许你对容器中的两个值分别应用不同的函数。
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
是转换后的容器,包含转换后的值。import Data.Bifunctor
-- 定义一个二元组类型,Bifunctor 实现
instance Bifunctor (,) where
bimap 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::optional
和 std::vector
类型,都可以看作是函子的实现,它们通过 map
将函数应用于其包含的值。std::variant
在 C++ 中也表现出类似函子的特性,通过重载函数来处理其内部不同类型的值。bimap
允许你同时对双值容器进行操作,并且将两个不同类型的函数应用到容器的不同值上。这一部分继续讨论 函子 的概念,重点讲解了 fmap 的特性及其在函数式编程中的作用。
fmap
是一个非常有用的工具,它允许我们对一个函子中的值进行轻松的函数调用。它遵循 值语义(value semantics),即使容器中的值被封装在容器里,我们仍然可以以一致的方式对其进行操作。fmap
的定义有一个关键的特性:它返回一个同类型的 函子。也就是说,应用 fmap
后,得到的结果还是同一种类型的容器,内部包含经过函数转换后的值。std::optional
类型,如果我们对其应用 fmap
,那么它依然是一个 std::optional
类型的容器,只不过其中的值已经被应用了某个函数(比如加一)。fmap
保证了返回的函子是同类型的容器,所以它们被称为 endofunctors(自函子)。Endofunctor 是指那些能够映射到它们自身的函子。
fmap
后,得到的还是同类型的容器(即类型保持不变)。#include
#include
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
并没有变化。
fmap
是一个非常有用的工具,它遵循值语义并允许对容器中的值进行函数调用。在这一部分,讲解了 Applicative Functors 的概念,并澄清了之前的解释。接下来,我们将会讨论 Applicative Functors 和它们在 C++ 中的一些实际应用。
讲者在这里自嘲,之前的解释可能并不完全准确,因为 Applicative Functors 其实是比普通的 Functor 更强大的工具。通过 Applicative Functors,我们能够将函数本身也包装成一个 Functor,这给我们提供了更多的功能和灵活性。
std::optional
),这就为组合函数和容器提供了更多的灵活性。举个简单的例子:假设我们有一个 可选函数(比如一个返回 std::optional
的函数)和一个 可选值(std::optional
),我们可以通过 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;
在上面的代码中,function
和 value
都是 std::optional
类型的容器。应用 Applicative Functor 后,如果它们都包含有效值,我们就能将函数应用于这个值。否则,结果就是 std::nullopt
。
std::optional
)时,可以轻松组合函数和容器。在这部分,讲者介绍了 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>
类型,即一个嵌套的 optional
。boost::optional
类型,即扁平化后的 optional
。optional
包含值(即 opt
为真),那么 join
就会提取出内层的 optional
并返回它的值。如果外层 optional
是空的(即 opt
为 boost::none
),那么函数返回一个空的 optional
(boost::none
)。boost::optional>
)转变成一个单一的容器(boost::optional
)。这就像把多个层次的包装去掉,只留下真正的内容。join
与 Monad
:在 Monad 的上下文中,join
是一个非常重要的操作,它用于将嵌套的 Monad
扁平化。通过 join
,我们能够将多个层次的 容器(比如 optional
、future
等)简化成一个层次,使得我们可以更轻松地进行链式操作。join
是一个操作,它将嵌套的容器结构“合并”或“扁平化”,使得我们能够更直接地访问内层的值。join
for std::vector
这段代码演示了如何为 std::vector
类型实现类似于 join 的操作,使得我们能够“扁平化”一个嵌套的 vector。
join
的作用:join
的作用是将一个嵌套的 std::vector>
(即一个包含多个 std::vector
的 vector
)展平,变成一个单一的 std::vector
,使得原来嵌套的结构成为一维的。vector
中的元素都会被提取出来,全部放入到一个大的 vector
中。template<typename T>
std::vector<T> join(std::vector<std::vector<T>> vec)
{
std::vector<T> ret;
// reserve enough space for all elements
for (auto && v : vec)
{
std::move(v.begin(), v.end(), std::back_inserter(ret));
}
return ret;
}
vec
是一个 std::vector>
,即一个包含多个 std::vector
的容器。ret
是一个扁平化后的 std::vector
,它包含了所有子 vector
中的元素。std::move
和 std::back_inserter
:
std::move(v.begin(), v.end(), std::back_inserter(ret))
通过 std::move
将每个子 vector
中的元素移动到 ret
中,而不是复制它们。这是为了提高性能,避免不必要的复制。std::back_inserter(ret)
将元素插入到 ret
的末尾。reserve
:
reserve
(通过注释)表示我们可以预先为 ret
分配足够的空间来容纳所有的元素,这样可以避免在插入过程中进行多次内存分配,提高效率。return
:
join
返回一个扁平化后的 std::vector
,包含了所有子 vector
中的元素。假设我们有以下嵌套的 vector
:
std::vector<std::vector<int>> vec = {
{1, 2, 3},
{4, 5},
{6, 7, 8}
};
通过调用 join
:
auto result = join(vec);
result
将会是一个扁平化的 std::vector
:
{1, 2, 3, 4, 5, 6, 7, 8}
join
操作不仅适用于 optional
类型,也适用于 std::vector
类型,它的作用是将嵌套的容器扁平化。join
通过移动嵌套 vector
中的元素来创建一个新的、扁平的 vector
。join
详解join
操作的基本概念是:将一个嵌套的 m (m a)
类型(即一个包含 m a
的容器)“展平”成一个单一的 m a
类型。
join :: m (m a) -> m a
m
代表一个容器(例如 optional
, vector
等),而 a
是容器内部的元素类型。join
的作用join
的主要目的是将一个嵌套容器展平到单一容器。这个操作在许多情况中都非常有用,比如:
optional>
类型,调用 join
可以将其展平为 optional
。vector>
类型,调用 join
可以将其展平为 vector
。join
实现join
执行的操作可能有所不同,并且对于某些类型,join
可能并没有太大意义。optional
类型,join
会消除嵌套的 optional
,将 optional>
转换为 optional
,这是有意义的。join
可能不那么直观或不具备实用意义。join
和 fmap
假设我们有一系列的计算步骤,每个步骤返回的是一个 optional
类型,形成了嵌套的 optional
:
fmap
操作:首先,使用 fmap
将函数应用到 optional
中的值。此时,返回的将是 optional>
。boost::optional<boost::optional<int>> opt1 = ...;
auto result = opt1 ? boost::make_optional(boost::make_optional(f(*opt1))) : boost::none;
join
操作:随后,可以使用 join
操作将嵌套的 optional
展平为 optional
,使得结构更加简单。boost::optional<int> flatOpt = join(opt1);
fmap
:最后,您可以在展平后的 optional
上再次应用 fmap
,进行进一步的操作。auto transformed = flatOpt ? boost::make_optional(f(*flatOpt)) : boost::none;
假设我们有一个计算步骤 f
,它接受一个整数并返回一个 optional
:
auto f = [](int x) {
return x > 0 ? boost::make_optional(x * 2) : boost::none;
};
我们先执行 f
,然后使用 join
操作将返回的 optional
展平为 optional
:
boost::optional<boost::optional<int>> opt1 = boost::make_optional(boost::make_optional(10));
auto flattened = join(opt1); // Now flattened is boost::optional with value 10
auto transformed = flattened ? boost::make_optional(f(*flattened)) : boost::none; // f(10) = 20
通过这种方式,您可以轻松地处理包含多个嵌套层次的容器,最终得到一个简单的结构。
join
操作的目的是将嵌套的容器展平,使得后续的操作更加简洁。join
和 fmap
配合使用,可以实现类似于链式计算的过程,每一步都返回一个容器类型,最终通过 join
展平嵌套结构。join
的具体实现和意义可能有所不同,但它总是帮助我们将容器中的元素提取并简化。mbind
详解mbind
的基本概念mbind
是 monad 中的一个操作符,在 Haskell 中它通常表示为 >>=
,其基本功能是将一个 monad 类型的值传递给一个返回 monad 的函数,并返回最终的 monad。
(>>=) :: m a -> (a -> m b) -> m b
x >>= f = join (fmap f x)
这里的 x
是一个包含类型 a
的 monad,而 f
是一个函数,它接受一个 a
类型的值并返回一个 m b
类型的 monad。mbind
>>=
,我们能够将一个 monad 的值传递给一个函数,函数的返回值本身也是一个 monad。通过 join
和 fmap
结合使用,我们可以将嵌套的 monad 展平并获得最终的结果。fmap f x
会将 x
中的值传递给 f
,这会产生一个新的 monad(通常是嵌套的)。接着,我们通过 join
将嵌套的 monad 展平,从而得到最终的 monadic 值。mbind
mbind
(即 >>=
)的行为和 Haskell 中的实现类似,但存在一个问题:C++ 中的操作符 >>=
的结合性与 Haskell 的行为不一样。a >>= b >>= c
,它的执行顺序是 (a >>= b) >>= c
,而不是 a >>= (b >>= c)
,这会导致不同的结果。由于 C++ 中的结合性问题,可能需要手动调整括号或修改实现来避免错误。
auto x = a >>= b; // This is (a >>= b), not a >>= (b >>= c)
mbind
与 join
的关系mbind
与 join
:有时,我们需要实现 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
则展平嵌套的结果。假设我们有一个 std::optional
类型的 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
} else {
return std::optional<decltype(f(*opt))>(); // Return an empty std::optional
}
}
这个 mbind
操作符将一个 std::optional
传递给一个返回 std::optional
类型的函数 f
,并返回最终的结果。
mbind
(>>=
) 是 monad 中的一个重要操作符,用于将一个值(可能包含在 monad 中)传递给一个返回 monad 的函数。它依赖于 join
和 fmap
的组合。mbind
时,需要特别注意操作符的结合性问题,可能需要调整实现或使用额外的括号来确保正确的计算顺序。mbind
比 join
更加直接和清晰,因为它允许我们在 monad 内部进行函数应用,同时保持 monadic 结构不变。;
)在程序中表示一个语句的结束和下一个语句的开始一样,Monads 也能表示计算流程中的步骤。具体来说,Monads 能够管理和控制这些计算步骤的顺序,尤其是涉及副作用(比如输入输出操作、状态修改等)时。它们不仅是计算流的“分隔符”,还可以在其中插入特定的逻辑,比如错误处理、状态传递等。Optional
, Future
等),确保计算的一致性和可靠性。bind
(Haskell 中是 >>=
操作符),Monads 可以将多个计算步骤串联起来,允许后续步骤依赖于前一步的结果。IO
Monad 允许我们在一个函数式程序中处理输入输出。fmap
、bind
、join
等),使得不同类型的计算(无论是同步的、异步的、包含副作用的等)都可以通过类似的方式进行组合。>>=
)的语法。它允许你以更直观和易读的方式编写基于 monads 的计算序列,特别是在函数式编程语言中,像 Haskell 就广泛使用 do-notation 来简化代码结构。do
语法可以帮助你编写多个 monadic 操作,而无需显式地使用 >>=
。它的工作方式是,将一个步骤的结果(如 something <- foo
)解包并传递到后续的计算中。do
something <- foo
bar
baz something
foo >>= \something -> bar >>= \_ -> baz something
foo
是一个可能返回 Maybe
(类似于 C++ 中的 Optional
)的操作。如果 foo
成功返回一个值,则将该值绑定到 something
并继续执行 bar
操作。如果 bar
成功,那么接着执行 baz something
。do-notation
就是简化了这些重复的 >>=
链接,使得代码更加易于阅读。Maybe
monad(类似于 C++ 的 Optional
类型),并且我们想要按照顺序执行多个可能失败的操作:do
x <- foo -- foo 返回 Maybe
y <- bar -- bar 返回 Maybe
return (x + y) -- 如果 foo 和 bar 都成功,返回它们的和
这相当于:
foo >>= \x ->
bar >>= \y ->
return (x + y)
std::optional
和 std::future
都可以类似地链式调用多个操作。虽然 C++ 没有直接的 do-notation
,但是我们也可以通过嵌套的函数调用来处理这种依赖计算:auto result = foo().then([](auto x) {
return bar().then([x](auto y) {
return x + y;
});
});
bind
操作。>>=
操作包装起来,提供一个更自然的结构,特别适用于处理可能失败或有副作用的计算(如 Maybe
或 Future
)。try-catch
语句。它们都强调“顺序执行”多个操作,并在某些情况下中断执行。do-notation
让我们以更直观的方式写出具有依赖关系的计算序列。如果某一步失败(比如值为 Nothing
或 Failure
),则后续的操作将不会继续执行。try-catch
机制非常相似,后者也会在遇到异常时停止当前的代码执行,转而执行异常处理逻辑。假设你有一系列操作,需要检查每个操作是否成功:
Haskell 代码 (使用 do-notation
):
do
something <- foo
bar
baz something
这在 C++ 中类似于:
C++ 代码 (使用 try-catch
):
try {
auto something = foo();
bar();
baz(something);
} catch ( /* ... */ ) {
// 处理错误
}
do
语法将多个操作链接在一起。如果任何一个操作失败,后续的操作就不会被执行,就像是抛出了一个“失败”信号。try-catch
也有类似的效果:如果 foo()
或 bar()
发生异常,那么程序会跳到 catch
语句中,而不执行 baz(something)
。do-notation
的一个有趣比喻:在 do-notation
中,我们没有“试试”,我们只做了。如果某个操作失败了,它就不会继续执行下去。