4
\$\begingroup\$

I have written a game engine for the game Reversi, also called Othello. The game engine works not exactly like the official rules. For example it is possible to place "holes" on the board, places which can never hold a stone.

The game engine works fine, I have only some problems with the command line interface which includes lots of IO. Another problem I couldn't solve is exception handling. I read, that in Haskell, exceptions are rarely used. Nevertheless I was unable to refactor them to pure code. I have placed comments to the functions where the exceptions occur.

So, the main points which need review are:

  1. Exceptions
  2. How to handle errors in the program? Exceptions should not be used and type signatures like x :: XXX -> Either ErrorResult SuccessResult look ugly.
  3. How to work with qualified imports? Are there style rules how to qualify them?
  4. It is inefficient to compute Game.possibleMoves more than one time per step. How to cache it during the steps? Should I save them to the Game data type?
  5. Is it possible to improve signatures like x :: Game -> XXX -> IO Game? Because there is no namespace for a data type, the data have have to be delivered explicitly. Thus, is there some syntactic sugar to do this?
  6. Code smells

A sample game:

> newGame 8 8
> move F5
> move D6
> move C5
> print
--------
--------
--------
---WB---
--BBBB--
---W----
--------
--------
turn: White
> possibleMoves
Possible moves: B4,B6,F4,F6
> abort
Game over! Black has won (5:2)!
> quit

The code is not yet documented and it contains about 450 lines of code. I hope, this means not to much work for a code review. ;)

Main.hs

import qualified Data.Map as Map
import qualified Data.List as List
import qualified Data.List.Split as Split
import qualified Data.Text as T
import qualified Data.Array as A
import qualified Data.Maybe as Maybe
import qualified Text.Printf as Print
import qualified Control.Exception as E
import Control.Exception.Base
import System.IO
import Position
import Board
import Cell
import Game
import InputParser

main :: IO ()
main = loop emptyGame >> return ()

loop :: Game -> IO Game
loop game =
  putStr "> " >>
  hFlush stdout >>
  getLine >>=
  handleCommand game . splitCommand . trim

handleCommand :: Game -> (String, String) -> IO Game
handleCommand game (command, args) =
  case command of
    "quit" -> return game
    "newGame" -> createNewGame game args >>= loop
    "hole" -> createHole game args >>= loop
    "move" -> move game args >>= loop
    "abort" -> abort game >>= loop
    "print" -> printCommand game >>= loop
    "possibleMoves" -> showPossibleMoves game >>= loop
    _ -> putStrLn "command not found" >> loop game

createNewGame :: Game -> String -> IO Game
createNewGame game args
  | mode game /= GameOverMode = printError "there is already an active game" >> return game
  | otherwise = if canMove newGame then return newGame else calculatePass newGame
  where
    newGame = parseAs args gameParser $ \f -> case f of
      (width, height, Nothing) -> createGame width height
      (width, height, Just positions) -> createGameWithConfig width height positions

-- how to change error?
createHole :: Game -> String -> IO Game
createHole game args
  | mode game /= NewMode = printError "can't add hole area. there is no game yet or the game is already started'" >> return game
  | otherwise = if canMove newGame then return newGame else calculatePass newGame
  where
    newGame = parseAs args holeParser $ \f -> case f of
      (from, to) -> addHole' game from to
    addHole' game from to
      | containsCells (board game) from to = error "can't add hole. it is not empty'"
      | otherwise = addHole game from to

-- how to change error?
move :: Game -> String -> IO Game
move game args = do
  bool <- requireGameStarted game
  if bool then return game
  else if canMove newGame then return newGame
  else calculatePass newGame
  where
    newGame = parseAs args position $ \f -> case f of
      pos -> game `moveTo'` pos
    moveTo' game pos
      | notElem pos $ possibleMoves game = error "move not possible"
      | otherwise = game `moveTo` pos

printCommand :: Game -> IO Game
printCommand game = do
  bool <- requireGameStarted game
  if bool then return game
  else
    putStr (mkString game) >>
    putStrLn ("turn: " ++ show (curPlayer game)) >>
    return game
  where
    mkString = unlines . rows . table . board
    --table :: Board -> A.Array (Int, Int) Char
    table (Board width height cells) =
      A.array ((1, 1), (height, width))
      [((y, x), sign $ Map.findWithDefault EmptyCell (Position x y) cells) |
      y <- [1 .. height],
      x <- [1 .. width]]
    --rows :: A.Array (Int, Int) a -> [[a]]
    rows arr = [[arr A.! (x, y) |
      y <- [cLow .. cHigh]] |
      x <- [rLow .. rHigh]]
      where
        ((rLow, cLow), (rHigh, cHigh)) = A.bounds arr

