Implement Multiple-Choice Reading Option
initial-mockingbird opened this issue ยท 3 comments
A generalization for #9 .
The choices can be modeled as their display name coupled with a parsing function to the desired data type of the answer:
data MultipleChoiceOption a = MultipleChoiceOption
{ displayName :: ByteString
, parsingFun :: ByteString -> Maybe a
}
Then, we can model multiple choices as: a message prompt, a non-empty list of choices (whose default choice will always be the head), and a value for the default option:
data MultipleChoicePrompt a = MultipleChoicePrompt
{ promptMessage :: ByteString
, promptOptions :: NonEmpty (MultipleChoiceOption a)
, defaultValue :: a
}
Thus, the following prompt data:
MultipleChoicePrompt
{ promptMessage="Are you sure?"
, promptOptions=
MultipleChoiceOption
{ displayName="y"
, parsingFun = \s -> if s == "y" then Just Yes else Nothing
}
:|
[ MultipleChoiceOption
{ displayName="n"
, parsingFun = \s -> if s == "n" then Just No else Nothing
}
]
, defaultValue=Yes
}
Will represent:
Are you sure? [y]/n
Finally, we can define a function that:
- Prints the question (via an aux function that takes the
MultipleChoicePrompt
- Reads answer
- Returns the first choice that is parsed correctly (can be done with a
foldMap
over theAlt
monoid) - Or prints an error message to
stderr
in case of error, returningNothing
multipleChoiceQuestion
:: MonadIO m
=> MultipleChoicePrompt a
-> m (Maybe a)
An example run would be:
>>> multipleChoiceQuestion
MultipleChoicePrompt
{ promptMessage="Are you sure?"
, promptOptions=
MultipleChoiceOption
{ displayName="y"
, parsingFun = \s -> if s == "y" then Just Yes else Nothing
}
:|
[ MultipleChoiceOption
{ displayName="n"
, parsingFun = \s -> if s == "n" then Just No else Nothing
}
, MultipleChoiceOption
{ displayName="cancel"
, parsingFun = \s -> if s == "cancel" then Just Cancel else Nothing
}
]
, defaultValue=Yes
}
>>> n
Just No
Something to notice is that the function will take the default value and build a parser to accept empty responses:
>>> multipleChoiceQuestion
MultipleChoicePrompt
{ promptMessage="Are you sure?"
, promptOptions=
MultipleChoiceOption
{ displayName="y"
, parsingFun = \s -> if s == "y" then Just Yes else Nothing
}
:|
[ MultipleChoiceOption
{ displayName="n"
, parsingFun = \s -> if s == "n" then Just No else Nothing
}
, MultipleChoiceOption
{ displayName="cancel"
, parsingFun = \s -> if s == "cancel" then Just Cancel else Nothing
}
]
, defaultValue=Yes
}
>>>
Just Yes
Finally, some points that might need refinement:
- Default value might be different than the value that is returned by the parsing function.
- Instead of printing to
stderr
and returningNothing
, it may be better to returnEither Bytestring a
and let another function decide what to do. - Instead of having a
Non-Empty
List, we could have either a tuple with the first two options and a normalList
for the rest, or a sized container (i.e, something from Data.Size). Having a multiple option question with just one option feels wrong.
I'm willing to try and accommodate all suggestions to this implementation :)
@initial-mockingbird Thanks for putting so much time into the design specification! I'll need some time to think if it's possible to come up with a simpler design ๐ค
Another few points that need refinement:
- How do you decide which value to put in
[]
in the prompt here?If it's the default value, it doesn't have a function to print it to string. Also, not only it can be different from the value returned by the parsing function, it might be different from the first element in theAre you sure? [y]/n
NonEmpty
list entirely! - Sometimes it's desirable to avoid the default value entirely and force users to select an option specifically.
Also, this can be done separately but I needed in the past the ability to specify several values by the multiple-choice option. Would be nice if this can be easily supported by a single representation but I also see the value in keeping the interface simple for users.
@chshersh Thank you for taking the time to review everything so thoughtfully!
- How do you decide which value to put in
[]
in the prompt here?Are you sure? [y]/n
The value between the []
would be the displayName
from the head of the Non-Empty list (that is, the default value will always be the head of the list).
- Sometimes it's desirable to avoid the default value entirely and force users to select an option specifically.
You are right! maybe a quick fix would be to turn the default :: a
(which represents the default value that should be returned in case an empty/whitespaced string is input) field into a default :: Maybe a
.
Also, this can be done separately but I needed in the past the ability to specify several values by the multiple-choice option.
Would be nice if this can be easily supported by a single representation but I also see the value in keeping the interface simple for users
Sadly, I don't really see how to incorporate this into the current proposal :( (how would the user input multiple choices?). Nevertheless, maybe a better design than the one I proposed would fall along the lines of the GHCUP TUI menu? i.e:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Multiple Choice Questions? โ
โ > [โ๏ธ] yay โ
โ > [โ] Not this one! โ
โ > [โ๏ธ] This too! โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Single Choice Question with default? โ
โ > [โ๏ธ] yay โ
โ > [โ] No Sir! โ
โ > [โ] Only one check can be marked at a time โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Single Choice Question with no default? โ
โ > [โ] Initially all disabled โ
โ > [โ] No Sir! โ
โ > [โ] Only one check can be marked at a time โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
This means that:
- We don't need to ask for parsing functions
- We no longer need the
default
field. - The
multipleChoiceQuestion
function always succeeds (no need to return a maybe).
Thus, the design would end up being:
-- tag in case one wants to receive multiple answes
data Mode = SingleAnswer | MultipleAnswer
data MultipleChoiceOption a = MultipleChoiceOption
{ displayName :: ByteString
, initiallyActive :: Bool -- which options should be active by default?
, value :: a -- no more parsing!
}
data MultipleChoicePrompt a = MultipleChoicePrompt
{ promptMessage :: ByteString
, promptOptions :: NonEmpty (MultipleChoiceOption a)
, mode :: Mode
}
multipleChoiceQuestion
:: MonadIO m
=> MultipleChoicePrompt a
-> m (NonEmpty a)
And then we can expose things like Yes/No questions, as partial application of multipleChoice
:
As a final note, I think this would benefit from the ncurses library.
Caveats:
- The
initiallyActive
may conflict with theMode
(manyinitiallyActive
s against aSingleAnswer
), in those cases, the single default value can be the first one on the list. - I'm not really sure how portable
ncurses
is (does it work on every terminal? what about windows terminal?). - Maybe
multipleChoiceQuestion
should also get an additional argument for aesthetic options? (Should the question be boxed? should the options have the check and cross or other characters?)
Iris focuses only on CLI, not TUI. So TUI-inspired interfaces are out of scope for this project. And we won't benefit from ncurses
either.
And then we can expose things like Yes/No questions, as partial application of multipleChoice:
I would be pretty happy to have a separate data Answer = No | Yes
type and specialised functions for yes-no prompts. No need to scaffold a bigger machinery for a simple case ๐ Especially, because I imagine, the yes/no questions would be a more common use case.
As for the design of this feature, I'm still not satisfied. But I need to think about the better design ๐ค