13

大多数 monad 解释都使用 monad 包装值的示例。例如Maybe aa类型变量是包装的内容。但我想知道从不包装任何东西的单子。

举一个人为的例子,假设我有一个可以控制但没有传感器的真实世界机器人。也许我想像这样控制它:

robotMovementScript :: RobotMonad ()
robotMovementScript = do
  moveLeft 10
  moveForward 25
  rotate 180

main :: IO ()
main = 
  liftIO $ runRobot robotMovementScript connectToRobot

在我们想象的 API 中,connectToRobot返回某种物理设备的句柄。这种联系成为RobotMonad. 因为我们与机器人的连接永远不会向我们发送值,所以 monad 的具体类型总是RobotMonad ().

一些问题:

  1. 我做的例子看起来对吗?
  2. 我是否正确理解了 monad 的“上下文”的概念?我将机器人的连接描述为上下文是否正确?
  3. 拥有一个从不包装值的RobotMonadmonad是否有意义?还是这与单子的基本概念相反?
  4. 幺半群更适合这种应用吗?我可以想象将机器人控制动作与<>. 虽然do符号似乎更具可读性。
  5. 在 monad 的定义中,是否会/可能有一些东西可以确保类型始终为RobotMonad ()

我看过Data.Binary.Put一个例子。它似乎与我的想法相似(或者可能相同?)。但它也涉及 Writer monad 和 Builder monoid。考虑到那些增加的皱纹和我目前的技能水平,我认为Put单子可能不是最有启发性的例子。

编辑

我实际上并不需要像这样构建机器人或 API。这个例子完全是人为的。我只需要一个例子,永远不会有理由从 monad 中提取一个值。所以我不是在寻求解决机器人问题的最简单方法。相反,这个关于没有内在价值的单子的思想实验是试图更好地理解单子。

4

4 回答 4

16

TL; DR Monad 没有它的包装值并不是很特别,你可以将它建模为一个列表。

有一种东西叫做Free单子。它很有用,因为它在某种意义上是所有其他 monad 的良好代表——如果你能理解Freemonad 在某些情况下的行为,你就会很好地了解Monads 通常在那里的行为。

看起来像这样

data Free f a = Pure a
              | Free (f (Free f a))

并且无论何时f是 a 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))

显然,这不再是 aMonad了,因为它的种类(类型类型)错误。

Monad f ===> f       :: * -> *
             Freed f :: *

但是我们仍然可以通过去掉部分来定义类似Monadic 功能的东西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)

看起来是(并且是!) a Monoid

instance Functor f => Monoid (Freed f) where
  mempty  = returned
  mappend = bound

并且Monoids 最初可以通过列表建模。我们使用列表的通用属性 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我们清楚地将这个方向列表转换为 a Monad,我们可以看到,将我们的首字母Monoid发送到Freed,然后将Freed f其视为Free f ()并解释为我们想要Monad的动作的首字母。IO

但很明显,如果您不使用“包装”值,那么您并没有真正使用Monad结构。你也可以有一个清单。

于 2013-11-10T04:00:11.910 回答
3

我将尝试对这些部分给出部分答案:

  • RobotMonad拥有一个从不包装值的 monad 是否有意义?还是这与单子的基本概念相反?
  • 幺半群更适合这种应用吗?我可以想象将机器人控制动作与<>. 虽然 do 表示法似乎更具可读性。
  • 在 monad 的定义中,是否会/可能有一些东西可以确保类型始终为RobotMonad ()

monads的核心操作是monadic bind操作

(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

这意味着一个动作取决于(或可以取决于)前一个动作的值。因此,如果您有一个固有的概念有时不包含可以被视为值的东西(即使是复杂的形式,例如延续单子),单子不是一个好的抽象

如果我们放弃>>=,我们基本上就剩下Applicative. 它还允许我们组合动作,但它们的组合不能依赖于前面的值。

正如您所建议的,还有一个Applicative不带值的实例:Data.Functor.Constant。它的类型动作a必须是一个幺半群,以便它们可以组合在一起。这似乎是最接近您的想法的概念。当然,Constant我们可以直接使用 a来代替Monoid


也就是说,也许更简单的解决方案是拥有一个带有值的monad(如前所述,RobotMonad a它本质上与 monad 同构)。Writer并声明runRobot为 require RobotMonad (),因此可以只执行没有价值的脚本:

runRobot :: RobotMonad () -> RobotHandle -> IO ()

这将允许您使用do表示法并使用机器人脚本中的值。即使机器人没有传感器,能够传递值通常也很有用。并且扩展这个概念将允许您创建一个单子转换器,例如RobotMonadT m a(类似WriterT)具有类似的东西

runRobotT :: (Monad m) => RobotMonadT m () -> RobotHandle -> IO (m ())

也许

runRobotT :: (MonadIO m) => RobotMonadT m () -> RobotHandle -> m ()

这将是一个强大的抽象,允许您将机器人动作与任意 monad 结合起来。

于 2013-11-10T08:42:29.003 回答
1

好吧,看来您的类型仅支持

(>>) :: m a -> m b -> m b

但是您进一步指定您只希望能够使用m ()s. 在这种情况下,我会投票

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单子。

不包装值的问题是

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 有任何有趣的状态,==返回 false,那么你需要存储那个值。

于 2013-11-10T00:13:47.220 回答
1

那么有

data Useless a = Useless
instance Monad Useless where
  return = const Useless
  Useless >>= f = Useless

但正如我所指出的,这没有用。

你想要的是Writermonad,它将一个 monoid 包装为一个 monad,这样你就可以使用 do 表示法。

于 2013-11-09T23:44:43.707 回答