正如Matthieu M.所说的,这对于C++来说是相对较新的事情,但对许多函数式语言来说却并非新鲜事物。
我想在这里补充我的观点:在我看来,部分困难和差异可以在"过程式vs函数式"方法中找到。我想使用Scala(因为我熟悉Scala和C++,而且我觉得它有一个更接近Expected<T>
的工具(Option))来说明这种区别。
在Scala中,你有Option[T],它可以是Some(t)或None。特别地,也可以有Option[Unit],它在道义上等同于Expected<void>
。
在Scala中,使用模式非常类似,并围绕着2个函数构建:isDefined()和get()。但它还有一个"map()"函数。
我喜欢把"map"看作是"isDefined + get"的函数式等价物:
if (opt.isDefined)
opt.get.doSomething
变成
val res = opt.map(t => t.doSomething)
"propagating" 选项到结果中
我认为在使用和组合选项的这种函数式风格中,就包含了你问题的答案:
那么如果你有另一个函数toUpper(s),它会就地修改字符串,并且没有返回值,你的代码会是什么样子呢?
个人而言,我不会就地修改字符串,或者至少不会返回空值。我把Expected看作是一个“函数式”的概念,需要一种函数式模式才能很好地工作:toUpper(s)要么返回一个新的字符串,要么在修改后返回它本身。
auto s = toUpper(s)
s.get()
或者,使用类似于Scala的map。
val finalS = toUpper(s).map(upperS => upperS.someOtherManipulation)
如果您不想遵循函数式路线,可以使用isDefined/valid,并以更加过程化的方式编写代码:
auto s = toUpper(s);
if (s.valid())
....
如果您按照这条路线(可能是因为需要),则需要注意“void vs. unit”:从历史上看,void并不被视为一种类型,而是“没有类型”(void foo()被认为类似于Pascal过程)。在函数式语言中使用的Unit更多地被视为表示“计算”的类型。因此,返回Option[Unit]更有意义,被视为“可能做了某些事情的计算”。在
Expected<void>
中,void具有类似的含义:一种计算,当它按预期工作时(没有异常情况),就会结束(不返回任何内容)。至少,在我看来是这样!
因此,使用Expected或Option[Unit]可以被视为可能产生结果的计算,也可能不产生结果。将它们链接在一起会使其变得困难:
auto c1 = doSomething(s); //do something on s, either succeed or fail
if (c1.valid()) {
auto c2 = doSomethingElse(s); //do something on s, either succeed or fail
if (c2.valid()) {
...
不是很干净。
在Scala中使用Map可以使代码变得更加简洁。
doSomething(s) //do something on s, either succeed or fail
.map(_ => doSomethingElse(s) //do something on s, either succeed or fail
.map(_ => ...)
哪个更好,但仍然远非理想。在这里,Maybe单子显然胜出...但那是另一回事了。