C++23 可选的一元绑定和一元返回是什么?

NoS*_*tAl 14 c++ monads stdoptional c++23

C++23std::optional终于得到了一些非常有用的补充。

由于我对 FP 的了解非常原始,我想知道以下两个操作的语法是什么(根据我的谷歌搜索,这是两个基本的一元操作):

  1. 单子绑定
  2. 单子回归

我最好的猜测是:

单子绑定就是变换

Monadic 返回只是 C++17std::optional 构造函数(8)

Enr*_*lis 15

mbind(它不存在,我模仿 Haskell 的>>=

\n

在类似 C++ 的伪代码中,一元绑定(我们称之为mbind)应该具有这样的签名:

\n
C<U> mbind(C<T>, std::function<C<U>(T)>);\n
Run Code Online (Sandbox Code Playgroud)\n

C即它应该采用某种类型的monad T,一个“将该monad 的内部C拉出”的函数,并变成(不一定)不同类型的monad UC<U>并返回给您C<U>

\n

transform(免费功能)

\n

transform首先,你提到的是一个成员函数,它有一个这样的签名

\n
C<U> C<T>::transform(std::function<U(T)>);\n
Run Code Online (Sandbox Code Playgroud)\n

但让我们重写它的签名,就像它是一个自由函数一样:

\n
C<U> transform(C<T>, std::function<U(T)>);\n
Run Code Online (Sandbox Code Playgroud)\n

正如您所看到的,它需要 a并在函子内部从右到右C<T>应用一个函数,从而产生 a 。TU CC<U>

\n

所以是有区别的。

\n

为了更好地理解区别是什么,请尝试传递transform一个C<T>和一个带有期望签名的函数mbindstd::function<C<U>(T)>

\n

你得到了什么?请记住,transform“在函子内部应用该函数,而不拉出任何东西”,因此您会得到一个C<C<U>>,即又一个函子层。

\n

mbind,相反,使用相同的两个参数,会给你一个C<U>.

\n

如何从transform(x, f)返回值转到mbind(x, f)返回值,即从 aC<C<U>>到 a C<U>join你可以通过Haskell 和flatten其他语言中的所谓方法来展平/连接/折叠/任何你想命名的两个函数级别。

\n

显然,如果你能做到这一点(并且你可以为C = std::optional),那么那些“函数层”实际上是“单子层”。

\n

那么有mbind类似的东西吗?

\n

正如其他答案中所建议的,有and_then成员函数。

\n

是我上面提到的and_then(不存在的)吗?是的,它们之间的唯一区别与成员函数和自由函数mbind相同:一个是成员函数,另一个是自由函数。(\xe2\x80\xa0)transformtransform

\n

顺便问一下flatten/在哪里?join

\n

您可能想知道 C++23 中是否有这个实用程序。我完全不知道,我几乎不知道 C++20 提供了什么。

\n

然而,由于创建函子的函数std::optional被定义为它自己的成员函数std::optional,我坚信,如果存在单子绑定函数 for std::optional,它也会被定义为成员函数,在这种情况下,它会在这个页面,在Monadic 操作部分中,与and_then//一起。既然不存在,我倾向于假设它不存在。transformor_else

\n

但是 Haskell 的一些知识可以帮助我了解为什么可能没有必要将其添加到标准中。

\n

如果你这样做会发生什么?

\n
auto optOfOpt = std::make_optional(std::make_optional(3));\nauto whatIsThis = optOfOpt.and_then(std::identity{});\n
Run Code Online (Sandbox Code Playgroud)\n

是的,就是这样:调用.and_then(std::identity{})嵌套可选相当于wannabe .join()

\n

其他图书馆呢?

\n

Boost.HanaFunctor定义了、Applicative、和许多其他概念Monad,为您提供了一种自动实现利用这些概念的所有抽象的方法,但代价是给出一些最小的定义。例如,如果您在for中定义了transform_impland ,那么您就可以在 it 上使用、、、以及任何其他需要 monad 或更少的操作。flatten_implnamespace boost::hanastd::optionaltransformflattenchainap

\n

例如,以下是工作代码(编译器资源管理器上的完整示例):

\n
    auto safeSqrt = [](auto const& x) {\n        return x > 0 ? std::optional(std::sqrt(x)) : std::nullopt;\n    };\n    {\n        auto opt = chain(std::optional(2), safeSqrt);\n        std::cout << opt.value_or(-1) << std::endl; // prints sqrt(2)\n    }\n    {\n        auto opt = chain(std::optional(-2), safeSqrt);\n        std::cout << opt.value_or(-1) << std::endl; // prints -1\n    }\n    {\n        auto opt = chain(std::nullopt, safeSqrt);\n        static_assert(std::is_same_v<decltype(opt), std::nullopt_t>); // passes\n    }\n
Run Code Online (Sandbox Code Playgroud)\n
\n

(\xe2\x80\xa0) 本来我更强调一点and_then不是,mbind因为前者是成员函数,后者是自由函数。

\n

我之所以强调这一点,是因为成员函数“属于”类(从某种意义上说,如果没有它所属的类,就不能拥有成员函数),所以在某种程度上,我们在这里讨论的and_then是与我们编写的用于创建std::vectormonad 的同名函数完全无关,因为它将存在于std::vector.

\n

另一方面,非成员函数transform和假设函数mbind都是自由函数,因此它们的存在不需要任何类,因此它们看起来更像是类型可以选择加入的一些通用抽象的接口(例如 Haskell\' s 类型类)。很明显,假设std::transformmbind是定制点,想要选择某种类型的客户端代码必须为该类型编写定制,也许这可以利用成员函数。

\n
\n

回答这个问题让我的脑海中浮现出另一个问题,所以我问了这个问题

\n

  • @NoSenseEtAl,好吧,正如我所写,`.and_then(std::identity)` _is_ 想要的`.flatten()`。我怀疑是否提供了比这更简单的东西。否则,它将被命名为“flatten”、“flat”或“join”(“&lt;ranges&gt;”和 Range-v3 确实有“join”,但它又是范围的)。然而,也许巴里知道更多的事情。 (2认同)
  • 我们是否需要在身份后面加上大括号才能使其工作 `auto WhatIsThis = optOfOpt.and_then(std::identity{});` (2认同)

Cal*_*eth 8

不完全的。

在 Haskell 语法中,bind 的形式为m a -> (a -> m b) -> m b,对应于满足这个概念(对于所有A, B, F

template <class Fn, class R, class... Args>
concept invocable_r = std::is_invocable_r_v<R, Fn, Args...>;

template <class Bind, template <class> M, class A, class B, invokable_r<M<B>, A> F>
concept bind = invocable_r<Bind, M<B>, M<A>, F>;
Run Code Online (Sandbox Code Playgroud)

那是and_then(与this第一个参数绑定)。transform是 fmap (this绑定到第二个参数),它是 Functor 操作(所有 Monad 都是 Functor)。

fmap的形式为(a -> b) -> f a -> f b.

template <class Fmap, template <class> M, class A, class B, invokable_r<B, A> F>
concept fmap = invocable_r<Fmap, M<B>, M<A>, F>;
Run Code Online (Sandbox Code Playgroud)

区别在于被绑定或映射的函数的返回类型。

这种区别的另一个例子是 .NET 的 linqSelectSelectMany

另一个挑剔是 monad 法则讨论表达式,而不是语句,因此您必须将构造函数包装在函数中。