abort :: Game -> IO Game
abort game = do
  bool <- requireGameStarted game
  if bool then return game else calculateWinner (endGame game)

showPossibleMoves :: Game -> IO Game
showPossibleMoves game = do
  bool <- requireGameStarted game
  if bool then return game else putStrLn ("Possible moves: " ++ moves) >> return game
  where
    moves = List.intercalate "," (map show $ List.sort $ possibleMoves game)

calculatePass :: Game -> IO Game
calculatePass game =
  if canMove passed then newGame else calculateWinner game
  where
    passed = passMove game
    newGame = putStrLn (show (curPlayer game) ++ " passes") >> return passed

calculateWinner :: Game -> IO Game
calculateWinner game =
  putStrLn str >> return newGame
  where
    newGame = endGame game
    (white, black) = List.partition (== White) $ filter (/= Hole) $ Map.elems $ cells $ board newGame
    (numOfWhite, numOfBlack) = (length white, length black)
    winner = if numOfWhite > numOfBlack then White else Black
    max' = max numOfWhite numOfBlack
    min' = min numOfWhite numOfBlack
    str = if numOfWhite == numOfBlack
      then "Game has ended in a draw."
      else Print.printf "Game over! %s has won (%d:%d)!" (show winner) max' min'

requireGameStarted :: Game -> IO Bool
requireGameStarted (Game mode _ _)
  | mode /= NewMode && mode /= ActiveMode = printError "game not started" >> return True
  | otherwise = return False

trim :: String -> String
trim = T.unpack . T.strip . T.pack

splitCommand :: String -> (String, String)
splitCommand str =
  splitAt index str
  where
    index = Maybe.fromMaybe (length str) $ List.elemIndex ' ' str

printError :: String -> IO ()
printError str = putStrLn ("Error!" ++ str)

InputParser.hs

module InputParser (parseAs, gameParser, holeParser, position) where

import Text.ParserCombinators.Parsec
import Control.Applicative
import Data.Char
import Game
import Position

parseAs :: String -> Parser a -> (a -> Game) -> Game
parseAs input p f =
  case parse safeP "" input of
    Left err -> error $ show err
    Right x -> f x
  where
    safeP = spaces *> p <* spaces <* eof

number :: Parser Int
number = read <$> many1 digit <?> "number"

config :: Parser String
config = many1 $ oneOf "WB#-,"

position :: Parser Position
position = (\x y -> Position (ord x - ord 'A' + 1) y)
  <$> upper
  <*> number

gameParser :: Parser (Int, Int, Maybe String)
gameParser = (,,)
  <$> number <* spaces
  <*> number <* spaces
  <*> optionMaybe config

holeParser :: Parser (Position, Position)
holeParser = (,)
  <$> position <* char ':'
  <*> position

Game.hs

module Game (
  Game(..), GameMode(..),
  emptyGame, createGame, createGameWithConfig,
  canMove, passMove, endGame, addHole, possibleMoves, moveTo) where

import qualified Data.List.Split as Split
import qualified Text.Printf as Print
import qualified Data.List as List
import qualified Data.Map as Map
import Board
import Position
import Cell

data GameMode = NewMode | ActiveMode | GameOverMode deriving (Show, Eq)

data Game = Game {
  mode :: GameMode,
  board :: Board,
  curPlayer :: Cell
}

directions :: [Int -> Int -> Position]
directions = [\x y -> Position (x+1) y, \x y -> Position (x+1) (y+1), \x y -> Position x (y+1),
  \x y -> Position (x-1) (y+1), \x y -> Position (x-1) y, \x y -> Position (x-1) (y-1),
  \x y -> Position x (y-1), \x y -> Position (x+1) (y-1)]

startPositions :: [((Int, Int), Cell)]
startPositions = [((0, 0), White), ((1, 0), Black), ((0, 1), Black), ((1, 1), White)]

emptyGame :: Game
emptyGame = Game GameOverMode (Board 0 0 Map.empty) Black

createGame :: Int -> Int -> Game
createGame w h = createGameWithConfig w h ""

