HangPerson in Haskell
While your code might be working and written in a functional language, it also feels very procedural to me. Most idiomatic Haskell code I see has a very high signal to noise ratio. In your code I have to read every single line and figure out what it does in my head.

You can achieve this by making a lot of tiny functions (after all, composition is key for a functional program), and use as much of the domain language as possible. These tiny functions don't need to stand on their own; using let x=... in ... or where x= tends to help a lot. 

Also, try to figure out what state you need to keep, and name it. I see a lot of functional programmers design their apps types-first. Having an explicit model that doesn't allow faulty state tends to help big-time.

Make sure you don't add state you can calculate from other state, use functions for that, otherwise your data structure might get inconsistent.

AFAIK tuples should mostly be used inside functions, but never exposed, except maybe for the most trivial cases like key-value pairs, and even that is debatable... After all, how much work is it to define data KeyValuePair a b= KeyValuePair a b

I am by no means an expert in Haskell, and learned these rules from doing mostly object-oriented programming. However, in a functional language these rules tend to be way easier to implement.

About operator shortcuts etc: I don't use them a lot - yet -, unless the Haskell linter suggests me to use it.

I do think that using a tool like ghc-mod and hlint is a necessity for Haskell development.

TL;DR: use proper types, extract parts into smaller functions, and try to properly name them. 

import           Control.Monad (when)
import           Data.Char     (toLower)
import           Data.List     (transpose)
import           System.Random (randomIO)

wordsPath :: FilePath
wordsPath = "words.txt"-- "/usr/share/dict/words"

data GameState = GameState
    { _wordToGuess :: String
    , guesses     :: String
    }

data GameStatus = Guessing | GameWon | GameLost deriving Eq

hangmanImages :: [[String]]
hangmanImages =
    transpose
    [ [ "   ", " O ", " O ", " O ", " O " , "_O " , "_O_"  ]
    , [ "   ", "   ", " | ", " | ", " | " , " | " , " | "  ]
    , [ "   ", "   ", "   ", "/  ", "/ \\", "/ \\", "/ \\" ]
    ]

fullHangmanImage :: Int -&gt; [String]
fullHangmanImage index =
    "=========" :
    "|    |" :
    map ("|   " ++) img
    where img = hangmanImages !! index


maxWrongGuesses :: Int
maxWrongGuesses = length hangmanImages - 1

numberOfWrongGuesses :: GameState -&gt; Int
numberOfWrongGuesses (GameState word' guesses') =
    length $ filter charNotInWord guesses'
    where charNotInWord c = c `notElem` word'

gameStatus :: GameState -&gt; GameStatus
gameStatus (GameState word' guesses')
    | isGuessed     = GameWon
    | isLastGuess'  = GameLost
    | otherwise     = Guessing
    where
        isGuessed = all isCharInGuesses word'
        isCharInGuesses x = x `elem` guesses'
        gameState = GameState word' guesses'
        isLastGuess' = numberOfWrongGuesses gameState == maxWrongGuesses

-- for one reason or another getChar also appends &lt;CR&gt;
-- so I implemented my own getChar and made sure empty input is refused
getAChar :: IO Char
getAChar = do
    line &lt;- getLine
    case line of
        [] -&gt; getAChar
        (c:_) -&gt; return c

getANewChar :: GameState -&gt; IO Char
getANewChar gameState = do
    putStrLn "Next char to guess"
    c &lt;- getAChar
    if c `elem` guesses gameState
    then do
        putStrLn "Character already used in guesses."
        getANewChar gameState
    else
        return c

displayState :: GameState -&gt; IO ()
displayState (GameState word' guesses')  =
    putStrLn $ unlines $ case gameStatus gameState of
        Guessing -&gt; fullHangmanImage' ++
            [ "Word to guess: " ++ wordWithGuesses
            , ""
            , "Guesses: " ++ guesses'
            ]
        GameWon -&gt; fullHangmanImage' ++
            [ "CONGRATULATIONS!"
            , "You correctly guessed the word " ++ word'
            , " in " ++ show (length guesses') ++ " tries "
            ]
        GameLost -&gt; fullHangmanImage' ++
            [ "YOU FAILED!"
            , "You failed to guess the word " ++ word'
            ]
    where
        gameState = GameState word' guesses'
        fullHangmanImage' = fullHangmanImage currentHangmanIndex
        currentHangmanIndex = numberOfWrongGuesses gameState
        wordWithGuesses = blankOrChar &lt;$&gt; word'
        blankOrChar c
            | c `elem` guesses' = c
            | otherwise = '_'

gameLoop :: GameState -&gt; IO ()
gameLoop gameState = do
    displayState gameState
    when (gameStatus gameState == Guessing) $ do
        c &lt;- getANewChar gameState
        gameLoop $ gameState { guesses = guesses gameState ++ [c] }

newGame :: IO GameState
newGame = do
    contents &lt;- readFile wordsPath
    let words' = filter validWord $ lines contents
    let wordcount = length words'
    randomNumber &lt;- randomIO
    let randomWord = words' !! (randomNumber `mod` wordcount)
    return $ GameState randomWord []
    where
        validWord word =
            '\'' `notElem` word &amp;&amp;
            map toLower word == word

main :: IO ()
main = do
    newGame &gt;&gt;= gameLoop
    putStrLn "Play again? (y/n):"
    option &lt;- getAChar
    when (option == 'y') main