好吧,让我们考虑一下可以简化这种事情的方式。我猜一个非单子版本看起来像const' f a = const a (f a),它显然等同于具有更具体类型的flip const。然而,对于单子版本,f a 的结果可以对函子的非参数结构做任意事情(即,通常称为“副作用”),包括依赖于 a 值的事情.这告诉我们的是,尽管 假装 好像我们正在丢弃 f a 的结果,但我们实际上并没有做任何此类事情。将a 作为函子的参数部分原样返回并不重要,我们可以将return 替换为其他东西,并且仍然具有概念上相似的功能。
所以我们可以得出的第一件事是,它可以看作是函数的一个特例,如下所示:
doBoth :: (Monad m) => (a -> m b) -> (a -> m c) -> a -> m c
doBoth f g a = f a >> g a
从这里开始,有两种不同的方法来寻找某种底层结构。
一种观点是识别在多个函数之间拆分单个参数,然后重新组合结果的模式。这是函数的Applicative/Monad 实例所体现的概念,如下所示:
doBoth :: (Monad m) => (a -> m b) -> (a -> m c) -> a -> m c
doBoth f g = (>>) <$> f <*> g
...或者,如果您愿意:
doBoth :: (Monad m) => (a -> m b) -> (a -> m c) -> a -> m c
doBoth = liftA2 (>>)
当然,liftA2 等价于 liftM2,所以你可能想知道是否将 monad 上的操作提升到另一个 monad 是否与 monad 转换器有关;一般来说,那里的关系很尴尬,但在这种情况下它很容易工作,给出如下内容:
doBoth :: (Monad m) => ReaderT a m b -> ReaderT a m c -> ReaderT a m c
doBoth = (>>)
...模适当的包装等等,当然。为了专门回到您的原始版本,return 的原始用法现在需要是类型为 ReaderT a m a 的东西,这应该不难识别为阅读器单子的 ask 函数。
另一种观点是认识到具有(Monad m) => a -> m b 等类型的函数可以直接组合,就像纯函数。函数 (<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c) 直接等效于函数组合 (.) :: (b -> c) -> (a -> b) -> (a -> c),或者您可以改用 Control.Category 和 newtype 包装器 Kleisli 以通用方式处理相同的事物。
然而,我们仍然需要拆分参数,所以我们真正需要的是一个“分支”组合,而 Category 单独没有它;通过使用Control.Arrow,我们得到(&&&),让我们重写函数如下:
doBoth :: (Monad m) => Kleisli m a b -> Kleisli m a c -> Kleisli m a (b, c)
doBoth f g = f &&& g
由于我们不关心第一个 Kleisli 箭头的结果,只关心它的副作用,我们可以用明显的方式丢弃那一半元组:
doBoth :: (Monad m) => Kleisli m a b -> Kleisli m a c -> Kleisli m a c
doBoth f g = f &&& g >>> arr snd
这让我们回到了通用形式。专注于您的原创,return 现在变成了简单的id:
constKleisli :: (Monad m) => Kleisli m a b -> Kleisli m a a
constKleisli f = f &&& id >>> arr snd
由于常规函数也是Arrows,因此如果您概括类型签名,上述定义也适用。但是,扩展纯函数的定义并简化如下可能会有所启发:
-
\f x -> (f &&& id >>> arr snd) x
\f x -> (snd . (\y -> (f y, id y))) x
\f x -> (\y -> snd (f y, y)) x
\f x -> (\y -> y) x
-
\f x -> x。
所以我们回到flip const,正如预期的那样!
简而言之,您的函数是 (>>) 或 flip const 的一些变化,但在某种程度上依赖于差异——前者同时使用 ReaderT 环境和底层 monad 的 (>>) ,后者使用特定 Arrow 的隐式副作用以及 Arrow 副作用以特定顺序发生的期望。由于这些细节,不可能有任何概括或简化。从某种意义上说,您使用的定义完全符合它的需要,这就是为什么我给出的替代定义更长和/或涉及一些包装和展开。
这样的函数自然会添加到某种“monad 实用程序库”中。虽然Control.Monad 提供了一些符合这些方面的组合器,但它远非详尽无遗,而且我在标准库中既找不到也无法回忆起这个函数的任何变化。但是,如果在一个或多个关于 hackage 的实用程序库中找到它,我一点也不感到惊讶。
在很大程度上省略了存在的问题,除了你可以从上面关于相关概念的讨论中得到的东西之外,我真的不能提供太多关于命名的指导。
最后,请注意,您的函数没有基于一元表达式结果的控制流选择,因为无论主要目标是什么,都会执行表达式。拥有一个独立于参数内容的计算结构(即Monad m => m a 中的a 类型的东西)通常表明您实际上不需要完整的Monad,并且可以使用更一般的概念Applicative.