rlkw1024
rlkw1024

Reputation: 6515

Monad with no wrapped value?

Most of the monad explanations use examples where the monad wraps a value. E.g. Maybe a, where the a type variable is what's wrapped. But I'm wondering about monads that never wrap anything.

For a contrived example, suppose I have a real-world robot that can be controlled, but has no sensors. Maybe I'd like to control it like this:

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

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

In our imaginary API, connectToRobot returns some kind of handle to the physical device. This connection becomes the "context" of the RobotMonad. Because our connection to the robot can never send a value back to us, the monad's concrete type is always RobotMonad ().

Some questions:

  1. Does my contrived example seem right?
  2. Am I understanding the idea of a monad's "context" correctly? Am I correct to describe the robot's connection as the context?
  3. Does it make sense to have a monad--such as RobotMonad--that never wraps a value? Or is this contrary to the basic concept of monads?
  4. Are monoids a better fit for this kind of application? I can imagine concatenating robot control actions with <>. Though do notation seems more readable.
  5. In the monad's definition, would/could there be something that ensures the type is always RobotMonad ()?

I've looked at Data.Binary.Put as an example. It appears to be similar (or maybe identical?) to what I'm thinking of. But it also involves the Writer monad and the Builder monoid. Considering those added wrinkles and my current skill level, I think the Put monad might not be the most instructive example.

Edit

I don't actually need to build a robot or an API like this. The example is completely contrived. I just needed an example where there would never be a reason to pull a value out of the monad. So I'm not asking for the easiest way to solve the robot problem. Rather, this thought experiment about monads without inner values is an attempt to better understand monads generally.

Upvotes: 14

Views: 957

Answers (4)

Petr
Petr

Reputation: 63359

I'll try to give a partial answer for these parts:

  • Does it make sense to have a monad--such as RobotMonad--that never wraps a value? Or is this contrary to the basic concept of monads?
  • Are monoids a better fit for this kind of application? I can imagine concatenating robot control actions with <>. Though do notation seems more readable.
  • In the monad's definition, would/could there be something that ensures the type is always RobotMonad ()?

The core operation for monads is the monadic bind operation

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

This means that an action depends (or can depend) on the value of a previous action. So if you have a concept that inherently doesn't sometimes carry something that could be considered as a value (even in a complex form such as the continuation monad), monad isn't a good abstraction.

If we abandon >>= we're basically left with Applicative. It also allows us to compose actions, but their combinations can't depend on the values of preceding ones.

There is also an Applicative instance that carries no values, as you suggested: Data.Functor.Constant. Its actions of type a are required to be a monoid so that they can be composed together. This seems like the closest concept to your idea. And of course instead of Constant we could use a Monoid directly.


That said, perhaps simpler solution is to have a monad RobotMonad a that does carry a value (which would be essentially isomorphic to the Writer monad, as already mentioned). And declare runRobot to require RobotMonad (), so it'd be possible to execute only scripts with no value:

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

This would allow you to use the do notation and work with values inside the robot script. Even if the robot has no sensors, being able to pass values around can be often useful. And extending the concept would allow you to create a monad transformer such as RobotMonadT m a (resembling WriterT) with something like

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

or perhaps

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

which would be a powerful abstraction that'd allow you to combine robotic actions with an arbitrary monad.

Upvotes: 3

J. Abrahamson
J. Abrahamson

Reputation: 74354

TL;DR Monad without its wrapped value isn't very special and you get all the same power modeling it as a list.

There's a thing known as the Free monad. It's useful because it in some sense is a good representer for all other monads---if you can understand the behavior of the Free monad in some circumstance you have a good insight into how Monads generally will behave there.

It looks like this

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

and whenever f is a Functor, Free f is a Monad

instance Functor f => Monad (Free f) where
  return       = Pure
  Pure a >>= f = f a
  Free w >>= f = Free (fmap (>>= f) w)

So what happens when a is always ()? We don't need the a parameter anymore

data Freed f = Stop 
             | Freed (f (Freed f))

Clearly this cannot be a Monad anymore as it has the wrong kind (type of types).

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

But we can still define something like Monadic functionality onto it by getting rid of the a parts

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)

Which looks to be (and is!) a Monoid.

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

And Monoids can be initially modeled by lists. We use the universal property of the list Monoid where if we have a function Monoid m => (a -> m) then we can turn a list [a] into an 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)

So in the case of your robot, we can get away with just using a list of actions

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
                      ]

Now when we cast it to IO we're clearly converting this list of directions into a Monad and we can see that as taking our initial Monoid and sending it to Freed and then treating Freed f as Free f () and interpreting that as an initial Monad over the IO actions we want.

But it's clear that if you're not making use of the "wrapped" values then you're not really making use of Monad structure. You might as well just have a list.

Upvotes: 17

daniel gratzer
daniel gratzer

Reputation: 53871

Well it seems like you have a type that supports just

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

But you further specify that you only want to be able to use m ()s. In this case I'd vote for

foo = mconcat
      [ moveLeft 10
      , moveForward 25
      , rotate 180]

As the simple solution. The alternative is to do something like

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

Using the Writer monad.

The problem with not wrapping the value is that

return a >>= f === f a

So suppose we had some monad that ignored the value, but contained other interesting information,

newtype Robot a = Robot {unRobot :: [RobotAction]}

addAction :: RobotAction -> Robot a -> Robot b

f a = Robot [a]

Now if we ignore the value,

instance Monad Robot where
  return = const (Robot [])
  a >>= f = a -- never run the function

Then

return a >>= f  /= f a

so we don't have a monad. So if you want to the monad to have any interesting states, have == return false, then you need to store that value.

Upvotes: 1

Philip JF
Philip JF

Reputation: 28539

Well there is

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

but as I indicated, that isn't usefull.

What you want is the Writer monad, which wraps up a monoid as a monad so you can use do notation.

Upvotes: 3

Related Questions