-- how to change error?
createGameWithConfig :: Int -> Int -> String -> Game
createGameWithConfig width height str
  | not $ isValidSize width minWidth maxWidth = error "invalid width"
  | not $ isValidSize height minHeight maxHeight = error "invalid height"
  | otherwise = Game NewMode (Board width height $ Map.fromList cells) Black
  where
    cells = if null str then middleCells width height else calculateCells width height str

middleCells :: Int -> Int -> [(Position, Cell)]
middleCells width height =
  map (\((x, y), cell) -> (Position (x+xMiddle) (y+yMiddle), cell)) startPositions
  where
    (xMiddle, yMiddle) = (width `div` 2, height `div` 2)

-- how to change error?
calculateCells :: Int -> Int -> String -> [(Position, Cell)]
calculateCells width height str
  | not $ isValidData str width height = error "invalid data"
  | otherwise = [
    (Position x y, asCell c) |
    y <- [1 .. height],
    x <- [1 .. width],
    let c = rows !! (y-1) !! (x-1),
    isCell c]
  where
    rows = Split.splitOn "," str

isValidData :: String -> Int -> Int -> Bool
isValidData str width height =
  length rows == height && isRowValid `all` rows
  where
    rows = Split.splitOn "," str
    isRowValid row = length row == width && (\c -> isCell c || isEmpty c) `all` row

isValidSize :: Int -> Int -> Int -> Bool
isValidSize int from to =
  int >= from && int <= to && int `mod` 2 == 0

possibleMoves :: Game -> [Position]
possibleMoves (Game _ board cur) =
  List.nub $ possiblePositions =<< cellsToProof
  where
    cellsToProof = Map.keys $ Map.filter (cur ==) $ cells board
    possiblePositions pos = id =<< map (checkDirection pos) directions
    checkDirection pos f
      | isCurPlayer = []
      | otherwise = loop $ f (x newPos) (y newPos)
      where
        newPos = f (x pos) (y pos)
        isCurPlayer = not (isOfPlayer board newPos $ nextPlayer cur)
        isInvalid pos = not (board `isInRange` pos) || isOfPlayer board pos cur || board `isHole` pos
        loop pos
          | isInvalid pos = []
          | board `isFree` pos = [pos]
          | otherwise = loop $ f (x pos) (y pos)

-- how to change error?
moveTo :: Game -> Position -> Game
moveTo game @ (Game _ board cur) pos
  | notElem pos $ possibleMoves game = error $ "it is impossible to move to position " ++ show pos
  | otherwise = Game ActiveMode newBoard $ nextPlayer cur
  where
    positions = pos:(transform =<< directions)
    newBoard = transformBy board positions cur
    transform f = loop [] $ f (x pos) (y pos)
      where
        loop xs pos
          | isOfPlayer board pos $ nextPlayer cur = loop (pos:xs) $ f (x pos) (y pos)
          | isOfPlayer board pos cur = xs
          | otherwise = []

-- how to change error?
addHole :: Game -> Position -> Position -> Game
addHole (Game mode board cur) from to
  | containsCells board from to = error $ Print.printf "can't add hole (%s:%s). It is not empty" (show from) (show to)
  | otherwise = Game mode newBoard cur
  where
    positions = [
      Position x y |
      y <- [y from .. y to],
      x <- [x from .. x to]]
    newBoard = transformBy board positions Hole

canMove :: Game -> Bool
canMove = not . null . possibleMoves

nextPlayer :: Cell -> Cell
nextPlayer cur
  | cur == White = Black
  | otherwise = White

passMove :: Game -> Game
passMove (Game mode board cur) =
  Game mode board $ nextPlayer cur

endGame :: Game -> Game
endGame (Game _ board cur) =
  Game GameOverMode board cur

Board.hs

module Board (
  Board(..),
  maxWidth, maxHeight, minWidth, minHeight,
  isFree, isInRange, isOfPlayer, isHole,
  containsCells, transformBy) where

import Position
import Cell
import Data.Map

data Board = Board {
  width :: Int,
  height :: Int,
  cells :: Map Position Cell
}

maxWidth = 26 :: Int
maxHeight = 98 :: Int
minWidth = 2 :: Int
minHeight = 2 :: Int

isFree :: Board -> Position -> Bool
isFree board @ (Board _ _ cells) pos =
  board `isInRange` pos && pos `notMember` cells

