As the title suggests, I'm a complete beginner in Haskell. I've just made my way through the "Input and Output" section of Learn You A Haskell For Greater Good and I thought it would be good to try my hand at a simple project like a "Guess The Word" game in Haskell.
The idea behind the game is to guess all the letters of a randomly selected word. If you guess all the letters, you win.
Without further ado, the complete program is below. I'm looking for feedback regarding program structure (particularly in separating I/O from pure functions), any inefficiencies I could look out for, any potential bugs, and any Haskell idioms and general best practices I could incorporate.
import Data.Char import Data.Maybe import System.Random data Difficulty = Easy | Medium | Hard deriving (Read, Show, Eq) createDifficulty :: String -> Maybe Difficulty createDifficulty s = lookup s [ ("e", Easy), ("m", Medium), ("h", Hard), ("", Easy) ] isDifficulty :: String -> Bool isDifficulty = isJust . createDifficulty randomNumber :: Int -> Int -> IO (Int) randomNumber begin end = do gen <- getStdGen return $ head $ randomRs (begin, end) gen -- Please don't provide review about these tables. They're just meant to be demonstrative of the general idea I'm going for. fetchWords :: Difficulty -> [String] fetchWords Easy = ["foo", "bar"] fetchWords Medium = ["abc", "def"] fetchWords Hard = ["foobar", "whoami"] generateSecret :: Difficulty -> IO (String) generateSecret difficulty = do let table = fetchWords difficulty num <- randomNumber 0 (length table) return (table !! num) replaceChar :: Int -> String -> Char -> String replaceChar n s c = pre ++ [c] ++ tail post where (pre, post) = splitAt n s compareSecret :: String -> String -> Int -> Char -> String compareSecret secret guess pos c = if secret !! pos == c then replaceChar pos guess c else guess isInt :: String -> Bool isInt = all isDigit isChar :: String -> Bool isChar  = False isChar (c:cs) = isAlpha c && null cs promptUntil :: (String -> Bool) -> String -> (String -> IO ()) -> IO (String) promptUntil pred prompt failure = do putStr (prompt ++ "\n> ") input <- getLine if pred input then return input else do failure input promptUntil pred prompt failure loop :: String -> String -> IO () loop secret guess = do putStrLn guess input1 <- promptUntil isInt "Enter position:" (\input -> putStrLn $ input ++ " is not a number.") input2 <- promptUntil isChar "Enter character:" (\input -> putStrLn $ input ++ " is not a character.") let pos = read input1 :: Int let c = head input2 let result = compareSecret secret guess pos c if result /= secret then loop secret result else return () main = do putStrLn "\n Welcome to Guess The Word!\n" difficultyString <- promptUntil isDifficulty "Enter difficulty level [e/m/h] or leave empty for easy difficulty: " (\input -> putStrLn $ input ++ " is not a valid difficulty.") let difficulty = fromMaybe (Easy) (createDifficulty difficultyString) putStrLn $ show difficulty ++ " difficulty selected." secret <- generateSecret difficulty loop secret $ replicate (length secret) '-' putStrLn secret putStrLn "You win!"