HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Snake in Haskell

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
haskellsnakestackoverflow

Problem

I wrote a partial implementation of Snake in Haskell. As of now, it only supports the movement of the snake. However, since the code is getting complex, I'm requesting for a review of the code before I add on the food, growing and scoring functions.

I'd be glad to hear any suggestions on how this code could be written in a more idiomatic and simpler way (namely, stepSnake and step are long and messy).

``
-- Snake implementation in Haskell.

module Snake (
newGame
) where

import Control.Lens (set, ix)
import Data.Foldable (toList)
import qualified Data.Sequence as S

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

data Marking = Empty | Food | Snake
deriving (Show, Read, Eq)
type Grid = [[Marking]]

emptyGrid :: Int -> Grid
emptyGrid n = replicate n $ replicate n Empty

type Coord = (Int, Int)
data Direction = N | E | W | S
deriving (Show, Read, Eq)

stepCoord :: Coord -> Direction -> Coord
stepCoord (i, j) dir = case dir of
N -> (i - 1, j)
E -> (i, j + 1)
W -> (i, j - 1)
S -> (i + 1, j)

setCoord :: Marking -> Coord -> Grid -> Grid
setCoord mark (i, j) grid = set (ix i . ix j) mark grid

validCoord :: Coord -> Bool
validCoord (i, j) = i > 0 && j > 0 && i Snake
newSnake n = S.fromList [c, c', c'']
where c = (n
div 2, n div 2)
c' = stepCoord c E
c'' = stepCoord c' E

stepSnake :: Snake -> Direction -> Maybe (Coord, Snake, Coord)
stepSnake snake dir = if validCoord tail' && not (tail'
elem` toList snake')
then Just (head, snake'', tail')
else Nothing
where (head, snake') = case S.viewl snake of
S.EmptyL -> error "snake is empty"
x S.: (x, xs)
tail = case S.viewr snake of
S.EmptyR -> error "snake is empty"
_ S.:> x -> x
tail' = stepCoord tail dir
snake'' = snake' S.|> tail'

data Game = Game {
grid :: Grid,
snake :: Snake
} deriving Show

newGame :: Game
newGame = Game {
g

Solution

It's a little dangerous to define a top-level constant like n, the name isn't descriptive or distinctive and you end up shadowing it quite a bit. A compiling but incorrect typo is almost inevitable. Since you gave it a definitive comment, why not just give it that name instead? Try defaultGridSize.

I can see why you may have used a case statement in stepCoord (to save typing out stepCoord (i, j) repeatedly), but I have a strong preference in the other direction. All pattern-matching which can happen at the top-level generally should.

stepCoord :: Coord -> Direction -> Coord
stepCoord (i, j) N = (i - 1, j)
-- etc...


Or you can write a handy little utility function and reorder your arguments to write a point-free function.

tuply :: (a -> c, b -> d) -> (a, b) -> (c, d) -- tuple, apply, two ply!
tuply (f, g) (a, b) = (f a, g b)

stepCoord :: Direction -> Coord -> Coord
stepCoord N = tuply (subtract 1, id        )
stepCoord E = tuply (id        , + 1       )
stepCoord W = tuply (id        , subtract 1)
stepCoord S = tuply (+ 1       , id        )


Generally all your types should be at the top of the file, i.e. Snake and Game.

I might use a list comprehension in writing newSnake to clean it up a little and reduce potential errors from referencing the wrong c('(')) value.

newSnake :: Int -> Snake
newSnake boardSize = S.fromList [(mid, j) | j <- [mid .. mid + 2]]
  where mid = boardSize `div` 2


Right-hand sides composed solely of an if-statement are good candidates for writing guards.

stepSnake snake dir
    | validCoord tail' && not (tail' `elem` toList snake') = Just (head, snake'', tail')
    | otherwise                                            = Nothing


This is a very dense function though, I think it might be easier to write if you wrap the version using a Seq snake around a primitive list version. Your snakes will presumably never grow so long as to actually cause a user noticeable slowdown between steps due to list processing, so you might think about tossing out Seq altogether.

stepSnake :: Snake -> Direction -> Maybe (Coord, Snake, Coord)
stepSnake snake dir = fmap (second fromList) (stepSnakeList (toList snake) dir)
  where second f (a, b, c) = (a, f b, c)

stepSnakeList :: [Coord] -> Direction -> Maybe (Coord, [Coord], Coord)
stepSnakeList snake dir
    | validCoord newHead && not (newHead `elem` snake) = Just (oldTail, newSnake, newHead)
    | otherwise                                        = Nothing
  where
    oldTail  = head snake
    body     = tail snake
    oldHead  = last snake
    newHead  = stepCoord oldHead dir
    newSnake = body ++ [newHead]


Use foldl' from Data.List instead of foldl for newGame and step. Why? It's a long story.

Code Snippets

stepCoord :: Coord -> Direction -> Coord
stepCoord (i, j) N = (i - 1, j)
-- etc...
tuply :: (a -> c, b -> d) -> (a, b) -> (c, d) -- tuple, apply, two ply!
tuply (f, g) (a, b) = (f a, g b)

stepCoord :: Direction -> Coord -> Coord
stepCoord N = tuply (subtract 1, id        )
stepCoord E = tuply (id        , + 1       )
stepCoord W = tuply (id        , subtract 1)
stepCoord S = tuply (+ 1       , id        )
newSnake :: Int -> Snake
newSnake boardSize = S.fromList [(mid, j) | j <- [mid .. mid + 2]]
  where mid = boardSize `div` 2
stepSnake snake dir
    | validCoord tail' && not (tail' `elem` toList snake') = Just (head, snake'', tail')
    | otherwise                                            = Nothing
stepSnake :: Snake -> Direction -> Maybe (Coord, Snake, Coord)
stepSnake snake dir = fmap (second fromList) (stepSnakeList (toList snake) dir)
  where second f (a, b, c) = (a, f b, c)

stepSnakeList :: [Coord] -> Direction -> Maybe (Coord, [Coord], Coord)
stepSnakeList snake dir
    | validCoord newHead && not (newHead `elem` snake) = Just (oldTail, newSnake, newHead)
    | otherwise                                        = Nothing
  where
    oldTail  = head snake
    body     = tail snake
    oldHead  = last snake
    newHead  = stepCoord oldHead dir
    newSnake = body ++ [newHead]

Context

StackExchange Code Review Q#90054, answer score: 5

Revisions (0)

No revisions yet.