isInRange :: Board -> Position -> Bool
isInRange (Board width height _) (Position x y) =
  x > 0 && x <= width && y > 0 && y <= height

isOfPlayer :: Board -> Position -> Cell -> Bool
isOfPlayer (Board _ _ cells) pos player =
  player == findWithDefault EmptyCell pos cells

isHole :: Board -> Position -> Bool
isHole (Board _ _ cells) pos =
  Hole == findWithDefault EmptyCell pos cells

containsCells :: Board -> Position -> Position -> Bool
containsCells (Board _ _ cells) from to
  | from == to = Hole /= findWithDefault Hole from cells
  | otherwise = any (Hole /=) cellsToCheck
    where
      cellsToCheck = [
        findWithDefault Hole pos cells |
        y <- [y from .. y to - 1],
        x <- [x from .. x to - 1],
        let pos = Position x y]

transformBy :: Board -> [Position] -> Cell -> Board
transformBy (Board w h cells) positions cell =
  Board w h newCells
    where
      newCells = foldl f cells positions
      f cells pos = insert pos cell cells

Cell.hs

module Cell (
  Cell(..),
  sign,
  asCell,
  isEmpty,
  isCell) where

data Cell = White | Black | Hole | EmptyCell deriving (Show, Eq)

sign :: Cell -> Char
sign White = 'W'
sign Black = 'B'
sign Hole = '#'
sign EmptyCell = '-'

dataCells = map sign [White, Black, Hole]

asCell :: Char -> Cell
asCell c
  | c == sign White = White
  | c == sign Black = Black
  | c == sign Hole = Hole

isEmpty :: Char -> Bool
isEmpty c =
  c == sign EmptyCell

isCell :: Char -> Bool
isCell c =
  c `elem` dataCells

Position.hs

module Position (Position(..)) where

import Data.Char

data Position = Position {
  x :: Int,
  y :: Int
} deriving (Eq)

instance Show Position where
  show (Position x y) = chr (ord 'A' + x - 1) : show y

instance Ord Position where
  (Position x1 y1) `compare` (Position x2 y2) =
    toOrd $ if comp == 0 then y1 - y2 else comp
    where
      comp = x1 - x2

toOrd :: Int -> Ordering
toOrd x
  | x == 0 = EQ
  | x > 0 = GT
  | otherwise = LT

After reading @Cygals post I overworked my code. Now all works fine. I did changes to two files:

Main.hs

import qualified Data.Map as Map
import qualified Data.List as List
import qualified Data.List.Split as Split
import qualified Data.Text as T
import qualified Data.Array as A
import qualified Data.Maybe as Maybe
import qualified Text.Printf as Print
import Control.Exception
import Control.Exception.Base
import System.IO
import Position
import Board
import Cell
import Game
import InputParser

main :: IO ()
main = loop emptyGame >> return ()

loop :: Game -> IO Game
loop game = putStr "> " >> hFlush stdout >> getLine >>= handleInpuut game

handleInpuut :: Game -> String -> IO Game
handleInpuut game input
  | null command = loop game
  | command == "quit" = return game
  | otherwise = do
    handler <- try $ handleCommand game splitted
    case handler of
      Left e -> putStrLn ("Error: " ++ show (e :: ErrorCall)) >> loop game
      Right g -> loop g
  where
    splitted @ (command, _) = splitCommand $ trim input

handleCommand :: Game -> (String, String) -> IO Game
handleCommand game (command, args) =
  case command of
    "newGame" -> createNewGame game args
    "hole" -> createHole game args
    "move" -> move game args
    "abort" -> abort game
    "print" -> printCommand game
    "possibleMoves" -> showPossibleMoves game
    _ -> putStrLn "command not found" >> return game

createNewGame :: Game -> String -> IO Game
createNewGame game args
  | mode game /= GameOverMode = error "there is already an active game"
  | otherwise = if canMove newGame then return newGame else calculatePass newGame
  where
    newGame = parseAs args gameParser $ \f -> case f of
      (width, height, Nothing) -> createGame width height
      (width, height, Just positions) -> createGameWithConfig width height positions

createHole :: Game -> String -> IO Game
createHole game args
  | mode game /= NewMode = error "can't add hole area. there is no game yet or the game is already started"
  | otherwise = if canMove newGame then return newGame else calculatePass newGame
  where
    newGame = parseAs args holeParser $ \f -> case f of
      (from, to) -> addHole' game from to
    addHole' game from to
      | containsCells (board game) from to = error "can't add hole. it is not empty"
      | otherwise = addHole game from to

move :: Game -> String -> IO Game
move game args = do
  requireGameStarted game
  if canMove newGame then return newGame else calculatePass newGame
  where
    newGame = parseAs args position $ \f -> case f of
      pos -> game `moveTo'` pos
    moveTo' game pos
      | notElem pos $ possibleMoves game = error "move not possible"
      | otherwise = game `moveTo` pos

printCommand :: Game -> IO Game
printCommand game = do
  requireGameStarted game
  putStr $ mkString game
  putStrLn $ "turn: " ++ show (curPlayer game)
  return game
  where
    mkString = unlines . rows . table . board
    --table :: Board -> A.Array (Int, Int) Char
    table (Board width height cells) =
      A.array ((1, 1), (height, width))
      [((y, x), sign $ Map.findWithDefault EmptyCell (Position x y) cells) |
      y <- [1 .. height],
      x <- [1 .. width]]
    --rows :: A.Array (Int, Int) a -> [[a]]
    rows arr = [[arr A.! (x, y) |
      y <- [cLow .. cHigh]] |
      x <- [rLow .. rHigh]]
      where
        ((rLow, cLow), (rHigh, cHigh)) = A.bounds arr

abort :: Game -> IO Game
abort game = do
  requireGameStarted game
  calculateWinner (endGame game)

showPossibleMoves :: Game -> IO Game
showPossibleMoves game = do
  requireGameStarted game
  putStrLn ("Possible moves: " ++ moves)
  return game
  where
    moves = List.intercalate "," (map show $ List.sort $ possibleMoves game)

calculatePass :: Game -> IO Game
calculatePass game
  | canMove passed = newGame
  | otherwise = calculateWinner game
  where
    passed = passMove game
    newGame = putStrLn (show (curPlayer game) ++ " passes") >> return passed

calculateWinner :: Game -> IO Game
calculateWinner game =
  putStrLn str >> return newGame
  where
    newGame = endGame game
    (white, black) = List.partition (== White) $ filter (/= Hole) $ Map.elems $ cells $ board newGame
    (numOfWhite, numOfBlack) = (length white, length black)
    winner = if numOfWhite > numOfBlack then White else Black
    max' = max numOfWhite numOfBlack
    min' = min numOfWhite numOfBlack
    str = if numOfWhite == numOfBlack
      then "Game has ended in a draw."
      else Print.printf "Game over! %s has won (%d:%d)!" (show winner) max' min'

requireGameStarted :: Game -> IO ()
requireGameStarted (Game mode _ _ _)
  | mode /= NewMode && mode /= ActiveMode = error "game not started"
  | otherwise = return ()

trim :: String -> String
trim = T.unpack . T.strip . T.pack

splitCommand :: String -> (String, String)
splitCommand str =
  splitAt index str
  where
    index = Maybe.fromMaybe (length str) $ List.elemIndex ' ' str

Game.hs

module Game (
  Game(..), GameMode(..),
  emptyGame, createGame, createGameWithConfig,
  canMove, passMove, endGame, addHole, moveTo) where

import qualified Data.List.Split as Split
import qualified Text.Printf as Print
import qualified Data.List as List
import qualified Data.Map as Map
import Board
import Position
import Cell

data GameMode = NewMode | ActiveMode | GameOverMode deriving (Show, Eq)

data Game = Game {
  mode :: GameMode,
  board :: Board,
  curPlayer :: Cell,
  possibleMoves :: [Position]
}

directions :: [Int -> Int -> Position]
directions = [\x y -> Position (x+1) y, \x y -> Position (x+1) (y+1), \x y -> Position x (y+1),
  \x y -> Position (x-1) (y+1), \x y -> Position (x-1) y, \x y -> Position (x-1) (y-1),
  \x y -> Position x (y-1), \x y -> Position (x+1) (y-1)]

startPositions :: [((Int, Int), Cell)]
startPositions = [((0, 0), White), ((1, 0), Black), ((0, 1), Black), ((1, 1), White)]

emptyGame :: Game
emptyGame = Game GameOverMode (Board 0 0 Map.empty) Black []

createGame :: Int -> Int -> Game
createGame w h = createGameWithConfig w h ""

createGameWithConfig :: Int -> Int -> String -> Game
createGameWithConfig width height str
  | not $ isValidSize width minWidth maxWidth = error "invalid width"
  | not $ isValidSize height minHeight maxHeight = error "invalid height"
  | otherwise = Game NewMode newBoard Black $ calculatePossibleMoves newBoard Black
  where
    newBoard = Board width height $ Map.fromList cells
    cells = if null str then middleCells width height else calculateCells width height str

middleCells :: Int -> Int -> [(Position, Cell)]
middleCells width height =
  map (\((x, y), cell) -> (Position (x+xMiddle) (y+yMiddle), cell)) startPositions
  where
    (xMiddle, yMiddle) = (width `div` 2, height `div` 2)

calculateCells :: Int -> Int -> String -> [(Position, Cell)]
calculateCells width height str
  | not $ isValidData str width height = error "invalid data"
  | otherwise = [
    (Position x y, asCell c) |
    y <- [1 .. height],
    x <- [1 .. width],
    let c = rows !! (y-1) !! (x-1),
    isCell c]
  where
    rows = Split.splitOn "," str

isValidData :: String -> Int -> Int -> Bool
isValidData str width height =
  length rows == height && isRowValid `all` rows
  where
    rows = Split.splitOn "," str
    isRowValid row = length row == width && (\c -> isCell c || isEmpty c) `all` row

isValidSize :: Int -> Int -> Int -> Bool
isValidSize int from to =
  int >= from && int <= to && int `mod` 2 == 0

calculatePossibleMoves :: Board -> Cell -> [Position]
calculatePossibleMoves board cur =
  List.nub $ possiblePositions =<< cellsToProof
  where
    cellsToProof = Map.keys $ Map.filter (cur ==) $ cells board
    possiblePositions pos = id =<< map (checkDirection pos) directions
    checkDirection pos f
      | isCurPlayer = []
      | otherwise = loop $ f (x newPos) (y newPos)
      where
        newPos = f (x pos) (y pos)
        isCurPlayer = not (isOfPlayer board newPos $ nextPlayer cur)
        isInvalid pos = not (board `isInRange` pos) || isOfPlayer board pos cur || board `isHole` pos
        loop pos
          | isInvalid pos = []
          | board `isFree` pos = [pos]
          | otherwise = loop $ f (x pos) (y pos)

moveTo :: Game -> Position -> Game
moveTo game @ (Game _ board cur moves) pos
  | notElem pos moves = error $ "it is impossible to move to position " ++ show pos
  | otherwise = Game ActiveMode newBoard next $ calculatePossibleMoves newBoard next
  where
    positions = pos:(transform =<< directions)
    newBoard = transformBy board positions cur
    next = nextPlayer cur
    transform f = loop [] $ f (x pos) (y pos)
      where
        loop xs pos
          | isOfPlayer board pos next = loop (pos:xs) $ f (x pos) (y pos)
          | isOfPlayer board pos cur = xs
          | otherwise = []

addHole :: Game -> Position -> Position -> Game
addHole (Game mode board cur _) from to
  | containsCells board from to = error $ Print.printf "can't add hole (%s:%s). It is not empty" (show from) (show to)
  | otherwise = Game mode newBoard cur $ calculatePossibleMoves newBoard cur
  where
    positions = [
      Position x y |
      y <- [y from .. y to],
      x <- [x from .. x to]]
    newBoard = transformBy board positions Hole

canMove :: Game -> Bool
canMove = not . null . possibleMoves

nextPlayer :: Cell -> Cell
nextPlayer cur
  | cur == White = Black
  | otherwise = White

passMove :: Game -> Game
passMove (Game mode board cur _) =
  Game mode board next $ calculatePossibleMoves board next
  where
    next = nextPlayer cur

endGame :: Game -> Game
endGame (Game _ board cur moves) =
  Game GameOverMode board cur moves
\$\endgroup\$
4
\$\begingroup\$
  • Handling of errors is done via monads, but you don't need to understand monads in their full glory to know how to handle errors gracefully in Haskell. Oh, and Haskell has exceptions (that's the same link as before), but it's not always the best way to do it.
  • Imports You can look at the guidelines of the HaskellWiki regarding imports.
  • Signatures x :: Game -> XXX -> IO Game is explicit in a nice way, I don't believe it should be avoided.
  • Caching possibleMoves You can cache them in Game or something else, but you can also wonder how related it is to the issue of memoization.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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