Search code examples
validationhaskellscopeeither

Haskell - Continuing to next do after validating with either


I'm new to Haskell and using do notation to validate a user's choice with an Either.

userChoice :: String -> Either String String
userChoice option
    | option == "1" = Right "You proceed with option 1."
    | option == "2" = Right "You proceed with option 2"
    | option == "3" = Right "You proceed with option 3"
    | option == "4" = Right "You proceed with option 4"
    | otherwise = Left $ "\n\nInvalid Option...\nWhat would you like to do?\n" ++ displayOptions(0)

displayOptions :: Int -> String
displayOptions option
    | option == 0 = "1 - Option 1\n2 - Option 2\n3 - Option 3\n4 - Option 4"
    | otherwise = "invalid"

main = do
    putStrLn "Welcome."
    let start startMessage = do
        putStrLn startMessage
        let ask message = do
            putStrLn message
            choice <- getLine
            either ask ask $ userChoice(choice)
        ask $ "What would you like to do?\n" ++ displayOptions(0)
    start "Choose a number between 1-4."

This works fine, but after the user chooses a correct option, I'd like to proceed to the next part of the program. I could use return but I lose which option the user chose.

As an example, I can put return here when the user chooses a right, but then it will not say the String "You proceed with option...".

main = do
    putStrLn "Welcome."
    let start startMessage = do
        putStrLn startMessage
        let ask message = do
            putStrLn message
            choice <- getLine
            either ask return $ userChoice(choice)
        ask $ "What would you like to do?\n" ++ displayOptions(0)
    start "Choose a number between 1-4."

    -- putStrLn userChoice2(choice)

    let continue continueMessage = do
        putStrLn continueMessage
        let ask message = do
            putStrLn message
            choice <- getLine
            either continue continue $ userChoice(choice)
        ask $ "What would you like to do?"
    continue "We are now here..."

If I try to put continue as the right option then it throws a scope error. Similarly, if I use return and try to make a primitive clone function for userChoice I no longer have access to the scope choice is in.

userChoice2 :: String -> String
userChoice2 option
    | option == "1" = "You proceed with option 1."
    | option == "2" = "You proceed with option 2"
    | option == "3" = "You proceed with option 3"
    | option == "4" = "You proceed with option 4"
    | otherwise = "\n\nInvalid Option...\nWhat would you like to do?\n" ++ displayOptions(0)

Is there a way to chain these together elegantly?


Solution

  • It sounds like you just need to bind the value returned in the monad from start, like this:

    main = do
        putStrLn "Welcome."
        let start startMessage = do
            putStrLn startMessage
            let ask message = do
                putStrLn message
                choice <- getLine
                either ask return $ userChoice(choice)
            ask $ "What would you like to do?\n" ++ displayOptions(0)
    
        -- THE NEW BIT I ADDED:
        opt <- start "Choose a number between 1-4."
        putStrLn opt   -- THIS PRINTS "You proceed with ..."
    
        -- SNIP
    

    You can read about "desugaring do notation" to learn how this translates to lambdas and the methods of the Monad class (>>= and return).


    A couple other suggestions:

    You can use (and should prefer) pattern matching instead of guards and == (a method of the Eq class, which not all types implement). E.g.

    userChoice2 option = case option of
        "1" -> "You proceed with option 1."
        "2" -> "You proceed with option 2"
    

    Remember that in haskell the syntax for calling foo on a and b is foo a b not foo(a,b)

    It can be very helpful to turn on warnings (both when you're learning and on production code bases) e.g. here I load your file in ghci after turning on -Wall:

    Prelude> :set -Wall
    Prelude> :l k.hs
    [1 of 1] Compiling Main             ( k.hs, interpreted )
    
    k.hs:15:1: warning: [-Wmissing-signatures]
        Top-level binding with no type signature: main :: IO b
       |
    15 | main = do
       | ^^^^
    
    k.hs:24:5: warning: [-Wunused-do-bind]
        A do-notation statement discarded a result of type ‘String’
        Suppress this warning by saying
          ‘_ <- start "Choose a number between 1-4."’
       |
    24 |     start "Choose a number between 1-4."
       |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    Ok, one module loaded.
    

    Notice it warns you that start returns IO String but that you've done nothing with the String value, which indeed was an error. If you really want to discard the value you can use void to make it explicit and silence the error.