Reputation: 4108
I have two functions:
prompt :: Text -> (Text -> Either Text a) -> IO a
subPrompt :: Text -> (Text -> Bool) -> IO a -> IO (Maybe (Text, a))
subPrompt takes a second prompt (argument 3) and displays it if the function in argument 2 comes back as true after running the first prompt.
What I don't like is that argument 3 is IO a
I would like it to be something more like:
subPrompt :: Text -> (Text -> Bool) -> prompt -> IO (Maybe (Text, a))
But I know I can't do that. I'm stuck trying to think of a way to make it clearer from the signature what the third argument is. Is there some way I can define a clearer type? Or maybe I'm overthinking it and IO a
is actually fine - I'm pretty new to haskell.
Upvotes: 0
Views: 1020
Reputation: 152682
One way is to reify the two things as a data structure. So:
{-# LANGUAGE GADTs #-}
data Prompt a where
Prompt :: Text -> (Text -> Either Text a) -> Prompt a
SubPrompt :: Text -> (Text -> Bool) -> Prompt a -> Prompt (Maybe (Text, a))
Now because the third argument to SubPrompt
is a Prompt
, you know it must either be a call to SubPrompt
or Prompt
-- definitely not some arbitrary IO
action that might do filesystem access or some other nasty thing.
Then you can write an interpreter for this tiny DSL into IO
:
runPrompt :: Prompt a -> IO a
runPrompt (Prompt cue validator) = {- what your old prompt used to do -}
runPrompt (SubPrompt cue deeper sub) = {- what your old subPrompt used to do,
calling runPrompt on sub where needed -}
Besides the benefit of being sure you don't have arbitrary IO
as an argument to SubPrompt
, this has the side benefit that it makes testing easier. Later you could implement a second interpreter that is completely pure; say, something like this, which takes a list of texts to be treated as user inputs and returns a list of texts that the prompt output:
data PromptResult a = Done a | NeedsMoreInput (Prompt a)
purePrompt :: Prompt a -> [Text] -> ([Text], PromptResult a)
purePrompt = {- ... -}
Upvotes: 2
Reputation: 1257
Nothing wrong with making the second prompt a simple IO a
- specially if you document what it is somewhere.
That said, yes, it is good practice to make the types as self-explanatory as possible; you can create an alias:
type Prompt a = IO a
and then use it in subPrompt's signature:
subPrompt :: Text -> (Text -> Bool) -> Prompt a -> IO (Maybe (Text, a))
This makes the signature more self-explanatory, while still allowing you to pass any IO a
as the third parameter (the keyword type
just creates an alias).
But wait, there is more: you'd rather not accidentally pass any IO a
that isn't actually a prompt! You don't want to pass it an IO action that, say, launches the missiles...
So, we can declare an actual Prompt type (not just an alias, but a real type):
newtype Prompt a = Prompt { getPrompt :: IO a }
This allows you to wrap any value of type IO a
inside a type, ensuring it doesn't get mixed up with other functions with the same type, but different semantics.
The signature of subPrompt remains the same as before:
subPrompt :: Text -> (Text -> Bool) -> Prompt a -> IO (Maybe (Text, a))
But now you cannot pass just any old IO a
to it; to pass your prompt
, for example, you have to wrap it:
subPrompt "Do we proceed?" askYesNo (Prompt (prompt "Please enter your name" processName))
(subPrompt won't be able to call it directly, but will have to extract "prompt" from inside the wrapper: let actualPrompt = getPrompt wrappedPrompt
)
Upvotes: 1