Monad是函数式编程中的一种设计模式,它允许一般地构造程序,同时自动消除程序逻辑所需的 boilerplate code。
Monad通过提供自己的数据类型(每种monad的特定类型)来实现此目的,该数据类型代表一种特定的计算形式,同时还提供了一种在Monad中包装任何基本类型的值的程序(产生monadic值),以及另一个到撰写功能即输出一元值(称为一元函数)。
而错误处理是编程中一个广泛的主题,尤其是在C ++中。我今天想向您介绍的方面是如何处理多个错误。
让我们考虑以下4个功能:
int f1(int a);
int f2(int b, int c);
int f3(int d);
int f4(int e);
f4( f4( f3( f2( f1(42), f1(55) ) ) ) )
应该依次调用这些函数:将f1
的结果(两次调用)传递给f2
,然后将f2
的结果传递给f3
,依此类推。到目前为止,一切都很好。
现在让我们说它们每个都可能失败。也就是说,它们通常返回整数,但是在某些情况下,它们只是无法构建返回值。这实际上在现实生活中是有意义的。
sqrt
如果您将负数传递给它,将不知道该怎么办。std::stoi `如果传递给它的字符串不代表一个整数,则将无法返回整数。这两个示例均取自该标准,但是这也发生在用户代码中。有时,函数只是无法返回结果。
这是一个相当简单的问题,但是有几种解决方案。我们已经详细了解了如何使用来使一个给定 一个clearer function
通过使用optional<T>
。现在,让我们看看如何处理错误,其中 连续调用了多个函数,每个函数可能会失败。
以往的多重错误处理
C ++的function
深深地扎在C ++的根源中。处理多种错误处理的一种方法是将错误(或成功)状态保留在标志中。
为此,让我们修改函数的接口:
bool f1(int a, int& result);
bool f2(int b, int c, int& result);
bool f3(int d, int& result);
bool f4(int e, int& result);
我们必须同意所有函数都返回一个标志,表示……表示成功。
调用就像这样:
bool success = true;
int b1 = 0;
int b2 = 0;
int c = 0;
int d = 0;
int result = 0;
success &= f1(3, b1);
success &= f1(4, b2);
success &= f2(b1, b2, c);
success &= f3(c, d);
success &= f4(d, result);
if (success)
{
// we can use result
}
else
{
// we know that something went wrong
}
没关系……当您习惯使用C时。但这在C ++中绝对不是很酷。
这里的主要问题是,函数应按返回类型提供输出。这使得代码更加清晰自然。
此解决方案的其他问题包括:
- 我们被迫在动作发生之前声明所有变量(最好使用默认值)
- 而且这些函数中出现的问题并没有真正说明它们是否意味着错误或成功。
所以这不是要走的路。但是我认为值得看这个例子,因为这可以在生产代码中找到。
抛出异常
一种更现代的处理方式是使函数仅在空中挥舞手臂,告诉大家这里抛出了异常。
这样,原始界面保持不变。 如果函数成功,它将提供一个整数。 如果不是这样,则说明您不在这里,并且栈缠绕stack wound
(入栈)一直到遇到catch
为止。 这样,我们就知道代码何时成功执行,并且函数的初始接口不必更改。
不幸的是,抛出异常并不是那么简单,并且会带来后果。一个是性能方面的考虑。另一个重要的事情是,引发异常的的代码,以及围绕它展开的代码也必须是异常安全的(解译者注:解释在下方)。这不是偶然发生的,并不是所有的代码都是异常安全的。
下面为译者注:
假如你有一份C++98代码,里面用到了std::vector<Widget>
。Widget
通过push_back
一次又一次的添加进std::vector
:
std::vector<Widget> vw;
…
Widget w;
… // work with w
vw.push_back(w); // add w to vw
假设这个代码能正常工作,你也无意修改为C++11风格。但是你确实想要C++11移动语义带来的性能优势,毕竟这里的类型是可以移动的(move-enabled types)。因此你需要确保Widget有移动操作,可以手写代码也可以让编译器自动生成,当然前提是自动生成的条件能满足(参见Item 17)。
当新元素添加到std::vector
,std::vector
可能没地方放它,换句话说,std::vector
的大小(size)等于它的容量(capacity)。这时候,std::vector
会分配一片的新的大块内存用于存放,然后将元素从已经存在的内存移动到新内存。在C++98中,移动是通过复制老内存区的每一个元素到新内存区完成的,然后老内存区的每个元素发生析构。 这种方法使得push_back
可以提供很强的异常安全保证:如果在复制元素期间抛出异常,std::vector
状态保持不变,因为老内存元素析构必须建立在它们已经成功复制到新内存的前提下。
在C++11中,一个很自然的优化就是将上述复制操作替换为移动操作。但是很不幸运,这回破坏push_back
的异常安全。如果n个元素已经从老内存移动到了新内存区,但异常在移动第n+1个元素时抛出,那么push_back操作就不能完成。但是原始的std::vector已经被修改:有n个元素已经移动走了。恢复std::vector至原始状态也不太可能,因为从新内存移动到老内存本身又可能引发异常。
这是个很严重的问题,因为老代码可能依赖于push_back
提供的强烈的异常安全保证。因此,C++11版本的实现不能简单的将push_back
里面的复制操作替换为移动操作,除非知晓移动操作绝不抛异常,这时复制替换为移动就是安全的,唯一的副作用就是性能得到提升。
我们亲爱的朋友 optional<T>
实际上,我们一直在考虑使用可选功能来提高一个函数的错误处理的表达能力。
因此,让我们更改函数的接口以返回 optional
( 译者注:可以参考我的另一篇文章std::optional):
#include <boost/optional.hpp>
boost::optional<int> f1(int a);
boost::optional<int> f2(int b, int c);
boost::optional<int> f3(int d);
boost::optional<int> f4(int e);
我在这里故意使用boost
选项,因为在撰写本文时,它比C ++ 17 中std::optional
更加有用。但是,所有后面也适用于std::optional中,您只需更换boost 用std 和none 通过nullopt。
现在的问题是,可选内容如何构成?答案是:badly 。
实际上,可以在if语句(它具有到bool的转换)中检查每个可选项,以确定该函数是否成功。这给出了以下代码:
boost::optional<int> result;
boost::optional<int> b = f(3);
if (b)
{
boost::optional<int> c = f(4);
if (c)
{
boost::optional<int> d = g(*b, *c);
if (d)
{
boost::optional<int> e = h(*d);
if (e)
{
result = h(*e);
}
}
}
}
if (result)
{
// we can use *result
}
else
{
// we know that something went wrong
}
这些if语句彼此嵌套,通常可以在同一例程中使用多个可选参数在代码中看到。这感觉不对。确实,您可以感觉到代码太多了,对吧?
我们想要做的事情可以简单地说:继续计算直到一个函数失败,方法是返回一个空的可选值。但是上面的代码看起来抽象水平太低了,因为它显示了实现此目标的所有机制。
但是没有办法封装if语句吗?
C ++中的可选monad
事实证明,这可以通过使用来自函数编程的想法(称为monad)来实现。这在Haskell等语言中大量使用。
首先,让我澄清一件事:我什至不试图解释什么是monad
。确实,monads
似乎不能简单地解释。(你可以参考这篇文章:Abstraction, intuition, and the “monad tutorial fallacy”)
似乎有两种人:懂monad
的人和不懂monad
的人。两者之间没有沟通的桥梁。因此,一旦您了解了monad
,您就失去了向某人简单解释它们的全部能力。老实说,我不确定我属于哪一部分,这使情况变得更加混乱。
好消息是,您无需了解Haskell
,也无需对monad
有牢牢的了解即可了解后续情况。我想向您展示一种非常实用的,面向C ++的方式来处理optional受monad启发的多种方法。我在 2016年C ++ Now上David Sankel的精彩演讲中发现了这一点。
这个想法是要编写一个函数,该函数能够结合一个optional<T>
并返回optional<U>
。确实,这与我们的情况相对应,其中T和U为int。
将optional<T>
称为t ,函数f 以及函数的主体编写起来很简单:
if (t)
{
return f(*t);
}
else
{
return boost::none;
}
这是if语句被封装的地方。
现在,此函数的原型需要考虑两个方面:
- 我们使它成为一个运算符,而不是一个函数。正如您稍后将看到的那样,当将对各种函数的调用链接在一起时,这将使语法更好。我们选择operator>>=,(有一些用处 operator>>,但我建议使用此方法,因为它不能与流中模板化的流运算符冲突,也因为它恰好是Haskell中使用的一种)。
- 函数必须与任何可调用的类型(函数,函数指针std::function,lambda或其他函数对象)兼容。为此,我知道的唯一方法是使用模板参数。有些人使用,std::function但我不知道他们如何将lambda传递给它。
这是生成的原型:
template<typename T, typename TtoOptionalU>
auto operator>>=(boost::optional<T> const& t, TtoOptionalU f) -> decltype(f(*t))
要使用它,我们将每个函数返回的optional<int>
(代表 optional)与带有int
的lambda 结合在一起。这INT代表在T 在 TtoOptionalU
中。这将发生的是,如果此可选内容为空,则operator>>= just
返回一个空的可选内容。否则,它将下一个函数应用于可选中的值:
boost::optional<int> result = f(3) >>= [=](int b) // b is the result of f(3) if it succeeds
{ return f(4) >>= [=](int c) // c is the result of f(4) if it succeeds
{ return g2(b, c) >>= [=](int d) // and so on
{ return h(d) >>= [=](int e)
{ return h(e);
};};};};
也许您会更喜欢使用其他缩进:
boost::optional<int> result3 = f(3) >>= [=](int b) {
return f(4) >>= [=](int c) {
return g2(b, c) >>= [=](int d) {
return h(d) >>= [=](int e) {
return h(e);
};
};
};
};
将此代码与带有可选选项的初始试用版进行比较。if语句不见了。
但是出现了一种不寻常的语法。而且该技术比旧的C样式版本更加复杂。这个可以吗?如果您对函数式编程有一定的经验,那么您会更容易找到这种自然的方法。否则,您必须确定声明式样式是否值得。
但是,无论您是否认为这是一个可行的选择,我认为都是值得理解的,因为它说明了不同的编程范例。
公平地说,我必须指出,如果这些函数之一未返回可选值,而是直接返回一个int,则必须将其结果包装到可选值中。因为operator>>= 只期望可选。另一方面,在最初的示例中,使用optional的if函数将不需要。
如果您了解所有内容,但发现无法将自己的想法笼罩在全局概念中,那完全可以。这不容易。仔细看看最后一个示例,也许尝试自己编写它,这应该会越来越清晰。
template <typename R, typename ... P>
auto make_failable(R (*f)(P ... ps))
{
return [f](std::optional<P> ... xs) -> std::optional<R>
{
if ((xs && ...)) {
return {f(*(xs)...)};
} else {
return {};
}
};
}
auto failable_f1 = make_failable(f1);
auto failable_f2 = make_failable(f2);
auto failable_f3 = make_failable(f3);
auto failable_f4 = make_failable(f4);
make_failable
接受一个函数f(例如我们的API中的一个函数),然后返回一个新函数,该函数实质上将调用转发给,f 但处理optionals
并检查失败。可变参数模板允许使用任意数量的参数包装函数,并且xs && ... fold
是折叠表达式,出现在C ++ 17中。请注意,此特定实现接受function,但不接受更通用的可调用对象。还要注意,从C ++ 17开始, std::optional
它不接受引用(boost::optional 接受)
就是这样!我们可以使用支持optionals的这些函数来代替原始函数,它们将做正确的事情。例如,如果x 和y 是optionals,则以下表达式:
failable_f4( failable_f4( failable_f3( failable_f2( failable_f1(x), failable_f1(y) ) ) ) )
返回API的原始调用将返回的内容,optional
如果同时 包含x 且y包含一个值,则包装到一个if中,std::nullopt
否则返回。而且此调用代码不必担心在调用链的每个步骤中检查失败。
参考: