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

Utils

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


TicTacToe

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: "

putStr "Col: "

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


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: