Monad没有包装价值?

时间:2013-11-09 23:39:58

标签: haskell functional-programming monads

大多数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 ()

有些问题:

  1. 我的人为的例子是否正确?
  2. 我是否正确理解了monad的“背景”?我是否正确将机器人的连接描述为上下文?
  3. 有一个monad是有意义的 - 例如RobotMonad - 永远不会包装一个值吗?或者这与monads的基本概念相反?
  4. 幺半群是否更适合这种应用?我可以想象用<>连接机器人控制动作。虽然do符号似乎更具可读性。
  5. 在monad的定义中,是否/可能存在确保类型始终为RobotMonad ()的内容?
  6. 我以Data.Binary.Put为例。它似乎与我的想法相似(或者可能相同?)。但它也involves the Writer monad and the Builder monoid。考虑到那些增加的皱纹和我目前的技能水平,我认为Put monad可能不是最有启发性的例子。

    修改

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

4 个答案:

答案 0 :(得分:16)

TL; DR 没有包装值的Monad不是很特别,你可以获得与列表相同的功能。

有一种叫做Free monad的东西。它很有用,因为它在某种意义上是所有其他monad的良好代表 - 如果你能理解Free monad在某些情况下的行为,你就能很好地了解Monad一般会如何在那里表现。

看起来像这样

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

fFunctor时,Free fMonad

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,那么你需要存储该值。