8th lecture of CIS194 finally explained how to interact with outside world. Up to this point I have either written stand alone functions or integrated them with pre-existing solution that did the interaction with files.
Hello world written in Haskell isn’t particularly complex looking:
main :: IO () main = putStrLn "Hello World"
But as soon as you start writing code that mixes pure (regular Haskell functions) with impure (code that reads and writes files, prints on screen or reads input from keyboard), things start looking a bit different. It makes things easier, if one limits IO to as small portion of code as possible. There’s less chances for pure code to fail than impure, since pure code depends only on the inputs given to it, while impure code depends on the whole world (I exaggerate of course, but still).
The gist is, that any function that does any input or output has IO in its return type. If function returns a value in addition of IO, it’s return type is IO something. If it doesn’t, return type is just IO ():
printOnScreen :: IO () printOnScreen = putStrLn "Hello there" calculateStuff :: Integer -> Integer -> IO Integer calculateStuff a b = do let answer = a + b putStrLn "the answer is" ++ show answer return answer main :: IO () main = do printOnScreen calculateStuff 2 3 putStrLn "Bye Bye"
Working with IO introduces a new language structure: do notation. This is used to chain together several steps that work with IO (or monads in general, but those haven’t been talked about yet). In a sense, it makes code look quite a bit imperative: do this, then do that and finally do this. It’s just a convenient notation though, Haskell compiler will transform it to a long chain of lambdas that you could write by yourself too (but I find do notation more pleasant to work with).
It’s also worth mentioning that return in the calculateStuff function doesn’t work like return in pretty much every other programming language. Here it’s used to wrap answer into IO, which makes it IO Integer, instead of Integer.
Another thing worth noting is that merely executing a function with IO doesn’t actually execute the IO. It just constructs IO action that when handed to the main function (or the interactive shell) will perform the IO. Because Haskell is lazy language and programmer doesn’t have full control on when functions are evaluated, every IO action has to depend on the previous IO action, in order to have them execute in correct order. If any function could print on screen when-ever they wished, we would get pretty interesting results.
I did a little number guessing game to practice IO a bit more. I tried to separate IO parts from the logic of the game, but there aren’t that much to separate in such a game. The core part of the program is two functions:
guessNumber :: Integer -> IO () guessNumber secret = do putStrLn "Your guess?" answer <- getLine let (reply, cont) = parseGuess answer secret putStrLn reply if not cont then guessNumber secret else return () parseGuess :: String -> Integer -> (String, Bool) parseGuess s n = case readMaybe s of Just number | number == n -> ("You guessed right", True) | number < n -> ("Too low, try again", False) | otherwise -> ("Too high, try again", False) Nothing-> ("I didn't quite catch that, try again", False)
guessNumber is given a secret number that the player has to try and guess. It’ll ask for input and pass that for parseGuess for processing. parseGuess will then parse the string, and return what should be printed on the screen and if the player guessed correctly. This will loop until player finally guesses the secret number.