Prompt: README / hackage / github

Have you ever wanted to specify a computation involving some limited form of IO — like querying a database, or asking stdio — but didn’t want a computation in the IO monad, opening the entire can of worms that is arbitrary IO ? Have you ever looked at complicated IO a you wrote last week at 4am and prayed that it didn’t launch missiles if you decided to execute it? Do you want to be able to run an effectful computation and explicitly say what IO it can or cannot do?

Introducing the prompt library! It’s a small little lightweight library that allows you to specify and describe computations involving forms of effects where you “ask” with a value and receive a value in return (such as a database query, etc.), but not ever care about how the effects are fulfilled — freeing you from working directly with IO.

data Foo = Foo { fooBar :: String , fooBaz :: Int } deriving Show -- ask with a String, receive a String as an answer promptFoo :: Prompt String String Foo = Foo promptFoo <$> prompt "bar" prompt <*> fmap length (prompt "baz" ) (prompt

Running

You can now “run it” in IO, by talking to stdio —

> runPromptM promptFoo $ \str -> putStrLn str >> getLine ghcirunPromptM promptFoo\strstr -- stdout prompt bar > hello ! -- stdin response typed in hello -- stdout prompt baz > i am baz -- stdin response typed in i am baz Foo "hello!" 8 -- result

(this is also just interactP promptFoo )

Or you can maybe request it from the environment variables:

> import System.Environment ghci > setEnv "bar" "hello!" ghcisetEnv > setEnv "baz" "i am baz" ghcisetEnv > runPromptM promptFoo getEnv ghcirunPromptM promptFoo getEnv Foo "hello!" 8

Or maybe you want to fulfill the prompts purely:

> import qualified Data.Map as M ghci > let testMap = M.fromList [( "bar" , "hello!" ), ( "baz" , "i am baz" )] ghcitestMapM.fromList [(), ()] > runPrompt promptFoo (testMap M.! ) ghcirunPrompt promptFoo (testMap Foo "hello!" 8

With Prompt , specify the computation and your logic without involving any IO, so you can write safe code without arbitrary side effects. If you ever receive a Prompt , you know it can’t wipe out your hard drive or do any IO other than exactly what you allow it to do! I’d feel more safe running a Prompt a b r than an IO r .

You can also do some cute tricks; Prompt a () r with a “prompt response function” like putStrLn lets you do streaming logging, and defer how the logging is done — to IO, to a list?

> let logHelloWord = mapM_ prompt [ "hello" , "world" ] ghcilogHelloWordprompt [ > runPromptM logHelloWorld putStrLn ghcirunPromptM logHelloWorld hello world > execWriter $ runPromptM logHelloWorld tell ghciexecWriterrunPromptM logHelloWorld tell "helloworld"

Prompt () b r is like a fancy ReaderT b m r , where you “defer” the choice of the Monad.

Combining with other effects

Prompt can be used as an underlying “effects” source for libraries like pipes, conduit, and auto. If your effects are only ever asking and prompting and receiving, there’s really no need to put the entire power of IO underneath your DSL as an effects source. That’s just crazy!

Prompt can be used with monad transformers to give you safe underlying effect sources, like StateT s (Prompt a b) r , which is a stateful computation which can sometimes sequence “prompty” effects. Prompt is also itself a “Traversable transformer”, with PrompT a b t r . It can perform computations in the context of a Traversable t , to be able to incorporate built-in short-circuiting and logging, etc.

This is all abstracted over with MonadPrompt , MonadError , MonadPlus , etc., typeclasses —

promptFoo2 :: ( MonadPlus m, MonadPrompt String String m) => m Foo m,m) = do promptFoo2 <- prompt "bar" barprompt <- prompt "baz" strprompt case readMaybe str of readMaybe str Just baz -> return $ Foo bar baz bazbar baz Nothing -> mzero mzero -- more polymorphic promptFoo :: MonadPrompt String String m => m Foo = Foo promptFoo <$> prompt "bar" prompt <*> fmap length (prompt "baz" ) (prompt

You can run promptFoo as a MaybeT (Prompt String String) Foo , and manually unwrap:

> interactP . runMaybeT $ promptFoo2 ghciinteractPrunMaybeTpromptFoo2 bar > hello ! hello baz > i am baz i am baz Nothing > interactP . runMaybeT $ promptFoo2 ghciinteractPrunMaybeTpromptFoo2 bar > hello ! hello baz > 19 Just ( Foo "hello!" 19 )

Or you can run it as a PromptT String String MaybeT Foo , to have PromptT handle the wrapping/unwrapping itself:

> interactPT promptFoo2 ghciinteractPT promptFoo2 bar > hello ! hello baz > i am baz i am baz Nothing > interactPT $ promptFoo2 <|> promptFoo ghciinteractPTpromptFoo2promptFoo bar > hello ! hello baz > i am baz i am baz -- failed to parse --- retrying with promptFoo! bar > hello ! hello baz > i am baz i am baz Just ( Foo "hello" 8 )

The previous example of logHelloWorld ?

> runPromptT ( logHelloWorld :: PromptT String () ( Writer String ) ()) tell ghcirunPromptT (() () ()) tell "helloworld"

Runners

The “runners” are:

interactP :: Prompt String String r -> IO r interactPT :: Applicative t => PromptT String String t r -> IO (t r) t r(t r) runPrompt :: Prompt a b r -> (a -> b) -> r a b r(ab) runPromptM :: Monad m => Prompt a b r -> (a -> m b) -> m r a b r(am b)m r runPromptT :: PromptT a b t r -> (a -> t b) -> t r a b t r(at b)t r runPromptTM :: Monad m => PromptT a b t r -> (a -> m (t b)) -> m (t r) a b t r(am (t b))m (t r)

Note that runPromptM and runPromptTM can run in monads (like IO ) that are completely unrelated to the Prompt type itself. It sequences them all “after the fact”. It’s also interesting to note that runPrompt is just a glorified Reader (a -> b) r .

With runPromptTM , you can incorporate t in your “prompt response” function, too. Which brings us to our grand finale – environment variable parsing!

import Control.Monad.Error.Class import Control.Monad.Prompt import Text.Read import qualified Data.Map as M type Key = String type Val = String data MyError = MENoParse Key Val | MENotFound Key deriving Show promptRead :: ( MonadError MyError m, MonadPrompt Key Val m, Read b) m,m,b) => Key -> m b m b -- promptRead :: Read b => Key -> PromptT Key Val (Either MyError) b = do promptRead k <- prompt k respprompt k case readMaybe resp of readMaybe resp Nothing -> throwError $ MEParse k resp throwErrork resp Just v -> return v promptFoo3 :: MonadPrompt Key Val m => m Foo -- promptFoo3 :: Applicative t => PromptT Key Val t Foo = Foo <$> prompt "bar" <*> promptRead "baz" promptFoo3promptpromptRead -- -- running! -- Lookup environment variables, and "throw" an error if not found throughEnv :: IO ( Either MyError Foo ) = runPromptTM parseFoo3 $ \k -> do throughEnvrunPromptTM parseFoo3\k <- lookupEnv k envlookupEnv k return $ case env of env Nothing -> Left ( MENotFound k) k) Just v -> Right v -- Fulfill the prompt through user input throughStdIO :: IO ( Either MyError Foo ) = interactPT parseFoo3 throughStdIOinteractPT parseFoo3 -- Fulfill the prompt through user input; count blank responses as "not found" throughStdIOBlankIsError :: IO ( Either MyError Foo ) = runPromptTM parseFoo3 $ \k -> do throughStdIOBlankIsErrorrunPromptTM parseFoo3\k putStrLn k <- getLine resp return $ if null resp resp then Left ( MENotFound k) k) else Right resp resp -- Fulfill the prompt purely through a Map lookup throughMap :: M.Map Key Val -> Either MyError Foo = runPromptT parseFoo3 $ \k -> throughMap mrunPromptT parseFoo3\k case M.lookup k m of M.lookup k m Nothing -> Left ( MENotFound k) k) Just v -> Right v

