| Portability | MPTCs, fundeps |
|---|---|
| Stability | provisional |
| Maintainer | Edward Kmett <ekmett@gmail.com> |
| Safe Haskell | None |
Control.Monad.Free.TH
Contents
Description
Automatic generation of free monadic actions.
Free monadic actions
makeFree :: Name -> Q [Dec]Source
$(makeFree ''Type) provides free monadic actions for the
constructors of the given type.
To generate free monadic actions from a Type, it must be a data
declaration with at least one free variable. For each constructor of the type, a
new function will be declared.
Consider the following generalized definitions:
data Type a1 a2 … aN param = …
| FooBar t1 t2 t3 … tJ
| (:+) t1 t2 t3 … tJ
| t1 :* t2
| t1 `Bar` t2
| Baz { x :: t1, y :: t2, …, z :: tJ }
| …
where each of the constructor arguments t1, …, tJ is either:
- A type, perhaps depending on some of the
a1, …, aN. - A type dependent on
param, of the forms1 -> … -> sM -> param, M ≥ 0. At most 2 of thet1, …, tJmay be of this form. And, out of these two, at most 1 of them may haveM == 0; that is, be of the formparam.
For each constructor, a function will be generated. First, the name of the function is derived from the name of the constructor:
- For prefix constructors, the name of the constructor with the first
letter in lowercase (e.g.
FooBarturns intofooBar). - For infix constructors, the name of the constructor with the first
character (a colon
:), removed (e.g.:+turns into+).
Then, the type of the function is derived from the arguments to the constructor:
… fooBar :: (MonadFree Type m) => t1' -> … -> tK' -> m ret (+) :: (MonadFree Type m) => t1' -> … -> tK' -> m ret baz :: (MonadFree Type m) => t1' -> … -> tK' -> m ret …
The t1', …, tK' are those t1 … tJ that only depend on the
a1, …, aN.
The type ret depends on those constructor arguments that reference the
param type variable:
- If no arguments to the constructor depend on
param,ret ≡ a, whereais a fresh type variable. - If only one argument in the constructor depends on
param, thenret ≡ (s1, …, sM). In particular, fM == 0, thenret ≡ (); ifM == 1,ret ≡ s1. - If two arguments depend on
param, (e.g.u1 -> … -> uL -> paramandv1 -> … -> vM -> param, thenret ≡ Either (u1, …, uL) (v1, …, vM).
Note that Either a () and Either () a are both isomorphic to Maybe a.
Because of this, when L == 0 or M == 0 in case 3., the type of
ret is simplified:
-
ret ≡ Either (u1, …, uL) ()is rewritten toret ≡ Maybe (u1, …, uL). -
ret ≡ Either () (v1, …, vM)is rewritten toret ≡ Maybe (v1, …, vM).
Example
This is literate Haskell! To run this example, open the source of this
module and copy the whole comment block into a file with '.lhs'
extension. For example, Teletype.lhs.
{-# LANGUAGE DeriveFunctor, TemplateHaskell, FlexibleContexts #-}--
import Control.Monad (mfilter) import Control.Monad.Loops (unfoldM) import Control.Monad.Free (liftF, Free, iterM, MonadFree) import Control.Monad.Free.TH (makeFree) import Control.Applicative ((<$>)) import System.IO (isEOF) import Control.Exception (catch) import System.IO.Error (ioeGetErrorString) import System.Exit (exitSuccess)
First, we define a data type with the primitive actions of a teleprinter. The
param will stand for the next action to execute.
type Error = String
data Teletype param = Halt -- Abort (ignore all following instructions)
| NL param -- Newline
| Read (Char -> param) -- Get a character from the terminal
| ReadOrEOF { onEOF :: param,
onChar :: Char -> param } -- GetChar if not end of file
| ReadOrError (Error -> param)
(Char -> param) -- GetChar with error code
| param :\^^ String -- Write a message to the terminal
| (:%) param String [String] -- String interpolation
deriving (Functor)
By including a makeFree declaration:
makeFree ''Teletype
the following functions have been made available:
halt :: (MonadFree Teletype m) => m a nL :: (MonadFree Teletype m) => m () read :: (MonadFree Teletype m) => m Char readOrEOF :: (MonadFree Teletype m) => m (Maybe Char) readOrError :: (MonadFree Teletype m) => m (Either Error Char) (\^^) :: (MonadFree Teletype m) => String -> m () (%) :: (MonadFree Teletype m) => String -> [String] -> m ()
To make use of them, we need an instance of 'MonadFree Teletype'. Since Teletype is a
Functor, we can use the one provided in the Free package.
type TeletypeM = Free Teletype
Programs can be run in different ways. For example, we can use the
system terminal through the IO monad.
runTeletypeIO :: TeletypeM a -> IO a
runTeletypeIO = iterM run where
run :: Teletype (IO a) -> IO a
run Halt = do
putStrLn "This conversation can serve no purpose anymore. Goodbye."
exitSuccess
run (Read f) = getChar >>= f
run (ReadOrEOF eof f) = isEOF >>= \b -> if b then eof
else getChar >>= f
run (ReadOrError ferror f) = catch (getChar >>= f) (ferror . ioeGetErrorString)
run (NL rest) = putChar '\n' >> rest
run (rest :\^^ str) = putStr str >> rest
run ((:%) rest format tokens) = ttFormat format tokens >> rest
ttFormat :: String -> [String] -> IO ()
ttFormat [] _ = return ()
ttFormat ('\\':'%':cs) tokens = putChar '%' >> ttFormat cs tokens
ttFormat ('%':cs) (t:tokens) = putStr t >> ttFormat cs tokens
ttFormat (c:cs) tokens = putChar c >> ttFormat cs tokens
Now, we can write some helper functions:
readLine :: TeletypeM String readLine = unfoldM $ mfilter (/= '\n') <$> readOrEOF
And use them to interact with the user:
hello :: TeletypeM ()
hello = do
(\^^) "Hello! What's your name?"; nL
name <- readLine
"Nice to meet you, %." % [name]; nL
halt
We can transform any TeletypeM into an IO action, and run it:
main :: IO () main = runTeletypeIO hello
Hello! What's your name? $ Dave Nice to meet you, Dave. This conversation can serve no purpose anymore. Goodbye.
When specifying DSLs in this way, we only need to define the semantics for each of the actions; the plumbing of values is taken care of by the generated monad instance.