3
\$\begingroup\$

I'm writing my first small programs in Haskell and still getting a feel for the syntax and idioms. I've written this Mad Libs implementation using recursive IO. I've used IO actions throughout and I'm sure there must be a better way of splitting up this code to separate pure functions from IO actions. Also, I'm not happy with the printf statement, but I couldn't find a native way to apply an arbitrary number of list items to printf.

import Text.Printf

getAnswer :: String -> IO String
getAnswer question = do
    putStrLn question
    answer <- getLine
    return answer

getAnswers :: [String] -> [String] -> IO [String]
getAnswers [] ys = return ys
getAnswers (x:xs) ys = do
    answer <- getAnswer x
    let answers = ys ++ [answer]
    getAnswers xs answers

main = do
    let questions = ["Enter a noun:", "Enter a verb:", "Enter an adjective:", "Enter an adverb:"]
    let madlib = "Your %s is %s up a %s mountain %s."
    answers <- getAnswers questions []
    printf madlib (answers!!0) (answers!!1) (answers!!2) (answers!!3)
    putStrLn ""
\$\endgroup\$
3
\$\begingroup\$

Can we make getAnswer simpler or out of IO? Well, not really. You want to ask the use a question, and you want to get an answer. So all we could do is to reduce the amount of unnecessary code:

getAnswer :: String -> IO String
getAnswer question = putStrLn question >> getLine
-- or
--                 = do
--      putStrLn question
--      getLine

However, getAnswers can be refactored quite heavily. First of all, its interface isn't really developer-friendly. What are the questions? What are the answers? We should probably hide that in the bowels of our function:

getAnswers :: [String] -> IO [String]
getAnswers xs = go xs [] 
  where go [] ys = return ys
        go (x:xs) ys = do
          answer <- getAnswer x
          let answers = ys ++ [answer]
          go xs answers

But ++ [...] isn't really best-practice. Instead, you would ask all other questions and then combine them:

  where go [] = return []
        go (x:xs) = do
          answer <- getAnswer x
          otherAnswers <- getAnswers x
          return (answer : otherAnswers)

But at that point, we're merily copying mapM's functionailty. Therefore, your getAnswers should be

getAnswers :: [String] -> IO [String]
getAnswers = mapM getAnswer

A lot simpler.

Now for your main. If you don't know how many words you'll get you will need a list, correct. But lets check the structure of your result:

 "Your %s is %s up a %s mountain %s."
       1     2       3           4

There is a pattern. We have our text, then whatever the user gave us, then again our text, and so on. Let's split that into fragments:

["Your ","%s"," is ","%s"," up a ","%s"," mountain ","%s","."]
--       ^^^^        ^^^^          ^^^^              ^^^^

This brings up the following idea: if you have a list of your answers, you only need the list of the other words, right?

["Your "," is "," up a "," mountain ","."]

And then we need to "zip" that list with yours:

interleave :: [a] -> [a] -> [a]
interleave (x:xs) (y:ys) = x : y : interleave xs ys
interleave xs     _      = xs

We end up with the following main:

main = do
    let questions = ["Enter a noun:", "Enter a verb:", "Enter an adjective:", "Enter an adverb:"]
    let madlib = ["Your "," is "," up a "," mountain ","."]
    answers <- getAnswers questions

    putStrLn $ interleave madlib questions

Here's all the code at once:

getAnswer :: String -> IO String
getAnswer q = putStrLn q >> getLine

getAnswers :: [String] -> IO [String]
getAnswers = mapM getAnswer

interleave :: [a] -> [a] -> [a]
interleave (x:xs) (y:ys) = x : y : interleave xs ys
interleave xs     _      = xs

main :: IO ()
main = do
    let questions = ["Enter a noun:", "Enter a verb:", "Enter an adjective:", "Enter an adverb:"]
    let madlib = ["Your "," is "," up a "," mountain ","."]
    answers <- getAnswers questions

    putStrLn $ interleave madlib questions

Exercises

The interleave function above is left-biased. Why? Could this pose problems for your program? Why not?

\$\endgroup\$
  • \$\begingroup\$ Thanks for such an in depth review, it's given me a lot of good feedback. \$\endgroup\$ – Pappa Nov 11 '16 at 9:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.