Hope you enjoy! Please feel free to leave a comment, find me on twitter, leave an issue on the github, etc. — and I’m usually on freenode’s #haskell as jle` if you have any questions!

Comparisons

To lay it all on the floor,

newtype PromptT a b t r = PromptT { runPromptTM :: forall m . Monad m => (a -> m (t b)) -> m (t r) } a b t r(am (t b))m (t r) }

There is admittedly a popular misconception that I’ve seen going around that equates this sort of type to Free from the free package. However, Free doesn’t really have anything significant to do with this. Sure, you might be able to generate this type by using FreeT over a specifically chosen Functor, but…this is the case for literally any Monad ever, so that doesn’t really mean much :)

It’s also unrelated in this same manner to Prompt from the MonadPrompt package, and Program from operational too.

One close relative to this type is forall m. ReaderT (a -> m b) m r , where prompt k = ReaderT ($ k) . This is more or less equivalent to Prompt , but still can’t do the things that PromptT can do without a special instance of Monad.

This type is also similar in structure to Bazaar , from the lens package. The biggest difference that makes Bazaar unusable is because the RankN constraint is only Applicative , not Monad , so a Monad instance is impossible. Ignoring that (or if it’s okay for you to only use the Applicative instance), Bazaar forces the “prompting effect” to take place in the same context as the Traversable t …which really defeats the purpose of this whole thing in the first place (the idea is to be able to separate your prompting effect from your application logic). If the Traversable you want to transform has a “monad transformer” version, then you can somewhat simulate PromptT for that specifc t with the transformer version.

It’s also somewhat similar to the Client type from pipes, but it’s also a bit tricky to use that with a different effect type than the logic Traversable , as well…so it has a lot of the same difference as Bazaar here.

But this type is common/simple enough that I’m sure someone has it somewhere in a library that I haven’t been able to find. If you find it, let me know!