wizards 0.1 → 1.0
raw patch · 6 files changed
+372/−181 lines, 6 filesdep +containersdep +control-monad-freedep −MonadPrompt
Dependencies added: containers, control-monad-free
Dependencies removed: MonadPrompt
Files
- System/Console/Wizard.hs +59/−56
- System/Console/Wizard/BasicIO.hs +29/−26
- System/Console/Wizard/Haskeline.hs +45/−29
- System/Console/Wizard/Internal.hs +149/−62
- System/Console/Wizard/Pure.hs +80/−0
- wizards.cabal +10/−8
System/Console/Wizard.hs view
@@ -1,17 +1,29 @@-{-# LANGUAGE GeneralizedNewtypeDeriving, Trustworthy, MultiParamTypeClasses, FlexibleInstances #-}+{-# LANGUAGE FlexibleContexts, TypeOperators, Trustworthy #-}+-- Necessary for MonadIO instance.+{-# LANGUAGE UndecidableInstances #-} module System.Console.Wizard ( -- * Wizards -- $intro Wizard (..) , PromptString (..)+ , run+ , (:<:)+ , (:+:) -- * Primitives -- $primitives+ , Line , line+ , LinePrewritten , linePrewritten+ , Password , password- , character + , Character+ , character+ , Output , output+ , OutputLn , outputLn+ , ArbitraryIO -- * Modifiers -- $modifiers , retry@@ -30,104 +42,95 @@ ) where import System.Console.Wizard.Internal+ import Control.Applicative import Control.Monad.Trans.Maybe+import Control.Monad.Trans+import Control.Monad.Free import Control.Monad.Reader-import Control.Monad.Prompt import Data.Maybe---- | A @Wizard a@ is a conversation with the user that will result in a data type @a@, or may fail.--- A 'Wizard' is made up of one or more \"primitives\" (see below), composed using the 'Applicative',--- 'Monad' and 'Alternative' instances. The 'Alternative' instance is, as you might expect, a maybe-style cascade. --- If the first wizard fails, the next one is tried.--- --- The 'Wizard' constructor is exported here for use when developing backends, but it is better for end-users to --- simply pretend that 'Wizard' is an opaque data type. Don't depend on this unless you have no other choice.--- --- 'Wizard's are, internally, just a maybe transformer over a prompt monad for each primitive action.-newtype Wizard backend a = Wizard (MaybeT (RecPrompt (WizardAction backend)) a)- deriving (Monad, Functor, Applicative, Alternative, MonadPlus)-instance MonadPrompt (WizardAction s (RecPrompt (WizardAction s))) (Wizard s) where - prompt = Wizard . lift . prompt--+import Data.Monoid -- $primitives -- /Primitives/ are the basic building blocks for @wizards@. Use these functions to produce wizards that -- ask for input from the user, or output information. --- | Read one line of input from the user.-line :: PromptString -> Wizard b String-line str = prompt $ Line str+-- | Output a string. Does not fail.+output :: (Output :<: b) => String -> Wizard b ()+output s = Wizard $ lift $ inject (Output s (Pure ())) +-- | Output a string followed by a newline. Does not fail.+outputLn :: (OutputLn :<: b) => String -> Wizard b ()+outputLn s = Wizard $ lift $ inject (OutputLn s (Pure ()))++-- | Read one line of input from the user. Cannot fail (but may throw exceptions, depending on the backend).+line :: (Line :<: b) => PromptString -> Wizard b String+line s = Wizard $ lift $ inject (Line s Pure) ++-- | Read a single character only from input. Cannot fail (but may throw exceptions, depending on the backend).+character :: (Character :<: b) + => PromptString+ -> Wizard b Char+character p = Wizard $ lift $ inject (Character p Pure)+++instance (ArbitraryIO :<: b) => MonadIO (Wizard b) where+ liftIO v = Wizard $ lift $ inject (ArbitraryIO v Pure) -- | Read one line of input, with some default text already present, before and/or after the editing cursor.--- Backends are not required to display this default text, or position the cursor anywhere, it is merely--- a suggestion.-linePrewritten :: PromptString+--- Cannot fail (but may throw exceptions, depending on the backend).+linePrewritten :: (LinePrewritten :<: b) + => PromptString -> String -- ^ Text to the left of the cursor -> String -- ^ Text to the right of the cursor -> Wizard b String-linePrewritten p s1 s2 = prompt $ LinePreset p s1 s2+linePrewritten p s1 s2 = Wizard $ lift $ inject (LinePrewritten p s1 s2 Pure) -- | Read one line of password input, with an optional mask character.--- The exact masking behavior of the password may vary from backend to backend. The masking character--- does not have to be honoured.-password :: PromptString+--- Cannot fail (but may throw exceptions, depending on the backend).+password :: (Password :<: b)+ => PromptString -> Maybe Char -- ^ Mask character, if any. -> Wizard b String-password str m = prompt $ Password str m - --- | Read a single character only from input.-character :: PromptString -> Wizard b Char-character = prompt . Character ---- | Output a string, if the backend used supports output.-output :: String -> Wizard b ()-output = prompt . Output---- | Output a string followed by a newline, if the backend used supports such output.-outputLn :: String -> Wizard b ()-outputLn = prompt . OutputLn+password p mc = Wizard $ lift $ inject (Password p mc Pure) -- $modifiers -- /Modifiers/ change the behaviour of existing wizards. -- | Retry produces a wizard that will retry the entire conversation again if it fails.--- Conceptually, it could thought of as @retry x = x \<|\> retry x@, however it also prints--- a user-friendly error message in the event of failure.-retry :: Wizard b a -> Wizard b a-retry = retryMsg "Invalid input. Please try again."+-- It is simply @retry x = x \<|\> retry x@.+retry :: Functor b => Wizard b a -> Wizard b a+retry x = x <|> retry x --- | Same as 'retry', except the error message can be specified.-retryMsg :: String -> Wizard b a -> Wizard b a+-- | Same as 'retry', except an error message can be specified.+retryMsg :: (OutputLn :<: b) => String -> Wizard b a -> Wizard b a retryMsg msg x = x <|> (outputLn msg >> retryMsg msg x) -- | @x \`defaultTo\` y@ will return @y@ if @x@ fails, e.g @parseRead line \`defaultTo\` 0@.-defaultTo :: Wizard b a -> a -> Wizard b a+defaultTo :: Functor b => Wizard b a -> a -> Wizard b a defaultTo wz d = wz <|> pure d -- | Like 'fmap', except the function may be partial ('Nothing' causes the wizard to fail).-parser :: (a -> Maybe c) -> Wizard b a -> Wizard b c+parser :: Functor b => (a -> Maybe c) -> Wizard b a -> Wizard b c parser f a = a >>= liftMaybe . f --- | @validator p w@ causes a wizard to fail if the output value does not satisfy the predicate @p@.-validator :: (a -> Bool) -> Wizard b a -> Wizard b a+-- | @validator p@ causes a wizard to fail if the output value does not satisfy the predicate @p@.+validator :: Functor b => (a -> Bool) -> Wizard b a -> Wizard b a validator = parser . ensure -- | Simply @validator (not . null)@, makes a wizard fail if it gets an empty string.-nonEmpty :: Wizard b [a] -> Wizard b [a]+nonEmpty :: Functor b => Wizard b [a] -> Wizard b [a] nonEmpty = validator (not . null) -- | Makes a wizard fail if it gets an ordered quantity outside of the given range.-inRange :: (Ord a) => (a,a) -> Wizard b a -> Wizard b a+inRange :: (Ord a, Functor b) => (a,a) -> Wizard b a -> Wizard b a inRange (b,t) = validator (\x -> b <= x && x <= t) -- | Simply @parser readP@. Attaches a simple @read@ parser to a 'Wizard'.-parseRead :: (Read a) => Wizard b String -> Wizard b a+parseRead :: (Read a, Functor b) => Wizard b String -> Wizard b a parseRead = parser (readP) -- | Translate a maybe value into wizard success/failure. -liftMaybe :: Maybe a -> Wizard b a+liftMaybe :: Functor b => Maybe a -> Wizard b a liftMaybe (Just v) = pure v liftMaybe (Nothing) = mzero
System/Console/Wizard/BasicIO.hs view
@@ -1,34 +1,37 @@-{-# LANGUAGE DeriveDataTypeable, FlexibleInstances, GADTs, KindSignatures #-}-module System.Console.Wizard.BasicIO+{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, TypeOperators, GeneralizedNewtypeDeriving, Trustworthy #-}+module System.Console.Wizard.BasicIO ( BasicIO- , runBasicIO+ , basicIO ) where import System.Console.Wizard-import System.Console.Wizard.Internal +import System.Console.Wizard.Internal import Control.Monad.Trans-import Control.Monad.Prompt import Control.Monad.Trans.Maybe-import Control.Applicative((<$>)) -import Data.Typeable---- | A very simple standard IO backend for @wizards@, supporting input and output.--- Default text and password masks are ignored.--- A more full-featured back-end is based on Haskeline.--- Arbitrary IO actions can be performed in wizards via a 'MonadIO' instance.-data BasicIO (m :: * -> *) r = ArbitraryIO (IO r)+instance Run IO Output where runAlgebra (Output s w) = putStr s >> w+instance Run IO OutputLn where runAlgebra (OutputLn s w) = putStrLn s >> w+instance Run IO Line where runAlgebra (Line s w) = getLine >>= w+instance Run IO Character where runAlgebra (Character s w) = getChar >>= w+instance Run IO ArbitraryIO where runAlgebra (ArbitraryIO iov f) = iov >>= f --- | Runs a Wizard action in the BasicIO backend.-runBasicIO :: Wizard BasicIO a -> IO (Maybe a)-runBasicIO (Wizard (MaybeT c)) = runRecPromptM f c- where f :: WizardAction BasicIO (RecPrompt (WizardAction BasicIO) ) c -> IO c- f (Line s) = getLine- f (Character s) = getChar- f (Password s m) = getLine- f (LinePreset s f b) = getLine- f (Output s) = putStr s- f (OutputLn s) = putStrLn s- f (Backend (ArbitraryIO a)) = a+-- | The 'BasicIO' backend supports only simple input and output.+-- Support for 'Password' and 'LinePrewritten' features can be added with +-- a shim from 'System.Console.Wizard.Shim'. +newtype BasicIO a = BasicIO (( Output + :+: OutputLn + :+: Line + :+: Character + :+: ArbitraryIO) a)+ deriving ( (:<:) Output+ , (:<:) OutputLn+ , (:<:) Line+ , (:<:) Character+ , (:<:) ArbitraryIO+ , Functor+ , Run IO+ ) -instance MonadIO (Wizard BasicIO) where- liftIO = prompt . Backend . ArbitraryIO+-- | A simple identity function, used to restrict types if the type inferred by GHC is too general.+-- You could achieve the same effect with a type signature, but this is slightly less typing.+basicIO :: Wizard BasicIO a -> Wizard BasicIO a+basicIO = id
System/Console/Wizard/Haskeline.hs view
@@ -1,50 +1,66 @@-{-# LANGUAGE DeriveDataTypeable, FlexibleInstances, GADTs #-}+{-# LANGUAGE DeriveDataTypeable, DeriveFunctor, FlexibleInstances, FlexibleContexts, MultiParamTypeClasses, TypeOperators, GeneralizedNewtypeDeriving, Trustworthy #-} module System.Console.Wizard.Haskeline - ( Haskeline- , UnexpectedEOF (..)- , runHaskeline+ ( UnexpectedEOF (..)+ , Haskeline+ , haskeline , withSettings+ , WithSettings(..) ) where import System.Console.Wizard import System.Console.Wizard.Internal import System.Console.Haskeline import Control.Monad.Trans-import Control.Monad.Prompt import Control.Monad.Trans.Maybe import Control.Exception import Data.Typeable --- | A Haskeline backend for @wizards@, supporting input, output, default text, and password input.--- In addition, Haskeline settings can be modified for a single wizard, and arbitrary IO can be--- performed using the 'MonadIO' instance.-data Haskeline m r = SetSettings (Settings IO) (m r)- | ArbitraryIO (IO r) -- | The Haskeline back-end will throw this exception if EOF is encountered -- when it is not expected. Specifically, when actions such as 'getInputLine' return 'Nothing'. data UnexpectedEOF = UnexpectedEOF deriving (Show, Typeable) instance Exception UnexpectedEOF --- | Runs a Wizard action in the Haskeline backend.-runHaskeline :: Wizard Haskeline a -> InputT IO (Maybe a)-runHaskeline (Wizard c) = runRecPromptM f $ runMaybeT c- where f :: WizardAction Haskeline (RecPrompt (WizardAction Haskeline) ) a -> InputT IO a- f (Line s) = getInputLine s >>= maybeToException UnexpectedEOF- f (Character s) = getInputChar s >>= maybeToException UnexpectedEOF- f (Password s m) = getPassword m s >>= maybeToException UnexpectedEOF- f (LinePreset s f b) = getInputLineWithInitial s (f,b) >>= maybeToException UnexpectedEOF - f (Output s) = outputStr s- f (OutputLn s) = outputStrLn s- f (Backend (SetSettings s v)) = liftIO $ runInputT s (runRecPromptM f v)- f (Backend (ArbitraryIO a)) = liftIO $ a+-- | Haskeline supports all the following features completely. +newtype Haskeline a = Haskeline (( Output + :+: OutputLn + :+: Line + :+: Character + :+: LinePrewritten + :+: Password + :+: ArbitraryIO + :+: WithSettings) a)+ deriving ( (:<:) Output+ , (:<:) OutputLn+ , (:<:) Line+ , (:<:) Character+ , (:<:) LinePrewritten+ , (:<:) Password+ , (:<:) ArbitraryIO+ , (:<:) WithSettings+ , Functor+ , Run (InputT IO)+ ) -- | Modifies a wizard so that it will run with different Haskeline 'Settings' to the top level input monad.-withSettings :: Settings IO -> Wizard Haskeline a -> Wizard Haskeline a-withSettings sets (Wizard (MaybeT v)) = Wizard $ MaybeT $ prompt $ Backend $ SetSettings sets $ v+withSettings :: (WithSettings :<: b) => Settings IO -> Wizard b a -> Wizard b a+withSettings sets (Wizard (MaybeT v)) = Wizard $ MaybeT $ inject (WithSettings sets v) -instance MonadIO (Wizard Haskeline) where- liftIO = prompt . Backend . ArbitraryIO+data WithSettings w = WithSettings (Settings IO) w deriving (Functor) -maybeToException :: (Monad m, Exception e) => e -> Maybe a -> m a-maybeToException e (Just v) = return v-maybeToException e (Nothing) = throw e+instance Run (InputT IO) Output where runAlgebra (Output s w) = outputStr s >> w+instance Run (InputT IO) OutputLn where runAlgebra (OutputLn s w) = outputStrLn s >> w+instance Run (InputT IO) Line where runAlgebra (Line s w) = getInputLine s >>= mEof w+instance Run (InputT IO) Character where runAlgebra (Character s w) = getInputChar s >>= mEof w+instance Run (InputT IO) LinePrewritten where runAlgebra (LinePrewritten p s1 s2 w) = getInputLineWithInitial p (s1,s2) >>= mEof w+instance Run (InputT IO) Password where runAlgebra (Password p mc w) = getPassword mc p >>= mEof w+instance Run (InputT IO) ArbitraryIO where runAlgebra (ArbitraryIO iov f) = liftIO iov >>= f+instance Run (InputT IO) WithSettings where runAlgebra (WithSettings sets w) = liftIO (runInputT sets w)++mEof = maybe (throw UnexpectedEOF) +++-- | A simple identity function, used to restrict types if the type inferred by GHC is too general.+-- You could achieve the same effect with a type signature, but this is slightly less typing.+haskeline :: Wizard Haskeline a -> Wizard Haskeline a+haskeline = id+
System/Console/Wizard/Internal.hs view
@@ -1,85 +1,172 @@-{-# LANGUAGE GADTs, KindSignatures #-}-module System.Console.Wizard.Internal ( WizardAction (..)+{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, DeriveFunctor, FlexibleContexts, TypeOperators, GeneralizedNewtypeDeriving, Trustworthy, ExistentialQuantification, EmptyDataDecls #-}+module System.Console.Wizard.Internal ( Wizard (..) , PromptString (..)+ , (:+:) (..)+ , (:<:)+ , inject+ , Run (..)+ , run+ -- $functors+ , Output (..)+ , OutputLn (..)+ , Line (..)+ , LinePrewritten (..)+ , Password (..)+ , Character (..)+ , ArbitraryIO (..) -- $backend ) where+import Control.Monad.Free+import Control.Monad.Trans.Maybe+import Control.Applicative -type PromptString = String +-- | A string for a prompt+type PromptString = String --- | Internally, a 'Wizard' is essentially a prompt monad with a 'WizardAction'. A constructor exists for each primitive action, as well--- as a special \"escape hatch\" constructor ('Backend') used for writing backend-specific primitives and modifiers.--- Each back-end has a corresponding data type, used as a type parameter for 'Wizard'. This data type is usually opaque, but internally--- specifies additional primitive actions that are specific to the back-end.--- 'WizardAction' is parameterised by this data type (for use in the 'Backend' constructor), the prompt monad itself (so that modifiers--- can be made as well as primitives) and the return type of the action.-data WizardAction :: ((* -> *) -> * -> *) -> (* -> *) -> * -> * where- Line :: PromptString -> WizardAction b m String- LinePreset :: PromptString -> String -> String -> WizardAction b m String- Password :: PromptString -> Maybe Char -> WizardAction b m String- Character :: PromptString -> WizardAction b m Char- Output :: String -> WizardAction b m ()- OutputLn :: String -> WizardAction b m () - Backend :: b m a -> WizardAction b m a+-- | A @Wizard b a@ is a conversation with the user via back-end @b@ that will result in a data type @a@, or may fail.+-- A 'Wizard' is made up of one or more \"primitives\" (see below), composed using the 'Applicative',+-- 'Monad' and 'Alternative' instances. The 'Alternative' instance is, as you might expect, a maybe-style cascade. +-- If the first wizard fails, the next one is tried. `mzero` can be used to induce failure directly.+-- +-- The 'Wizard' constructor is exported here for use when developing backends, but it is better for end-users to +-- simply pretend that 'Wizard' is an opaque data type. Don't depend on this unless you have no other choice.+-- +-- 'Wizard's are, internally, just a maybe transformer over a free monad built from some coproduct of functors,+-- each of which is a primitive action.+newtype Wizard backend a = Wizard (MaybeT (Free backend) a)+ deriving (Monad, Functor, Applicative, Alternative, MonadPlus)++-- | Coproduct of two functors+data (f :+: g) w = Inl (f w) | Inr (g w) deriving Functor++-- | Subsumption of two functors. You shouldn't define any of your own instances of this when writing back-ends, rely only on GeneralizedNewtypeDeriving.+class (Functor sub, Functor sup) => sub :<: sup where+ inj :: sub a -> sup a++instance Functor f => f :<: f where inj = id+instance (Functor f, Functor g) => f :<: (f :+: g) where inj = Inl+instance (Functor f, Functor g, Functor h, f :<: g) => f :<: (h :+: g) where inj = Inr . inj++-- | Injection function for free monads, see \"Data Types a la Carte\" from Walter Swierstra, @http:\/\/www.cs.ru.nl\/~W.Swierstra\/Publications\/DataTypesALaCarte.pdf@+inject :: (g :<: f ) => g (Free f a) -> Free f a+inject = Impure . inj++-- | A class for implementing actions on a backend. E.g Run IO Output provides an interpreter for the Output action in the IO monad.+class Run a b where+ runAlgebra :: b (a v) -> a v ++instance (Run b f, Run b g) => Run b (f :+: g) where+ runAlgebra (Inl r) = runAlgebra r+ runAlgebra (Inr r) = runAlgebra r++infixr 9 :+:++-- $functors+-- Each of the following functors is a primitive action. A back-end provides interpreters for these actions using the 'Run' class,++data Output w = Output String w deriving Functor+data OutputLn w = OutputLn String w deriving Functor+data Line w = Line PromptString (String -> w) deriving Functor+data Character w = Character PromptString (Char -> w) deriving Functor+data LinePrewritten w = LinePrewritten PromptString String String (String -> w) deriving Functor+data Password w = Password PromptString (Maybe Char) (String -> w) deriving Functor+data ArbitraryIO w = forall a. ArbitraryIO (IO a) (a -> w) +instance Functor (ArbitraryIO) where+ fmap f (ArbitraryIO iov f') = ArbitraryIO iov (fmap f f')++++run' :: (Functor f, Monad b, Run b f) => Free f a -> b a+run' = foldFree return runAlgebra++-- | Run a wizard using some back-end.+run :: (Functor f, Monad b, Run b f) => Wizard f a -> b (Maybe a)+run (Wizard c) = run' (runMaybeT c)++ -- $backend -- A short tutorial on writing backends. -- -- Backends consist of two main components: -- --- 1. A back-end data type (the type parameter to 'Wizard'), which includes constructors--- for any primitive actions or modifiers that are specific to the back-end.+-- 1. A monad, @M@, in which the primitive actions are interpreted. 'Run' instances specify an interpreter for each supported+-- action, e.g @Run M Output@ will specify an interpreter for the 'Output' primitive action in the monad M. ----- 2. An interpreter function, of type @Wizard DataType a -> B (Maybe a)@ for some type @B@ (depending on the backend).--- Typically this function will provide semantics for each 'WizardAction' using 'runRecPromptM' or similar.--- --- The 'Backend' constructor can be used to add back-end specific primitives and modifiers.+-- 2. A newtype, e.g @Backend a@, which is a functor, usually implemented by wrapping a coproduct of all supported features.+-- '(:<:)' instances, the 'Functor' instance, and the 'Run' instance are provided by generalized newtype deriving. -- --- As an example, suppose I am writing a back-end to @IO@, like "System.Console.Wizard.BasicIO".--- One additional primitive action that I might want to include is the ability to run arbitrary @IO@ actions while a wizard is running.--- So, my backend data type will be:+-- As an example, suppose I am writing a back-end to @IO@, like "System.Console.Wizard.BasicIO". I want to support basic input and output,+-- and arbitrary IO, so I declare instances for 'Run' for the 'IO' monad: ----- @--- data MyBackend (m :: * -> *) r = ArbitraryIO (IO r) -- kind signature to avoid defaulting to *--- @+-- @+-- instance Run IO Output where runAlgebra (Output s w) = putStr s >> w+-- instance Run IO OutputLn where runAlgebra (OutputLn s w) = putStrLn s >> w+-- instance Run IO Line where runAlgebra (Line s w) = getLine >>= w+-- instance Run IO Character where runAlgebra (Character s w) = getChar >>= w+-- instance Run IO ArbitraryIO where runAlgebra (ArbitraryIO iov f) = iov >>= f+-- @+-- +-- And then I would define the newtype for the backend, which we can call @MyIOBackend@:+-- +-- @+-- newtype MyIOBackend a = MyIOBackend ((Output :+: OutputLn :+: Line :+: Character :+: ArbitraryIO) a)+-- deriving ( Functor, Run IO+-- , (:<:) Output+-- , (:<:) OutputLn+-- , (:<:) Line+-- , (:<:) Character+-- , (:<:) ArbitraryIO+-- )+-- @+--+-- A useful convenience is to provide a simple identity function to serve as a type coercion:+-- +-- @+-- myIOBackend :: Wizard MyIOBackend a -> Wizard MyIOBackend a+-- myIOBackend = id+-- @ -- --- And my interpreter function will be:+-- One additional primitive action that I might want to include is the ability to clear the screen at a certain point.+-- So, we define a new data type for the action: ----- @--- runWizardMyBackend :: Wizard MyBackend a -> IO a--- runWizardMyBackend (Wizard (MaybeT c)) = runRecPromptM f c--- where f :: WizardAction MyBackend (RecPrompt (WizardAction MyBackend)) a -> IO a --- f (Output s) = putStr s--- f (... ) = ...--- f (Backend (ArbitraryIO io)) = io--- @+-- @+-- data ClearScreen w = ClearScreen w deriving Functor -- via -XDeriveFunctor+-- @ -- --- And then the action can be easily defined:+-- And a \"smart\" constructor for use by the user: ----- @--- runIO :: IO a -> Wizard MyBackend a--- runIO = prompt . Backend . ArbitraryIO --- @+-- @+-- clearScreen :: (ClearScreen :\<: b) => Wizard b ()+-- clearScreen = Wizard $ lift $ inject (ClearScreen (Pure ())) +-- @ ----- I might also want to include a /modifier/, which say, colours any output text green. Assuming I have a function--- @--- withGreenText :: IO a -> IO a--- @--- which causes any output produced by the input action to be coloured green, we can use the 'Backend' constructor to transform--- this into a wizard modifier.+-- (These smart constructors all follow a similar pattern. See the source of "System.Console.Wizard" for more examples)+--+-- And then we define an interpreter for it: -- --- @---data MyBackend m r = ArbitraryIO (IO r)--- | GreenText (m r)+-- @+-- instance Run IO ArbitraryIO where runAlgebra (ClearScreen f) = clearTheScreen >> f+-- @ -----runWizardMyBackend :: Wizard MyBackend---runWizardMyBackend (Wizard (MaybeT c)) = runRecPromptM f c--- where f :: WizardAction MyBackend (RecPrompt (WizardAction MyBackend)) a -> IO a --- f (Output s) = putStr s--- f (... ) = ...--- f (Backend (ArbitraryIO io)) = io--- f (Backend (GreenText a)) = withGreenText $ runRecPromptM f a+-- Now, we can use this as-is simply by directly extending our back-end: -----greenText :: Wizard MyBackend a -> Wizard MyBackend a---greenText (Wizard (MaybeT a)) = prompt (Backend (GreenText a))--- @+-- @+-- foo :: Wizard (ClearScreen :+: MyIOBackend)+-- foo = clearScreen >> output \"Hello World!\"+-- @ --+-- Or, we could modify @MyIOBackend@ to include the extension directly.+--+--+-- For custom actions that /return/ output, the definition looks slightly different. Here is the definition of Line:+--+-- @+-- data Line w = Line (PromptString) (String -> w) deriving Functor -- via -XDeriveFunctor+-- @ -- +-- And the smart constructor looks like this:+--+-- @+-- line :: (Line :\<: b) => PromptString -> Wizard b String+-- line s = Wizard $ lift $ inject (Line s Pure) +-- @
+ System/Console/Wizard/Pure.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE DeriveDataTypeable, FlexibleInstances, MultiParamTypeClasses, FlexibleInstances, TypeOperators, DoAndIfThenElse, GeneralizedNewtypeDeriving, Trustworthy #-}+module System.Console.Wizard.Pure+ ( Pure + , UnexpectedEOI (..)+ , runPure+ , PureState (..)+ ) where++import System.Console.Wizard+import System.Console.Wizard.Internal +import Control.Monad.Trans+import Control.Monad.State.Lazy+import Control.Monad.Trans.Maybe+import Control.Applicative((<$>))+import Data.Typeable+import Data.Sequence(Seq, (|>), (><), fromList, empty)+import Control.Monad+import Control.Exception+import Control.Arrow+import Data.Foldable(toList)++-- | Thrown if the wizard ever unexpectedly runs out of input.+data UnexpectedEOI = UnexpectedEOI deriving (Show, Typeable)+instance Exception UnexpectedEOI++-- | The pure backend is actually just a simple state monad, with the following state.+type PureState = ([String], Seq Char)++-- | Run a wizard in the Pure backend+runPure :: Wizard Pure a -> String -> (Maybe a, String)+runPure wz input = let (a,(_,o)) = runState (run wz) (lines input, empty) + in (a, toList o)++getPureLine :: State PureState String+getPureLine = do crashIfNull+ x <- head . fst <$> get+ modify (first tail)+ return x++crashIfNull :: State PureState ()+crashIfNull = do (x, y ) <- get+ when (null x) $ throw UnexpectedEOI++getPureChar :: State PureState Char+getPureChar = do crashIfNull+ x <- null . head . fst <$> get+ if x then do + modify (first tail)+ return '\n'+ else do+ r <- head . head . fst <$> get+ modify (first (\ (x : r) -> tail x : r))+ return r+ +outputPure :: String -> State PureState () +outputPure s = modify (second (>< fromList s))+ >> modify (\s -> s `seq` s)++outputLnPure :: String -> State PureState () +outputLnPure s = modify (second $ (|> '\n') . (>< fromList s))+ >> modify (\s -> s `seq` s)+++instance Run (State PureState) Output where runAlgebra (Output s w) = outputPure s >> w+instance Run (State PureState) OutputLn where runAlgebra (OutputLn s w) = outputLnPure s >> w+instance Run (State PureState) Line where runAlgebra (Line s w) = getPureLine >>= w+instance Run (State PureState) Character where runAlgebra (Character s w) = getPureChar >>= w++-- | The 'Pure' backend supports only simple input and output.+-- Support for 'Password' and 'LinePrewritten' features can be added with +-- a shim from "System.Console.Wizard.Shim". +newtype Pure a = Pure ((Output :+: OutputLn :+: Line :+: Character) a) + deriving ( (:<:) Output+ , (:<:) OutputLn+ , (:<:) Line+ , (:<:) Character+ , Functor+ , Run (State PureState)+ )+
wizards.cabal view
@@ -7,7 +7,7 @@ -- The package version. See the Haskell package versioning policy -- (http://www.haskell.org/haskellwiki/Package_versioning_policy) for -- standards guiding when and how versions should be incremented.-Version: 0.1+Version: 1.0 -- A short (one-line) description of the package. Synopsis: High level, generic library for interrogative user interfaces@@ -36,10 +36,11 @@ Everything from interactive system scripts, to installation wizards, to full-blown shells can be implemented with the support of @wizards@. .- It is developed transparently on top of a Prompt monad, which separates out the semantics of the program from any- particular interface. A variety of backends exist, including "System.Console.Wizard.Haskeline" and - "System.Console.Wizard.BasicIO". It is also possible to write your own backends. While both built-in backends - operate on a console, there is no reason why @wizards@ cannot also be used for making GUI wizard interfaces.+ It is developed transparently on top of a free monad, which separates out the semantics of the program from any+ particular interface. A variety of backends exist, including console-based "System.Console.Wizard.Haskeline" and + "System.Console.Wizard.BasicIO", and the pure "System.Console.Wizard.Pure". It is also possible to write your + own backends, or extend existing back-ends with new features. While both built-in IO backends operate on a+ console, there is no reason why @wizards@ cannot also be used for making GUI wizard interfaces. . . See the github page for examples on usage:@@ -69,7 +70,7 @@ source-repository this type: git location: git://github.com/liamoc/wizards.git- tag: 0.1+ tag: 1.0 Library@@ -78,10 +79,11 @@ System.Console.Wizard.Internal System.Console.Wizard.Haskeline System.Console.Wizard.BasicIO- + System.Console.Wizard.Pure+ Extensions: OverlappingInstances -- Packages needed in order to build this package.- Build-depends: base == 4.*, haskeline == 0.6.*, mtl == 2.0.*, transformers == 0.2.*, MonadPrompt == 1.0.*+ Build-depends: base == 4.*, haskeline == 0.6.*, mtl == 2.0.*, transformers == 0.2.*, control-monad-free ==0.5.*, containers == 0.4.* -- Modules not exported by this package. -- Other-modules: