大多数monad解释使用monad包含值的示例。例如。 Maybe a
,其中a
类型变量包含在内。但是我想知道从未包装任何东西的monad。
对于一个人为的例子,假设我有一个可以控制但没有传感器的真实机器人。也许我想像这样控制它:
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
为例。它似乎与我的想法相似(或者可能相同?)。但它也involves the Writer monad and the Builder monoid。考虑到那些增加的皱纹和我目前的技能水平,我认为Put
monad可能不是最有启发性的例子。
修改
我实际上并不需要构建这样的机器人或API。这个例子完全是人为的。我只需要一个例子,那里永远不会有理由从monad中取出一个值。所以我不是要求解决机器人问题的最简单方法。相反,这种关于没有内在价值的单子的思想实验是为了更好地理解monad。
答案 0 :(得分:16)
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
部分来定义类似a
ic功能的内容
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
1}}然后将Freed f
视为Free f ()
并将其解释为我们想要的Monad
行为的初始IO
。
但很明显,如果你没有使用“包裹”值,那么你并没有真正使用Monad
结构。你也可以只列一个清单。
答案 1 :(得分:3)
我会尝试对这些部分给出部分答案:
- 有一个monad - 例如
RobotMonad
- 从不包含值吗?或者这与monads的基本概念相反?- 幺半群是否更适合这种应用?我可以想象用
<>
连接机器人控制动作。虽然注释看起来更具可读性。- 在monad的定义中,是否/可能存在确保类型始终为
RobotMonad ()
的内容?
monad的核心操作是monadic绑定操作
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
这意味着某个操作取决于(或可能取决于)上一个操作的值。因此,如果你有一个概念本身有时不带有可以被视为一个值的东西(即使是一个复杂的形式,比如continuation monad), monad也不是一个好的抽象。 / p>
如果我们放弃>>=
,我们基本上只剩下Applicative
。它还允许我们组成动作,但它们的组合不能取决于前面的值。
还有一个Applicative
实例没有值,正如您所建议的那样:Data.Functor.Constant。类型a
的行为必须是一个幺半群,以便它们可以组合在一起。这似乎是与您的想法最接近的概念。当然,我们可以直接使用Constant
代替Monoid
。
也就是说,或许更简单的解决方案是让一个monad RobotMonad a
确实带有一个值(这与Writer
monad基本上是同构的,如前所述)。并声明runRobot
需要RobotMonad ()
,因此只能执行没有值的脚本:
runRobot :: RobotMonad () -> RobotHandle -> IO ()
这将允许您使用do
表示法并使用机器人脚本中的值。即使机器人没有传感器,能够传递值也常常很有用。扩展概念将允许您创建一个monad变换器,例如RobotMonadT m a
(类似于WriterT
),类似
runRobotT :: (Monad m) => RobotMonadT m () -> RobotHandle -> IO (m ())
或者
runRobotT :: (MonadIO m) => RobotMonadT m () -> RobotHandle -> m ()
这将是一个强大的抽象,允许您将机器人动作与任意monad结合起来。
答案 2 :(得分:1)
好吧有
data Useless a = Useless
instance Monad Useless where
return = const Useless
Useless >>= f = Useless
但正如我所说,这没用。
你想要的是Writer
monad,它将monoid包装为monad,这样你就可以使用do notation。
答案 3 :(得分:1)
好像你有一个只支持
的类型(>>) :: m a -> m b -> m b
但您进一步指定您只希望能够使用m ()
。在这种情况下,我会投票支持
foo = mconcat
[ moveLeft 10
, moveForward 25
, rotate 180]
作为简单的解决方案。另一种方法是做
之类的事情type Robot = Writer [RobotAction]
inj :: RobotAction -> Robot ()
inj = tell . (:[])
runRobot :: Robot a -> [RobotAction]
runRobot = snd . runWriter
foo = runRobot $ do
inj $ moveLeft 10
inj $ moveForward 25
inj $ rotate 180
使用Writer
monad。
没有包装值的问题是
return a >>= f === f a
因此,假设我们有一些忽略该值的monad,但包含其他有趣的信息,
newtype Robot a = Robot {unRobot :: [RobotAction]}
addAction :: RobotAction -> Robot a -> Robot b
f a = Robot [a]
现在,如果我们忽略该值,
instance Monad Robot where
return = const (Robot [])
a >>= f = a -- never run the function
然后
return a >>= f /= f a
所以我们没有monad。因此,如果你想让monad有任何有趣的状态,让==
返回false,那么你需要存储该值。