6
\$\begingroup\$

I'm relatively new to Haskell and I'd like to get feedback on the style of my program. Specifically:

  • Coding Style: Can any parts be written in a more concise or more readable way?
  • Misuse: Are there any parts that are generally should be avoided, or any typical beginner mistakes? (Basically the do's and don't's)

The game

The program solves a puzzle that is also known as binoxxo: There is a 2n x 2n square grid that is partially filled with X and O. The goal filling each cell of the grid with an X or O such that the result satisfies three conditions:

  1. In each column and each row, the same symbol cannot occur more than twice in a row. (I.e. OXXOXO is ok, however OXXXOO is not.)
  2. Each row and each column have exactly n Xs and n Os.
  3. No two rows are equal and no two columns are equal.

(An online version can be found here.)

The program

The following module consists of three parts: First we define the new type to describe the grid, with the type constructors X,O and finally E (for an empty cell). Then we have two functions: isValid checks whether all three rules hold so far, and backtrack which actually solves the puzzle. It does so by placing an X and O in the first E spot of the given puzzle checking the validity of the new grids, and if that succeeds, recursing a step deeper. This function finds all the possible solutions, but due to the lazy evaluation we can use take 1 $ backtrack myBoard to get only one and finish a little bit quicker.

module Binoxxo (isValid, backtrack, exampleBoard, Cell (..), Board) where
import Control.Applicative
import Data.List
data Cell = E | X | O -- E = empty
 deriving (Eq,Show)
type Board = [[Cell]]
exampleBoard = -- for solving: call "backtrack exampleBoard"
 [[X,X,E,E],
 [E,E,E,E],
 [O,E,E,X],
 [O,O,E,X]]
-- backtracking
backtrack :: Board -> [Board]
backtrack b
 | isFull b = [b] --board has no more empty cells
 | otherwise = nub $ concat $ map backtrack validBoards
 where
 isFull b = not $ E `elem` concat b
 newBoards = generateAllBoards b :: [Board] 
 validBoards = filter isValid newBoards
 generateAllBoards :: Board -> [Board] -- adds one new X/O in the position of a E
 generateAllBoards b = concat $ map assembleBoards (prefixRowSuffix b)
 where 
 prefixRowSuffix :: [a] -> [([a],a,[a])]-- [1,2,3,4] -> [([],1,[2,3,4]), ([1],2,[3,4]), ([1,2],3,[4]), ([1,2,3],4,[])]
 prefixRowSuffix b = zip3 (inits b) b (drop 1 $ tails b)
 assembleBoards :: ([[Cell]],[Cell],[[Cell]]) -> [Board]
 assembleBoards (front,m,back) = take 2 -- we only need to place X and O in the first occurence of E, because one of them MUST be correct
 [front ++[f++[x]++b]++ back |
 (f,E,b)<-prefixRowSuffix m,x<-[X,O]]
-- validity check (implement the three rules)
isValid :: Board -> Bool
isValid b = and $ 
 [and . map checkNeighbours, checkDupli, and . map checkCount] -- each of these get applied to all normal and transposed board
 <*> [rows, cols] 
 where
 rows = b :: Board
 cols = transpose b :: Board
 -- we cannot have three consecutive X or O
 checkNeighbours :: [Cell] -> Bool
 checkNeighbours (a:b:c:xs) = 
 let this = not $ any ((&& a==b && b==c) . (c==)) [X,O] 
 rest = checkNeighbours (b:c:xs)
 in this && rest
 checkNeighbours _ = True
 -- we cannot have two equal rows/columns
 checkDupli :: Board -> Bool
 checkDupli b = check $ filter (all (/=E)) b -- only check full rows for duplicates
 where check (x:xs) =
 (not $ x `elem` xs) && check xs
 check [] = True
 -- if row is of length n, we can have at most n/2 X and O
 checkCount :: [Cell] -> Bool
 checkCount xs = notTooMany O && notTooMany X
 where 
 len = length xs
 notTooMany xo = len >= 2 * length (filter (==xo) xs)
asked Jun 28, 2017 at 13:18
\$\endgroup\$
1
  • \$\begingroup\$ If you're a Haskell beginner, you might want to add the beginner tag. \$\endgroup\$ Commented Jun 29, 2017 at 6:55

1 Answer 1

2
\$\begingroup\$

isFull fires when assembleBoards returns an empty list; we'd expect to be able to ask that question only once. newBoards and validBoards do not deserve names - if you want the reader to be able to tell what the value means, comments are more appropriate.

Most of the rest of backtrack is about descending into a nested data structure and changing a small part, which lens specializes in: traverse . traverse . filtered (==E) descends into the board, then each of its elements, then each of their E cells. holesOf gives you, roughly speaking, the positions of the targets in the original board - it separates the board into a Cell and a Cell -> Board for each target. peek lets you forget there was an E.

import Control.Comonad.Representable.Store (peek)
import Control.Lens (holesOf, filtered)
backtrack :: Board -> [Board]
backtrack b = case setFirstE b of
 -- board has no more empty cells
 Nothing -> [b]
 -- we only need to place X and O in the first occurence of E, because one of them MUST be correct
 Just setter -> nub $ concatMap backtrack $ filter isValid $ map setter [X,O]
-- Punches the first E out of the board, if any.
setFirstE :: Board -> Maybe (Cell -> Board)
setFirstE = listToMaybe . map (flip peek) . holesOf (traverse . traverse . filtered (==E))
answered Jun 28, 2017 at 17:46
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.