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 ""
200_success
146k22 gold badges190 silver badges479 bronze badges
asked Nov 10, 2016 at 6:53
\$\endgroup\$
0

1 Answer 1

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?

answered Nov 10, 2016 at 10:04
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for such an in depth review, it's given me a lot of good feedback. \$\endgroup\$ Commented Nov 11, 2016 at 9:42

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.