从 monad 中取出 monadic 函数

Taking monadic functions out of a monad

Haskell 具有函数 join,其中 "runs" 一个在 monad 中的 monadic 动作:

join :: Monad m => m (m a) -> m a
join m = m >>= \f -> f

我们可以为带有一个参数的单子函数编写一个类似的函数:

join1 :: Monad m => m (a -> m b) -> (a -> m b)
join1 m arg1 = m >>= \f -> f arg1

对于两个参数:

join2 :: Monad m => m (a -> b -> m c) -> (a -> b -> m c)
join2 m arg1 arg2 = m >>= \f -> f arg1 arg2

是否可以编写一个通用函数 joinN,它可以处理带有 N 个参数的单子函数?

如果你真的愿意,你可以做这样的事情,但要有相当多的丑陋。

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE UndecidableInstances #-}

import Control.Monad (join, liftM)

class Joinable m z | z -> m where
  joins :: m z -> z

instance Monad m => Joinable m (m a) where
  joins = join

instance (Monad m, Joinable m z) => Joinable m (r -> z) where
  joins m r = joins (liftM ($ r) m)

但是,如您所见,这依赖于一些不稳定的类型类魔法(尤其是令人羡慕的 UndecidableInstances)。编写所有实例 join1...join10 并直接导出它们可能更好(如果看起来很丑)。这也是 base 库中建立的模式。

值得注意的是,推理在这种情况下不会很好地工作。例如

λ> joins (return (\a b -> return (a + b))) 1 2
Overlapping instances for Joinable ((->) t0) (t0 -> t0 -> IO t0)
  arising from a use of ‘joins’
Matching instances:
  instance Monad m => Joinable m (m a)
    -- Defined at /Users/tel/tmp/ASD.hs:11:10
  instance (Monad m, Joinable m z) => Joinable m (r -> z)
    -- Defined at /Users/tel/tmp/ASD.hs:14:10

但是如果我们给我们的参数一个明确的类型

λ> let {q :: IO (Int -> Int -> IO Int); q = return (\a b -> return (a + b))}

那么它仍然可以像我们希望的那样工作

λ> joins q 1 2
3

出现这种情况是因为仅使用类型类就很难指示您是想在函数链的最终 return 类型中对 monad m 进行操作,还是对 monad (->) r 就是函数链 本身。

简短的回答是否定的。稍微长一点的答案是你可以定义一个中缀运算符。

查看 liftM 的实现:http://hackage.haskell.org/package/base-4.7.0.2/docs/src/Control-Monad.html#liftM

它定义了 liftM5。这是因为无法定义 liftMN,就像您无法定义 joinN 一样。

但是我们可以吸取 Appicative <$><*> 的教训,定义我们自己的中缀运算符:

> let infixr 1 <~> ; x <~> f = fmap ($ x) f
> :t (<~>)
(<~>) :: Functor f => a -> f (a -> b) -> f b
> let foo x y = Just (x + y)
> let foobar = Just foo
> join $ 1 <~> 2 <~> foobar
Just 3

这很像一个常见的应用模式:

f <$> a1 <*> a2 .. <*> an
join $ a1 <~> a2 .. <~> an <~> f

每个可能的单一功能N?并不真地。在 Haskell 中很难对具有不同数量参数的函数进行泛化,部分原因是 "number of arguments" 并不总是定义明确。以下是 id 类型的所有有效特化:

id :: a -> a
id :: (a -> a) -> a -> a
id :: (a -> a -> a) -> a -> a -> a
...

我们需要 一些 方法在类型级别获得 N,然后根据 N 是什么做不同的事情。

现有的 "variadic" 函数,如 printf,使用 typeclasses 执行此操作。他们通过对 -> 的归纳来确定 N 是什么:他们有一个 "base case" 非函数类型的实例,如 String 和一个函数的递归实例:

instance PrintfType String ...
instance (PrintfArg a, PrintfType r) => PrintfType (a -> r) ...

我们可以(经过深思熟虑 :P)在这里使用相同的方法,但有一个警告:我们的基本情况有点难看。我们想从正常的 join 开始,它会产生 m a 类型的结果;问题是要支持 any m a,我们必须与正常功能重叠。这意味着我们需要启用一些可怕的扩展,并且当我们实际使用我们的 joinN 时,我们可能会混淆类型推断系统。但是,有了正确的类型签名,我相信它应该可以正常工作。

首先,这是助手 class:

class Monad m => JoinN m ma where
  joinN :: m ma -> ma

ma 将采用相关函数类型,如 a -> m ba -> b -> m c 等。我不知道如何将 m 排除在 class 定义之外,所以我们需要立即启用 MultiParamTypeClasses.

接下来,我们的基本情况,这很正常 join:

instance Monad m => JoinN m (m a) where
  joinN = join

最后,我们有我们的递归案例。我们需要做的是"peel off"一个参数,然后根据一个更小的joinN实现函数。我们用 ap 来做到这一点,它是 <*> 专用于 monads 的:

instance (Monad m, JoinN m ma) => JoinN m (b -> ma) where
  joinN m arg = joinN (m `ap` return arg)

我们可以将实例中的=>理解为蕴涵:如果我们知道如何joinN一个ma,我们也知道如何做一个b -> ma

这个实例有点奇怪,所以它需要 FlexibleInstances 才能工作。更麻烦的是,因为我们的基本情况 (m (m a)) 完全由变量组成,它实际上与一堆其他合理的类型重叠。要真正完成这项工作,我们必须启用 OverlappingInstancesIncoherentInstances,它们相对棘手且容易出错。

经过一些粗略的测试,似乎 可以工作:

λ> let foo' = do getLine >>= \ x -> return $ \ a b c d -> putStrLn $ a ++ x ++ b ++ x ++ c ++ x ++ d
λ> let join4 m a b c d = m >>= \ f -> f a b c d
λ> join4 foo' "a" "b" "c" "d"

a  b  c  d
λ> joinN foo' "a" "b" "c" "d"

a  b  c  d