I wrote this program to model interactions between a user and an artificial player, both playing by the same rules (not enforced here for simplicity).The game played is here is your next word has to start with the last letter of minemodule Main whereimport Libimport System.Randomimport System.Exitimport Control.Monadvocab     = [alpha,beta,gamma]blacklist = []pick:: [a] -> IO a --picks random element. copy pasted, not understoodpick x = Control.Monad.liftM (x !!) (randomRIO (0, length x - 1))main :: IO ()main = do  userInput <- getLine  processUser userInput vocab blacklistprocessUser :: String -> [String] -> [String] -> IO aprocessUser input vocab blacklist = if input == quit then exitSuccess                                    else do                                          successor <- getNext input vocab (input:blacklist)                                          processPC successor vocab blacklistprocessPC :: Maybe (IO String) -> [String] -> [String] -> IO aprocessPC  Nothing      v b = do putStrLn I give up                                  exitSuccessprocessPC (Just ioWord) v b = do word <- ioWord                                 putStrLn word                                 userInput <- getLine                                 processUser userInput v (word:b)getNext:: String -> [String] -> [String] -> IO (Maybe (IO String))getNext lastWord vocab blacklist  = do let chooseFrom = filter (`notElem` blacklist) vocab                                       let matches    = filter (\x -> head x == last lastWord) chooseFrom                                       if null matches then return Nothing                                       else return (Just (pick matches) )I am especially interested how to formulate this better structurally-
Textbased User Interface for user and program taking turns
haskell;user interface
The main issue with this code is that basically everything lives in IO. One of the advantages of Haskell over other programming languages is the ability to cleanly separate effects, and you should strive to implement as much of your code as possible in a pure context, then only actually use IO at the top level of your program.Breaking apart getNextThe most egregious example of this is getNext, which has a pretty convoluted return value, IO (Maybe (IO String)). This is IO nested inside another IO type, which is pretty convoluted. Ideally, that function shouldnt need to be anything but completely pure, anyway, so it should really return something like Maybe String.Taking a step back for a moment, though, what does getNext even do? The name getNext is pretty vague. More than that, though, it has a lot of responsibilities. Lets list them:It accepts the last word the player typed in, then finds what letter it ended with.It filters blacklisted words out of a larger wordlist.It finds the words that start with the letter found in step 1.Finally, it picks a random word from that list.This is a lot of different responsibilities for just one function! Haskell works especially well when you define small, atomic functions that only do one thing at a time, then compose them together. Distilling the core functionality, this function should really probably just find all the words in a list that start with a particular character, then return all of them. You can implement this in a single line using isPrefixOf from Data.List:import Data.List (isPrefixOf)startingWith :: Char -> [String] -> [String]startingWith c = filter (isPrefixOf [c])(As an aside, this actually fixes a problem with your original code, which used head. The head function is partial; that is, it will crash if given an empty list. The isPrefixOf function will handle empty lists properly.)IO and strong typingNow, what about handling the other responsibilities? Well, picking a random element is probably the trickiest thing to do because it is sort of side-effectful. You could manually thread a random number generator state around, but that would be a bit cumbersome. One good way to handle this is the MonadRandom typeclass from the package of the same name, which allows writing a pickRandom function without IO:import Control.Monad.Random (MonadRandom(..))pickRandom :: MonadRandom m => [a] -> m (Maybe a)pickRandom [] = return NothingpickRandom xs = Just . (xs !!) <$> getRandomR (0, length xs - 1)This has two improvements over the pick function you found:It properly handles the case of empty lists by returning Maybe a. The version of pick you found would simply crash.It does not depend on IO, just on MonadRandom, which is significantly less powerful.Haskell has lots of ways to be very precise about the type of things. Rather than making everything nullable, it has Maybe. Rather than just passing strings around, Haskell favors using ADTs. Haskell also provides a way from distinguishing between pure and impure operations using the IO type, but in many ways, IO provides some very weak guarantees.When a function returns IO, it can effectively do anything. It can spawn threads, it can interact with the filesystem, and it can even send data over the network. Ideally, it would be nicer to have more fine-grained typing, just like we have with Haskells domain-specific, fine-grained ADTs.To accomplish this, its often possible to use typeclasses like MonadRandom, which encode a very specific set of capabilities. Functions that include a MonadRandom constraint can do one thing: generate random numbers. Now, because MonadRandom is a typeclass, not a datatype, it does not specify how those numbers are generated; it is up to the caller to decide that. The MonadRandom typeclass actually has an instance for IO, which allows generating numbers using the system random number generator, but it also has an instance for the Rand type, which is a purely functional random number generation monad.We havent decided how were going to generate random numbers yet. We might even use IO, eventually. However, thats not the point the main point is that weve now written a function that can do nothing more than its type claims it can, and thats a good thing.Handling turnsNow that we have some extremely basic primitives, we can put them together to create handlers for user and computer turns. Each turn can result in one of two actions: giving up, or submitting a word. Therefore, lets encode that into a datatype, then implement some extremely simple functions for each kind of turn:data Turn = Word String | GiveUpuserTurn :: String -> TurnuserTurn quit = GiveUpuserTurn input  = Word inputcomputerTurn :: MonadRandom m => String -> [String] -> m TurncomputerTurn lastWord wordList = maybeToTurn <$> maybeRandomWord  where lastChar = last lastWord        maybeRandomWord = pickRandom (startingWith lastChar wordList)        maybeToTurn = maybe GiveUp WordThe computerTurn function is a little more complex, but its not too bad. It uses the maybe function, which is a convenient helper for transforming a Maybe value into a value of another type (in this case, Turn), and it uses <$>, which is just an infix alias for fmap.One nice thing about these implementations is, again, we are able to learn a lot about these functions just by looking at their types. The userTurn function is extremely pure, and the computerTurn function uses randomness, but nothing else.Writing the main loopNow that we have some primitives, we can write a top-level interpreter that will actually drive the game itself. This function will be a bit longer, since it will handle the actual imperative logic of the game, but it will also be extremely simple, since its basically just wiring things together.One thing that we have pretty much eliminated is the concept of a word blacklist. After all, why not just remove words from the word list itself rather than maintaining two separate lists and threading them around everywhere? We can completely eliminate the need for a blacklist by just pulling words out of the computers vocabulary.import Data.List (delete)runGame :: [String] -> IO [String]runGame wordList = do  userInput <- getLine  case userTurn userInput of    GiveUp -> exitSuccess    Word userWord -> do      let remainingWords = delete userWord wordList      computerResult <- computerTurn userWord remainingWords      case computerResult of        GiveUp -> putStrLn I give up >> exitSuccess        Word computerWord -> do          putStrLn computerWord          runGame (delete computerWord remainingWords)This function may look a little complicated, but its really not so badby just following the types, the function effectively writes itself. We call userTurn, then handle both potential Turn cases. Next, we call computerResult and handle both of its possible cases. Once thats done, we just loop, and were done! The result of runGame is just the number of words left in the computers vocabulary.Now, all thats left to do is invoke runGame from main:main :: IO ()main = void $ runGame vocabThe void function just ignores the result of runGame vocab, properly returning IO (), and it kicks off the main loop by passing in the initial vocab list.The final resultHeres the final code after all of my changes:module Main whereimport Data.List (delete, isPrefixOf)import Control.Monad (void)import Control.Monad.Random (MonadRandom(..))import System.Exit (exitSuccess)data Turn = Word String | GiveUpvocab :: [String]vocab = [alpha,beta,gamma]main :: IO ()main = void $ runGame vocabrunGame :: [String] -> IO [String]runGame wordList = do  userInput <- getLine  case userTurn userInput of    GiveUp -> exitSuccess    Word userWord -> do      let remainingWords = delete userWord wordList      computerResult <- computerTurn userWord remainingWords      case computerResult of        GiveUp -> putStrLn I give up >> exitSuccess        Word computerWord -> do          putStrLn computerWord          runGame (delete computerWord remainingWords)userTurn :: String -> TurnuserTurn quit = GiveUpuserTurn input  = Word inputcomputerTurn :: MonadRandom m => String -> [String] -> m TurncomputerTurn lastWord wordList = maybeToTurn <$> maybeRandomWord  where lastChar = last lastWord        maybeRandomWord = pickRandom (startingWith lastChar wordList)        maybeToTurn = maybe GiveUp WordpickRandom :: MonadRandom m => [a] -> m (Maybe a)pickRandom [] = return NothingpickRandom xs = Just . (xs !!) <$> getRandomR (0, length xs - 1)startingWith :: Char -> [String] -> [String]startingWith c = filter (isPrefixOf [c])Formatting changes aside, the main differences from your original code are strengthening the types and separating concerns as much as possible to isolate effects. I have also removed do from most of the function implementations, since do tends to force code into a pseudo-imperative style that eliminates a lot of the declarative benefits of Haskell.Some of the things in this answer might be a bit more advanced than youve been exposed to yet, and thats okay! In truth, there are probably even fancier ways to accomplish what you wanta free monad came to mind when writing this answer, for example. However, the point of this is not to be either so far above you that you cant understand it, nor to be precisely at your level. Its alright if not all of this makes sense to you just yet, but I hope that by reaching for some more complicated concepts, youll be encouraged, not discouraged, to challenge yourself some more.