As a beginner, I tried to implement TicTacToe in Haskell. How can I improve my code?

I have refactored my code: General TicTacToe in Haskell


module Data.List.Utils where 

import Data.List (intersperse)

surround :: a -> [a] -> [a]
surround x ys = x : intersperse x ys ++ [x]

nth :: Int -> (a -> a) -> [a] -> [a]
nth _ _ [] = []
nth 0 f (x:xs) = f x : xs
nth n f (x:xs) = x : nth (n - 1) f xs


module TicTacToe where

import Data.List (transpose)
import Data.Foldable (asum)
import Data.List.Utils (nth, surround)

data Tile = Empty | O | X deriving (Show, Eq)

showTile :: Tile -> String
showTile Empty = " "
showTile O     = "O"
showTile X     = "X"

type Board = [[ Tile ]]

showBoard :: Board -> String
showBoard = unlines
          . surround vert
          . map (concat . surround horiz . map showTile)

    where vert  = "+-+-+-+"
          horiz = "|"

whoWon :: Board -> Maybe Tile
whoWon xs = asum . map winner $ diag : anti : rows ++ cols

    where rows = xs
          cols = transpose xs
          diag = zipWith (!!) xs [0..]
          anti = zipWith (!!) xs [2,1..]

          winner [O, O, O] = Just O
          winner [X, X, X] = Just X
          winner _         = Nothing 

fillTile :: Int -> Int -> Tile -> Board -> Board
fillTile row col = nth row . nth col . const

isOver :: Board -> Bool
isOver = all (all (/= Empty))

overMsg :: Maybe Tile -> String
overMsg (Just x) = "Winner: " ++ (showTile x)
overMsg Nothing  = "Game Over! No winner."

inRange :: Int -> Bool
inRange x = x >= 0 && x <= 2

gameLoop :: Tile -> Board -> IO ()
gameLoop player board
    | isOver board || winner /= Nothing = putStrLn $ overMsg winner
    | otherwise    = do

        putStrLn $ showBoard board
        putStrLn $ "Now player " ++ showTile player

        putStr "Row: "
        row <- fmap read getLine

        putStr "Col: "
        col <- fmap read getLine

        if inRange row && inRange col && (board !! row !! col == Empty)

            then gameLoop (if player == O then X else O) $
                     fillTile row col player board

            else do
                putStrLn "Invalid position, please try again."
                gameLoop player board

    where winner = whoWon board

startBoard :: Board
startBoard = replicate 3 (replicate 3 Empty)

main = gameLoop O startBoard

1 Answer 1


Some thoughts about your Tic-Tac-Toe version.

You made a confusion between Tile and Player. You assume a Player is the same as a Tile but it is not. A Tile can be Empty but a Player cannot be empty. You should separate these two notions. It will make your declarations clearer on what you expect. What if I call "gameLoop Empty startBoard" ?

The putStr function does not flush immediately its output in a compiled program. Your program asks the user for a row and a column and displays these labels after the user inputs.

Is it natural to ask for the row before the column ?

You have intertwined the game logic with the user interface in the gameLoop function. What will you do if you want to implement a computer player ? Or have a GUI to play the game ? You will have to patch your gameLoop function though the game logic has not changed.

Your solution is locked to the fact that your board is a 3×3 grid with a 3 tiles win. For example, your inRange functions works for row indices AND column indices, which are not the same. You use the constants 2 and 3 in your code (inRange, startBoard and whoWon), but nothing implies a relation between them. The 3 rows and 3 columns are characteristics of the Board, not of your functions.

Messages could be more specific. When you tell the user "Invalid position", there’s no way to know if it’s because the coordinates are out of bounds or because the tile has already been played.

Note: there are many Tic-Tac-Toe game related questions on CodeReview, you should take a look at:


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.