Another very attracting feature is its very rich toolbox of higher level programming constructs that allow one to express complex data processing tasks in just a few function calls.
A third beautiful aspect of the language is its typing system, which provides automated type matching of undeclared functions and variables. It provides a type checking system just like any other strongly typed language, but without the need to separately declare the types of each and every variable.
On the other hand, these features make Haskell quite difficult to read for people who are not familiar with it, but on the other hand, the amount of code to read can be very small, and absolutely oozing with semantics, i.e. there code is typically very dense, and the reader is not bothered with unnecessary details, such as the name of an iteration variable.
The way in which stateful programming is expressed in Haskell, which is a pure language in the sense that absolutely no side effects are allowed in the code, can be a bit confusing for a beginner, but after one gets the hang of it, one starts to really appreciate the pure functional programming paradigm.
With no side effects and a strong typing system, any program that compiles actually does something, there is no concept of a run-time failure, except in the case of a match against a partially defined pattern, or similar situations.
Additionally, lazy evaluation allows one to express interactive processes simply as ordinary pure functions, which is really convenient, though may be quite confusing.
Below is an example of a hangman program, written in Haskell, using lazy evaluation and the
The execution of the game looks like this:State
monad. The program lets the user guess a word one letter at a time. If the user makes 5 wrong guesses, he loses the game.
-- A hangman game using the State monad in Haskell by Reino Ruusu, 2017
import System.Environment
import System.Random
import Control.Monad.State.Lazy
import Data.Char
-- Hangman IO
main = do
args <- getArgs
if null args then
putStrLn "Please provide a file containing words. (One per line.)"
else do
-- Select random word
words <- readFile (head args)
word <- randomElement (lines words)
-- Play the game
interact $ hangmanMain (map toLower word)
-- Hangman game as a pure lazy string processing function
hangmanMain :: String -> String -> String
hangmanMain word = unlines . ("Welcome to Haskell Hangman":) . hangman . ("":) . lines
where
hangman input = map snd $ takeUntil fst $ evalState (sequence steps) initialState
where
steps = map (hangmanIteration word) input
initialState = (initialGuess, 0)
where
-- Alphabetic characters replaced with underscores
initialGuess = map (\w -> if isAlpha w then '_' else w) word
-- A single iteration of hangman: State update followed by output
hangmanIteration :: String -> String -> State (String, Int) (Bool, String)
hangmanIteration word input = update >> report
where
update = unless (null input) $ do
-- Check user's guess and update state
(guess, strikes) <- get
if elem l word && not (elem l guess) then
put (newguess guess, strikes) -- Hit
else
put (guess, strikes + 1) -- Miss
where
l = head input
newguess = zipWith (\w g -> if w == l then w else g) word
report = do
(guess, strikes) <- get
-- Status message - example: __a_e__ ###
let status = guess ++ " " ++ (replicate strikes '#')
-- Check game outcome
return $ if guess == word then
(True, status ++ "\nYou won!") -- Win
else if strikes >= 5 then
(True, status ++ "\nYou lost! (" ++ word ++ ")") -- Loss
else
(False, status) -- Continue
-- Utility functions
-- Take elements up to and including the first for which f returs True
takeUntil f l = first ++ [head last]
where (first, last) = break f l
-- Select a random element from a list (as an IO operation)
randomElement list = do
i <- randomRIO (0, length list - 1)
return (list !! i)
Note how the game itself is defined as a pure function that simply processes the input string into the output string. Furthermore, all iteration in the code happens via higher level programming constructs that clearly define rules for processing of data, instead of bothering with low-level things such as updating an iteration variable. All the IO is performed by calling this function via theWelcome to Haskell Hangman ____________ a ____________ # e ____________ ## i _i________i_ ## o _i__o_____i_ ## u _i__ou____i_ ## s _is_ou__s_i_ ## c _iscou__s_i_ ## v viscou__s_i_ ## t viscou_ts_i_ ## y viscou_ts_i_ ### n viscounts_i_ ### p viscounts_ip ### l viscounts_ip #### h viscountship #### You won!
interact
function, which simply maps input and output from the console to a pure function from string to string. The business logic can then be defined purely by describing the relationship between the input and the output, without bothering with any aspects of the IO operations.The steps of the game are here described using the State monad, which allows one to define processing as a combination of stateful operations.
An almost equally elegant solution can be achieved by the very versatile mapAccumL operation, which provides for simultaneous accumulation of state and processing of data. However, in this approach, the
hangmanIteration
function is much less understandable in isolation, whereas in the version based on the State monad, the state update is directly specified by the definition of the iteration function itself. The state monad allows us to also make use of elegant and semantically rich higher level constructs, such as unless
above.
-- A hangman game in Haskell by Reino Ruusu, 2017
import System.Environment
import System.Random
import Data.List
import Data.Char
-- Hangman IO
main = do
args <- getArgs
if null args then
putStrLn "Please provide a file containing words. (One per line.)"
else do
-- Select random word
words <- readFile (head args)
word <- randomElement (lines words)
-- Play the game
interact $ hangmanMain (map toLower word)
-- Hangman game as a pure lazy string processing function
hangmanMain :: String -> String -> String
hangmanMain word = unlines . ("Welcome to Haskell Hangman":) . hangman . ("":) . lines
where
hangman input = map snd $ takeUntil fst output
where
output = snd $ mapAccumL (hangmanIteration word) initialState input
initialState = (initialGuess, 0)
where
-- Alphabetic characters replaced with underscores
initialGuess = map (\w -> if isAlpha w then '_' else w) word
-- A single iteration of hangman: State update followed by output
hangmanIteration :: String -> (String, Int) -> String -> ((String, Int), (Bool, String))
hangmanIteration word state input = (newState, report newState)
where
-- Check user's guess and update state
newState = if null input then
state
else if elem l word && not (elem l guess) then
(newguess guess, strikes) -- Hit
else
(guess, strikes + 1) -- Miss
where
l = head input
(guess, strikes) = state
newguess = zipWith (\w g -> if w == l then w else g) word
report (guess, strikes) = let
-- Status message - example: __a_e__ ###
status = guess ++ " " ++ (replicate strikes '#')
-- Check game outcome
in if guess == word then
(True, status ++ "\nYou won!") -- Win
else if strikes >= 5 then
(True, status ++ "\nYou lost! (" ++ word ++ ")") -- Loss
else
(False, status) -- Continue
-- Utility functions
-- Take elements up to and including the first for which f returs True
takeUntil f l = first ++ [head last]
where (first, last) = break f l
-- Select a random element from a list (as an IO operation)
randomElement list = do
i <- randomRIO (0, length list - 1)
return (list !! i)