Monad没有包装值?
大部分monad解释都使用monad包装值的例子。 例如, Maybe a
,其中a
类型变量是包装的。 但我想知道monads不会包装任何东西。
对于一个人为的例子,假设我有一个可以控制的现实世界的机器人,但没有传感器。 也许我想要这样控制它:
robotMovementScript :: RobotMonad ()
robotMovementScript = do
moveLeft 10
moveForward 25
rotate 180
main :: IO ()
main =
liftIO $ runRobot robotMovementScript connectToRobot
在我们的虚拟API中, connectToRobot
返回物理设备的某种句柄。 这个连接成为RobotMonad
的“上下文”。 因为我们与机器人的连接永远不会向我们发回值,所以monad的具体类型始终是RobotMonad ()
。
一些问题:
RobotMonad
- 从不包装价值? 或者这是否违背了monads的基本概念? <>
连接机器人控制动作。 虽然do
标记看起来更具可读性。 RobotMonad ()
? 我已经看过Data.Binary.Put
作为例子。 它似乎与我所想的相似(或者相同?)。 但它也涉及作家monad和Builder monoid。 考虑到那些额外的皱纹和我目前的技能水平,我认为Put
monad可能不是最具启发性的例子。
编辑
我实际上并不需要构建一个像这样的机器人或API。 这个例子完全是人为的。 我只是需要一个永远不会有理由将价值从monad中提取出来的例子。 所以我并没有要求解决机器人问题的最简单方法。 相反,这个关于没有内在价值的单子的思想实验是试图更好地理解单子。
TL;没有包装价值的DR Monad并不是很特别,你可以将所有相同的功率建模为一个列表。
有一种叫做Free
monad的东西。 这很有用,因为它在某种意义上是所有其他monad的优秀代表 - 如果您在某些情况下可以了解Free
monad的行为,那么您可以很好地了解Monad
的行为。
它看起来像这样
data Free f a = Pure a
| Free (f (Free f a))
每当f
是Functor
, Free f
就是Monad
instance Functor f => Monad (Free f) where
return = Pure
Pure a >>= f = f a
Free w >>= f = Free (fmap (>>= f) w)
那么当a
总是()
时会发生什么? 我们不需要a
参数了
data Freed f = Stop
| Freed (f (Freed f))
显然,这不能再是Monad
了,因为它有错误的类型(类型)。
Monad f ===> f :: * -> *
Freed f :: *
但是,我们仍然可以定义类似Monad
被摆脱的IC功能上它a
零件
returned :: Freed f
returned = Stop
bound :: Functor f -- compare with the Monad definition
=> Freed f -> Freed f -- with all `a`s replaced by ()
-> Freed f
bound Stop k = k Pure () >>= f = f ()
bound (Freed w) k = Free w >>= f =
Freed (fmap (`bound` k) w) Free (fmap (>>= f) w)
-- Also compare with (++)
(++) [] ys = ys
(++) (x:xs) ys = x : ((++) xs ys)
看起来是(而且是!)一个Monoid
。
instance Functor f => Monoid (Freed f) where
mempty = returned
mappend = bound
Monoid
可以通过列表来初始化。 我们使用列表Monoid
的通用属性,如果我们有函数Monoid m => (a -> m)
那么我们可以将列表[a]
变成m
。
convert :: Monoid m => (a -> m) -> [a] -> m
convert f = foldr mappend mempty . map f
convertFreed :: Functor f => [f ()] -> Freed f
convertFreed = convert go where
go :: Functor f => f () -> Freed f
go w = Freed (const Stop <$> w)
所以就机器人而言,我们只需使用一系列动作即可脱身
data Direction = Left | Right | Forward | Back
data ActionF a = Move Direction Double a
| Rotate Double a
deriving ( Functor )
-- and if we're using `ActionF ()` then we might as well do
data Action = Move Direction Double
| Rotate Double
robotMovementScript = [ Move Left 10
, Move Forward 25
, Rotate 180
]
现在,当我们将它转换为IO
我们明确地将这个方向列表转换为Monad
并且我们可以看到,将我们的初始Monoid
发送给Freed
,然后将Freed f
当作Free f ()
并将其解释为初始Monad
关于我们想要的IO
操作。
但很明显,如果你不使用“包装”值,那么你并没有真正使用Monad
结构。 你可能只是有一个列表。
我会试着对这些部分给出部分答案:
RobotMonad
- 从不包装价值? 或者这是否违背了monads的基本概念? <>
连接机器人控制动作。 虽然符号似乎更具可读性。 RobotMonad ()
? monad的核心操作是monadic绑定操作
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
这意味着一个行动取决于(或可能取决于)前一个行动的价值。 因此,如果你有一个固有的有时并不带有可被视为价值的东西的概念(即使是复杂的形式,如继续单子), monad也不是一个好的抽象概念 。
如果我们放弃>>=
我们基本上只剩下Applicative
。 它也允许我们编写行动,但它们的组合不能依赖于前面的值。
还有一个Applicative
实例不带值,如您所示:Data.Functor.Constant。 它的类型a
行为必须是一个monoid,以便它们可以组合在一起。 这似乎是最接近你想法的概念。 当然,我们可以直接使用Monoid
而不是Constant
。
也就是说,也许更简单的解决方案是拥有一个monad RobotMonad a
,它具有一个值( RobotMonad a
,它本质上与Writer
monad是同构的)。 并声明runRobot
需要RobotMonad ()
,所以它可以只执行没有值的脚本:
runRobot :: RobotMonad () -> RobotHandle -> IO ()
这将允许您使用do
记号,并与机器人脚本中值的工作。 即使机器人没有传感器,能够传递值也常常有用。 并且扩展这个概念可以让你创建一个monad变换器,比如RobotMonadT ma
(类似于WriterT
)
runRobotT :: (Monad m) => RobotMonadT m () -> RobotHandle -> IO (m ())
也许
runRobotT :: (MonadIO m) => RobotMonadT m () -> RobotHandle -> m ()
这将是一个强大的抽象,可以让你将机器人动作与任意monad结合起来。
好吧
data Useless a = Useless
instance Monad Useless where
return = const Useless
Useless >>= f = Useless
但正如我所表明的那样,这不是有用的。
你想要的是Writer
monad,它把monoid包装成monad,所以你可以使用符号。