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 (
) 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 < n && j < n

type Snake = S.Seq Coord

newSnake :: Int -> 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.:< xs -> (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 {
    grid = foldl (flip $ setCoord Snake) (emptyGrid n) (toList snake),
    snake = snake
} where snake = newSnake n

step :: Game -> Direction -> Maybe Game
step (Game grid snake) dir = case stepSnake snake dir of
    Nothing -> Nothing
    Just (head, snake', tail') -> Just $ Game (foldl f grid [(head, Empty), (tail', Snake)]) snake'
    where f grid (coord, mark) = setCoord mark coord grid
  • \$\begingroup\$ How do I actually make use of this code? \$\endgroup\$ – Enlico Aug 31 '20 at 9:57
  • \$\begingroup\$ What do you plan to use it for? \$\endgroup\$ – wei2912 Sep 1 '20 at 10:33
  • \$\begingroup\$ As a reference for my own exercise of implementing it. Why? \$\endgroup\$ – Enlico Sep 1 '20 at 10:52
  • \$\begingroup\$ I think you can start from newGame and see how the grid changes as you run step game dir, hopefully that helps. \$\endgroup\$ – wei2912 Sep 2 '20 at 13:05
  • \$\begingroup\$ Ok, undestood, thanks. I gave for granted you were presenting a playable game. \$\endgroup\$ – Enlico Sep 2 '20 at 13:06

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
    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.

  • 1
    \$\begingroup\$ (Control.Arrow.&&&) can do the job tuply is. \$\endgroup\$ – Carl May 7 '15 at 22:22
  • \$\begingroup\$ Sure, but then you've got to be willing to use Control.Arrow. \$\endgroup\$ – bisserlis May 7 '15 at 22:39
  • \$\begingroup\$ @bisserlis Thanks for your feedback! I'll follow your suggestions and post a new version for review. @Carl Would you mind giving an example? I'm not familiar with Control.Arrow. \$\endgroup\$ – wei2912 May 8 '15 at 12:30
  • 1
    \$\begingroup\$ @wei2912 stepCoord N = subtract 1 &&& id would be the first branch if you have Control.Arrow imported. \$\endgroup\$ – Carl May 8 '15 at 12:46
  • \$\begingroup\$ @Carl Alright, giving that a try. \$\endgroup\$ – wei2912 May 8 '15 at 15:44

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.