9
\$\begingroup\$

Are there any ways which I could improve and simplify this code? Is my code idiomatic Haskell?

Is the use of a lens library to change an element of a two-dimensional array unnecessary? If so, how could I replace it with a more elegant and simpler solution?

Do I need to make use of more types in order to make my code more understandable?

As a Haskell beginner, I am still trying to get used to the functional programming way and would appreciate it if answerers would provide a detailed and easy-to-understand answer (as well as references to materials which I could look up to learn more).

-- Tic Tac Toe implementation in Haskell.

module TicTacToe (
    newGame,
    isWin,
    move
) where

import Control.Lens
import Data.List
import Data.Maybe

-- Size of the grid
n :: Int
n = 3

data Player = X | O deriving (Show, Read, Eq)
type Marking = Maybe Player
type Position = (Int, Int)

type Grid = [[Marking]]

-- An empty grid of size n x n.
emptyGrid :: Grid
emptyGrid = replicate n $ replicate n Nothing

data Game = Game {
    board :: Grid,
    curTurn :: Player
} deriving Show

-- Initial game which board is of size n x n,
-- starting with O as the first player.
newGame :: Game
newGame = Game {
    board = emptyGrid,
    curTurn = X
}

getWinSeqs :: Grid -> [[Marking]]
getWinSeqs grid = horizontal ++ vertical ++ [fDiag, bDiag]
  where horizontal = grid
        vertical = transpose grid
        fDiag = zipWith (\ i x -> i !! (n - x - 1)) grid [0..]
        bDiag = zipWith (!!) grid [0..]

-- Check if a game has been won on a board.
isWin :: Game -> Maybe Player
isWin (Game grid _)
    | isWin' X  = Just X
    | isWin' O  = Just O
    | otherwise = Nothing
    where
        isWin' :: Player -> Bool
        isWin' player = any (all (== Just player)) $ getWinSeqs grid

-- Make the next move.
move :: Game -> Position -> Game
move (Game grid player) (i, j) = Game {
    board = nextGrid,
    curTurn = nextPlayer
} where
    nextGrid =
        if isNothing $ grid !! i !! j
        then set (ix i . ix j) (Just player) grid
        else error "position is occupied"
    nextPlayer
        | player == X = O
        | otherwise   = X
\$\endgroup\$
1

1 Answer 1

5
+50
\$\begingroup\$

A haskell beginner using Control.Lens... Certainly better than my first program which happened to a tic tac toe solver, so I can compare. We all know Why functional programming matters, don't we?

Make your imports more specific:

import Control.Lens (set,ix)
import Data.List (transpose)
import Data.Maybe (catMaybes)

You will note I replaced your isNothing by pattern matching. You only use type Position = (Int, Int) once. I would not declare this one.

instead of

    fDiag = zipWith (\ i x -> i !! (n - x - 1)) grid [0..]

I recommend

    fDiag = zipWith (!!) (reverse grid) [0..] -- or
    fDiag = zipWith (!!) grid [n-1,n-2..]

Why force isWin to take Game? Grid would suffice. I suggest you to write a program that uses this module, then you know better.

OK, now for

move :: Game -> Position -> Game

I'd write

move :: Int -> Int -> Game -> Maybe Game

Instead of throwing an error, return Nothing. Then you can compute all possible moves with

moves :: Game -> [Game]
moves game = catMaybes [move x y game | x<-[0..2], y<-[0..2]]

and I put Game in the last position so you can compose functions like move 2 0 . move 1 1 $ newGame. Actually, that won't work because move now encapsulates in Maybe. But now you can write:

  test = Just newGame >>=
              move 1 1 >>= -- first move in center
              move 2 1 >>=
              move 2 0 >>=
              move 0 2 >>= -- forced
              move 0 0     -- setting up the trap

because Maybe is a monad, and >>= allows forward chaining instead of .! The modified move looks like this:

move :: Int -> Int -> Game -> Maybe Game
move i j (Game grid player) = case grid !! i !! j of
   Just _ -> Nothing
   Nothing -> Just $ Game {
     board = set (ix i . ix j) (Just player) grid,
     curTurn = nextPlayer player
  }

nextPlayer X = O
nextPlayer O = X

I also made nextPlayer a top level function, I'd export it, it looks useful.

\$\endgroup\$
2
  • \$\begingroup\$ Tbf the code using Control.Lens was from a person in #haskell-beginners. Thanks for your review; I'll wait for other answers to flow in before awarding the bounty. \$\endgroup\$
    – wei2912
    Commented Nov 6, 2014 at 8:08
  • 1
    \$\begingroup\$ I finally got the chance to work on my implementation and found your answer to be of great help. I'll award the bounty now. Thanks! \$\endgroup\$
    – wei2912
    Commented Nov 8, 2014 at 14:33

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.