SUDOKU IN HASKELL
Graham Hutton, January 2024
Based upon notes by Richard Bird
The program developed in this note is a good example of what
has been termed "wholemeal programming", the idea of focusing
on entire data structures rather than on their elements. This
approach naturally leads to a compositional style, and relies
on lazy evaluation for efficiency. Our development proceeds
by starting with a simple but impractical program, which is
then refined in a series of steps, ending up with a program
that can solve any newspaper Sudoku puzzle in an instant.
Library file

We use a few things from the list library:
> import Data.List
Basic declarations

We begin with some basic declarations, using the terminology of
sudoku.org.uk. Although the declarations do not enforce it,
we will only consider nonempty square matrices with a multiple
of boxsize (defined in the next section) rows. This assumption
is important for various properties that we rely on.
> type Grid = Matrix Value
>
> type Matrix a = [Row a]
>
> type Row a = [a]
>
> type Value = Char
Basic definitions

> boxsize :: Int
> boxsize = 3
>
> values :: [Value]
> values = ['1'..'9']
>
> empty :: Value > Bool
> empty = (== '.')
>
> single :: [a] > Bool
> single [_] = True
> single _ = False
Example grids

Solvable only using the basic rules:
> easy :: Grid
> easy = ["2....1.38",
> "........5",
> ".7...6...",
> ".......13",
> ".981..257",
> "31....8..",
> "9..8...2.",
> ".5..69784",
> "4..25...."]
First gentle example from sudoku.org.uk:
> gentle :: Grid
> gentle = [".1.42...5",
> "..2.71.39",
> ".......4.",
> "2.71....6",
> "....4....",
> "6....74.3",
> ".7.......",
> "12.73.5..",
> "3...82.7."]
First diabolical example:
> diabolical :: Grid
> diabolical = [".9.7..86.",
> ".31..5.2.",
> "8.6......",
> "..7.5...6",
> "...3.7...",
> "5...1.7..",
> "......1.9",
> ".2.6..35.",
> ".54..8.7."]
First "unsolvable" (requires backtracking) example:
> unsolvable :: Grid
> unsolvable = ["1..9.7..3",
> ".8.....7.",
> "..9...6..",
> "..72.94..",
> "41.....95",
> "..85.43..",
> "..3...7..",
> ".5.....4.",
> "2..8.6..9"]
Minimal sized grid (17 values) with a unique solution:
> minimal :: Grid
> minimal = [".98......",
> "....7....",
> "....15...",
> "1........",
> "...2....9",
> "...9.6.82",
> ".......3.",
> "5.1......",
> "...4...2."]
Empty grid:
> blank :: Grid
> blank = replicate n (replicate n '.')
> where n = boxsize ^ 2
Extracting rows, columns and boxes

