这样的功能是否已经存在?(或者,这个功能有什么更好的名字?)

Ada*_*ner 13 monads haskell idioms

我最近几次用以下模式编写代码,并且想知道是否有更短的编写方式.

foo :: IO String
foo = do
    x <- getLine
    putStrLn x >> return x
Run Code Online (Sandbox Code Playgroud)

为了让事情变得更清洁,我写了这个函数(虽然我不确定它是否合适):

constM :: (Monad m) => (a -> m b) -> a -> m a
constM f a = f a >> return a
Run Code Online (Sandbox Code Playgroud)

然后,我可以像这样做foo:

foo = getLine >>= constM putStrLn
Run Code Online (Sandbox Code Playgroud)

这样的函数/习语是否已经存在?如果没有,我的constM有什么更好的名字?

C. *_*ann 18

好吧,让我们考虑这样的事情的方式可能会被简化.我猜一个非monadic版本看起来像const' f a = const a (f a),这显然等同于flip const更具体的类型.然而,对于monadic版本,结果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
Run Code Online (Sandbox Code Playgroud)

从这里开始,有两种不同的方法可以查找某种基础结构.


一种观点是识别在多个函数之间拆分单个参数的模式,然后重新组合结果.这是函数的Applicative/ Monadinstances 体现的概念,如下所示:

doBoth :: (Monad m) => (a -> m b) -> (a -> m c) -> a -> m c
doBoth f g = (>>) <$> f <*> g
Run Code Online (Sandbox Code Playgroud)

...或者,如果您愿意:

doBoth :: (Monad m) => (a -> m b) -> (a -> m c) -> a -> m c
doBoth = liftA2 (>>)
Run Code Online (Sandbox Code Playgroud)

当然,liftA2相当于liftM2你可能想知道是否将monad上的操作提升到另一个monad与monad变换器有关; 一般来说,这种关系很尴尬,但在这种情况下,它很容易运作,给出这样的东西:

doBoth :: (Monad m) => ReaderT a m b -> ReaderT a m c -> ReaderT a m c
doBoth = (>>)
Run Code Online (Sandbox Code Playgroud)

...模数适当的包装等当然.要专门回到你的原始版本,return现在的原始用途需要是类型的东西ReaderT a m a,这不应该太难以识别ask为阅读器monad 的功能.


另一个观点是认识到类似类型的函数(Monad m) => a -> m b 可以直接组合,就像纯函数一样.该函数(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)直接等效于函数组合(.) :: (b -> c) -> (a -> b) -> (a -> c),或者您可以使用Control.Categorynewtype包装器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
Run Code Online (Sandbox Code Playgroud)

由于我们不关心第一个Kleisli箭头的结果,只有它的副作用,我们可以以明显的方式丢弃那一半的元组:

doBoth :: (Monad m) => Kleisli m a b -> Kleisli m a c -> Kleisli m a c
doBoth f g = f &&& g >>> arr snd
Run Code Online (Sandbox Code Playgroud)

这让我们回到了通用形式.专注于您的原创,return现在变得简单id:

constKleisli :: (Monad m) => Kleisli m a b -> Kleisli m a a
constKleisli f = f &&& id >>> arr snd
Run Code Online (Sandbox Code Playgroud)

由于常规函数也是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环境和(>>)潜在的单子,后者使用特定的隐含副作用Arrow和期望该Arrow副作用发生在一个特定的顺序.由于这些细节,不太可能有任何概括或简化.从某种意义上说,你使用的定义就像它需要的那样简单,这就是我给出的替代定义更长和/或涉及一些包装和展开的原因.

像这样的函数将是某种"monad实用程序库"的自然添加.虽然Control.Monad沿着这些线提供了一些组合器,但它远非详尽无遗,而且我既不能在标准库中找到也不能回想起这个函数的任何变化.但是,在hackage的一个或多个实用程序库中找到它并不会感到惊讶.

大多数人没有存在的问题,除了上面关于相关概念的讨论之外,我无法提供更多关于命名的指导.

最后,请注意,根据monadic表达式的结果,您的函数没有控制流选择,因为执行表达式无论主要目标是什么.有一个计算结构的独立参数的内容(即类型的东西aMonad m => m a)通常是你实际上并不需要一个完整的迹象Monad,并可能与更普遍的观点得到通过Applicative.