Cameron Ball
Cameron Ball

Reputation: 4108

How can I make the signature of this function more precise

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

Answers (2)

Daniel Wagner
Daniel Wagner

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

typedfern
typedfern

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

Related Questions