Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Multiple-Choice Reading Option #81

Open
initial-mockingbird opened this issue Oct 4, 2022 · 3 comments
Open

Implement Multiple-Choice Reading Option #81

initial-mockingbird opened this issue Oct 4, 2022 · 3 comments

Comments

@initial-mockingbird
Copy link

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 the Alt monoid)
  • Or prints an error message to stderr in case of error, returning Nothing
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 returning Nothing, it may be better to return Either 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 normal List 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 :)

@chshersh
Copy link
Owner

chshersh commented Oct 6, 2022

@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:

  1. How do you decide which value to put in [] in the prompt here?
    Are you sure? [y]/n
    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 the NonEmpty list entirely!
  2. 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 chshersh added the hacktoberfest https://hacktoberfest.com/ label Oct 6, 2022
@initial-mockingbird
Copy link
Author

@chshersh Thank you for taking the time to review everything so thoughtfully!

  1. 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).

  1. 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 the Mode (many initiallyActives against a SingleAnswer), 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?)

@chshersh
Copy link
Owner

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 🤔

@chshersh chshersh removed the hacktoberfest https://hacktoberfest.com/ label Feb 28, 2023
@chshersh chshersh removed the terminal label Mar 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants