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

• How do I actually make use of this code? – Enlico Aug 31 '20 at 9:57
• What do you plan to use it for? – wei2912 Sep 1 '20 at 10:33
• As a reference for my own exercise of implementing it. Why? – Enlico Sep 1 '20 at 10:52
• I think you can start from newGame and see how the grid changes as you run step game dir, hopefully that helps. – wei2912 Sep 2 '20 at 13:05
• Ok, undestood, thanks. I gave for granted you were presenting a playable game. – 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
where

Use foldl' from Data.List instead of foldl for newGame and step. Why? It's a long story.
• (Control.Arrow.&&&) can do the job tuply is. – Carl May 7 '15 at 22:22
• Sure, but then you've got to be willing to use Control.Arrow. – bisserlis May 7 '15 at 22:39
• @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. – wei2912 May 8 '15 at 12:30
• @wei2912 stepCoord N = subtract 1 &&& id would be the first branch if you have Control.Arrow imported. – Carl May 8 '15 at 12:46