Extracting rows is trivial:
> rows :: Matrix a > [Row a]
> rows = id
We also have, trivially, that rows . rows = id. This property (and
similarly for cols and boxs) will be important later on.
Extracting columns is just matrix transposition:
> cols :: Matrix a > [Row a]
> cols = transpose
Example: cols [[1,2],[3,4]] = [[1,3],[2,4]]. Exercise: define
transpose, without looking at the library definition.
We also have that cols . cols = id.
Extracting boxes is more complicated:
> boxs :: Matrix a > [Row a]
> boxs = unpack . map cols . pack
> where
> pack = split . map split
> split = chop boxsize
> unpack = map concat . concat
>
> chop :: Int > [a] > [[a]]
> chop n [] = []
> chop n xs = take n xs : chop n (drop n xs)
Example: if boxsize = 2, then we have
[[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]

pack

v
[[[[1,2],[3,4]],[[5,6],[7,8]]],[[[9,10],[11,12]],[[13,14],[15,16]]]]

map cols

v
[[[[1,2],[5,6]],[[3,4],[7,8]]],[[[9,10],[13,14]],[[11,12],[15,16]]]]

unpack

v
[[1,2,5,6],[3,4,7,8],[9,10,13,14],[11,12,15,16]]
Note that concat . split = id, and moreover, boxs . boxs = id.
Validity checking

Now let us turn our attention from matrices to Sudoku grids. A grid
is valid if there are no duplicates in any row, column or box:
> valid :: Grid > Bool
> valid g = all nodups (rows g) &&
> all nodups (cols g) &&
> all nodups (boxs g)
>
> nodups :: Eq a => [a] > Bool
> nodups [] = True
> nodups (x:xs) = not (elem x xs) && nodups xs
A basic solver

The function choices replaces blank squares in a grid by all possible
values for that square, giving a matrix of choices:
> type Choices = [Value]
>
> choices :: Grid > Matrix Choices
> choices = map (map choice)
> where
> choice v = if empty v then values else [v]
Reducing a matrix of choices to a choice of matrices can be defined
in terms of the normal cartesian product of a list of lists, which
generalises the cartesian product of two lists:
> cp :: [[a]] > [[a]]
> cp [] = [[]]
> cp (xs:xss) = [y:ys  y < xs, ys < cp xss]
For example, cp [[1,2],[3,4],[5,6]] gives:
[[1,3,5],[1,3,6],[1,4,5],[1,4,6],[2,3,5],[2,3,6],[2,4,5],[2,4,6]]
It is now simple to collapse a matrix of choices:
> collapse :: Matrix [a] > [Matrix a]
> collapse = cp . map cp
Finally, we can now specify a suduku solver:
> solve :: Grid > [Grid]
> solve = filter valid . collapse . choices
For the easy example grid, there are 51 empty squares, which means
that this function will consider 9^51 possible grids, which is:
4638397686588101979328150167890591454318967698009
Searching this space isn't feasible; we need to think further.
Pruning the search space

Our first step to making things better is to introduce the idea
of "pruning" the choices that are considered for each square.
Prunes go well with wholemeal programming! In particular, from
the set of all possible choices for each square, we can prune
out any choices that already occur as single entries in the
associated row, column, and box, as otherwise the resulting
grid will be invalid. Here is the code for this:
> prune :: Matrix Choices > Matrix Choices
> prune = pruneBy boxs . pruneBy cols . pruneBy rows
> where pruneBy f = f . map reduce . f
>
> reduce :: Row Choices > Row Choices
> reduce xss = [xs `minus` singles  xs < xss]
> where singles = concat (filter single xss)
>
> minus :: Choices > Choices > Choices
> xs `minus` ys = if single xs then xs else xs \\ ys
Note that pruneBy relies on the fact that rows . rows = id, and
similarly for the functions cols and boxs, in order to decompose
a matrix into its rows, operate upon the rows in some way, and
then reconstruct the matrix. Now we can write a new solver:
> solve2 :: Grid > [Grid]
> solve2 = filter valid . collapse . prune . choices
For example, for the easy grid, pruning leaves an average of around
2.4 choices for each of the 81 squares, or 1027134771639091200000000
possible grids. A much smaller number, but still not feasible.
Repeatedly pruning

After pruning, there may now be new single entries, for which pruning
again may further reduce the search space. More generally, we can
iterate the pruning process until this has no further effect, which
in mathematical terms means that we have found a "fixpoint". The
simplest Sudoku puzzles can be solved in this way.
> solve3 :: Grid > [Grid]
> solve3 = filter valid . collapse . fix prune . choices
>
> fix :: Eq a => (a > a) > a > a
> fix f x = if x == x' then x else fix f x'
> where x' = f x
For example, for our easy grid, the pruning process leaves precisely
one choice for each square, and solve3 terminates immediately. However,
for the gentle grid we still get around 2.8 choices for each square, or
154070215745863680000000000000 possible grids.
Back to the drawing board...
Properties of matrices

In this section we introduce a number of properties that may hold of
a matrix of choices. First of all, let us say that such a matrix is
"complete" if each square contains a single choice:
> complete :: Matrix Choices > Bool
> complete = all (all single)
Similarly, a matrix is "void" if some square contains no choices:
> void :: Matrix Choices > Bool
> void = any (any null)
In turn, we use the term "safe" for matrix for which all rows,
columns and boxes are consistent, in the sense that they do not
contain more than one occurrence of the same single choice:
> safe :: Matrix Choices > Bool
> safe cm = all consistent (rows cm) &&
> all consistent (cols cm) &&
> all consistent (boxs cm)
>
> consistent :: Row Choices > Bool
> consistent = nodups . concat . filter single
Finally, a matrix is "blocked" if it is void or unsafe:
> blocked :: Matrix Choices > Bool
> blocked m = void m  not (safe m)
Making choices one at a time

Clearly, a blocked matrix cannot lead to a solution. However, our
previous solver does not take account of this. More importantly,
a choice that leads to a blocked matrix can be duplicated many
times by the collapse function, because this function simply
considers all possible combinations of choices. This is the
primary source of inefficiency in our previous solver.
This problem can be addressed by expanding choices one square at
a time, and filtering out any resulting matrices that are blocked
before considering any further choices. Implementing this idea
is straightforward, and gives our final Sudoku solver:
> solve4 :: Grid > [Grid]
> solve4 = search . prune . choices
>
> search :: Matrix Choices > [Grid]
> search m
>  blocked m = []
>  complete m = collapse m
>  otherwise = [g  m' < expand m
> , g < search (prune m')]
The function expand behaves in the same way as collapse, except that
it only collapses the first square with more than one choice:
> expand :: Matrix Choices > [Matrix Choices]
> expand m =
> [rows1 ++ [row1 ++ [c] : row2] ++ rows2  c < cs]
> where
> (rows1,row:rows2) = break (any (not . single)) m
> (row1,cs:row2) = break (not . single) row
Note that there is no longer any need to check for valid grids at
the end, because the process by which solutions are constructed
guarantees that this will always be the case. There also doesn't
seem to be any benefit in using "fix prune" rather than "prune"
above; the program is faster without using fix. In fact, our
program now solves any newspaper Sudoku puzzle in an instant!
Exercise: modify the expand function to collapse a square with the
smallest number of choices greater than one, and see what effect
this change has on the performance of the solver.
Testing

> main :: IO ()
> main = putStrLn (unlines (head (solve4 easy)))