如何使用Monad实例以及关于(->)的困惑

40

在不同的问题中,我发现评论中有关于使用(->)单子实例(instance of Monads)来实现无参式风格(point-free style)的提示。

对于我来说,这有点抽象。好吧,我看到(->)上有Arrow实例,似乎(->)可以用于实例表示法,但不能用于类型声明(那就另一个问题了)。

有人有使用(->)作为Monad实例的示例吗? 或者有一个好的链接?

如果这个问题已经在这里讨论过了,请原谅,但搜索""(->) Monad instance"会给你很多结果,正如你所想...因为几乎每个关于Haskell的问题都涉及到(->)或"Monad"。


2
你是什么意思说“箭头符号 (->) 可以用于实例表示法但不能用于类型声明”? 你可以这样做:type F a b = (->) a bf :: (->) a b - R. Martinho Fernandes
3
(->) 是一个类型构造器,属于 * -> * -> * 类别;而单子必须是 * -> * 类别的类型构造器(比如 Maybe、[]、IO 等等)。因此,试图用 (->) 实例化 Monad 会导致一种类型错误!但是你可以将 (-> a) 变成 Monad 的一个实例!——甚至可能 (a ->) 也可以成为一个 Monad? - phynfo
2
哦,谢谢你的提示!那么我可以把 (->) a b 理解为 a->b,对吗?所以,(->) 是类型中的一个运算符。 - makelc
是的:(->)是一种类型函数,通常称为“类型构造器”或仅称为“构造器”,它期望两种类型作为参数并产生一个函数类型作为结果。 - phynfo
@phynfo:实际上,(r ->) 是一个单子,而 (-> r) 不是,因为你会遇到实现 return :: a -> (a -> r) 的问题! - yatima2975
这个问题在回答中有一个非常好的例子。 - John F. Miller
2个回答

37
给定类型 r,类型为 r -> a 的函数可以被视为使用类型为 r 的环境来提供 a 的计算。如果给定两个函数 r -> aa -> (r -> b),那么很容易想象在给定环境(再次是类型为r)时可以将它们组合起来。

但等等,这正是单子的用途所在!

因此,我们可以为 (->) r 创建一个 Monad 实例,该实例通过传递 rfg 来实现 f >>= g。这就是 (->) r 的 Monad 实例所做的事情。

要实际访问环境,您可以使用 id :: r -> r,现在您可以将其视为在环境 r 中运行并提供 r 的计算。要创建本地子环境,可以使用以下内容:

inLocalEnvironment :: (r -> r) -> (r -> a) -> (r -> a)
inLocalEnvironment xform f = \env -> f (xform env)

这种通过将环境传递给计算机并允许它在本地进行查询和修改的模式不仅适用于 (->) r单子,这就是为什么将其抽象为MonadReader 类的原因,使用比我在此处使用的名称更加明智:

http://hackage.haskell.org/packages/archive/mtl/2.0.1.0/doc/html/Control-Monad-Reader-Class.html

基本上,它有两个实例:我们在此处看到的 (->) rReaderT r m,它只是一个newtype的包装器,包装了r -> m a,所以它与我在此处描述的(->) r 单子是相同的,只是它在其他转换后的单子中提供计算。


2
好的回答,希望能看到使用一些单子组合器的实例以及它是如何展开的。例如,liftM2 - luqui
1
我还想补充一点,Monad.Reader #17期刊中包含一篇关于Reader Monad的文章。在这里阅读:http://themonadreader.wordpress.com/2011/01/09/issue-17/ - Cactus

29
为了为 (->) r 定义一个单子,我们需要两个操作,return(>>=),并满足三个定律:
instance Monad ((->) r) where

如果我们看一下 (->) r 的返回签名

    return :: a -> r -> a

我们可以看到这只是一个常数函数,它忽略了它的第二个参数。
    return a r = a

或者,另外一种选择是,

    return = const

要构建(>>=),如果我们使用单子(->) r来特化其类型签名,则需要进行以下操作:

    (>>=) :: (r -> a) -> (a -> r -> b) -> r -> b

实际上只有一个可能的定义。

    (>>=) x y z = y (x z) z

使用这个monad就像给每个函数传递一个额外的参数r。您可以将其用于配置,或将选项传递到程序的深处。
我们可以通过验证三个monad定律来检查它是否是monad:
1. return a >>= f = f a 

return a >>= f 
= (\b -> a) >>= f -- by definition of return
= (\x y z -> y (x z) z) (\b -> a) f -- by definition of (>>=)
= (\y z -> y ((\b -> a) z) z) f -- beta reduction
= (\z -> f ((\b -> a) z) z) -- beta reduction
= (\z -> f a z) -- beta reduction
= f a -- eta reduction

2. m >>= return = m

m >>= return
= (\x y z -> y (x z) z) m return -- definition of (>>=)
= (\y z -> y (m z) z) return -- beta reduction
= (\z -> return (m z) z) -- beta reduction
= (\z -> const (m z) z) -- definition of return
= (\z -> m z) -- definition of const
= m -- eta reduction

最终的单子法则:

3. (m >>= f) >>= g  ≡  m >>= (\x -> f x >>= g)

接下来是类似的、简单的等式推理。

我们还可以为 ((->) r) 定义许多其他的类,比如 Functor 类,

instance Functor ((->) r) where

如果我们查看签名,会发现:
   -- fmap :: (a -> b) -> (r -> a) -> r -> b

我们可以看到,这只是组合!
   fmap = (.)

同样地,我们可以创建一个Applicative的实例。

instance Applicative ((->) r) where
   -- pure :: a -> r -> a
   pure = const

   -- (<*>) :: (r -> a -> b) -> (r -> a) -> r -> b
   (<*>) g f r = g r (f r)

拥有这些实例的好处在于,它们让您在操作函数时可以使用所有MonadApplicative组合器。
涉及(->)的类有很多实例,例如,如果给定a上的Monoid,则可以手动编写(b -> a)的Monoid实例,如下所示:
enter code here
instance Monoid a => Monoid (b -> a) where
    -- mempty :: Monoid a => b -> a
    mempty _ = mempty
    -- mappend :: Monoid a => (b -> a) -> (b -> a) -> b -> a
    mappend f g b = f b `mappend` g b

但是通过Monad/Applicative实例,您也可以定义此实例:

instance Monoid a => Monoid (r -> a) where
    mempty = pure mempty
    mappend = liftA2 mappend

使用 (->) r 的 Applicative 实例或者使用

instance Monoid a => Monoid (r -> a) where
    mempty = return mempty
    mappend = liftM2 mappend

使用 (->) r 的单子实例。

在这里节省的不多,但是,例如生成无点代码的 @pl 工具,由 #haskell IRC 频道上的 lambdabot 提供,相当滥用了这些实例。


为了更清晰地说明,举个例子:一个简单的函数,接受一个整数并返回参数+20,可以这样写:import Control.Applicative -- this has monad instance of (->) r\n add20 :: Int -> Int \n add20= do\n p <- id \n return $ p + 20。感谢您详细的回答。 - makelc
哦,这行代码 (>>=) x y z = y (x r) r 应该是 (>>=) x y r = y (x r) r,我的意思是,z 应该是一个 r,或者我错过了什么? - makelc
我可能搞砸了,因为我随口打了所有这些字,而且我在中途改变了一半的名称,以便更容易看到重写。 - Edward Kmett

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接