If you are stuck on this assignment, please read the page on Getting Help carefully!

Good luck!

Lab Assignment 3 -- Sudoku

In this Lab Assignment, you will design a Haskell program that will be able
to solve Sudokus, a popular logical puzzle originating from Japan.

Assignments and Deadlines

There are 6 regular assignments as part of this Lab: A, B, C, D, E, and F. The
lab consists (again) of two parts.

For submission, assignments A, B, C and D are called Lab 3A.

Assignments
E and F are called Lab 3B.

Deadlines for each of these parts are given on the home page.
There are also extra assignments. You can choose freely
whether to do one of these. Those are just for fun.

Hints

Some assignments have hints. Often, these involve particular standard Haskell
functions that you could use. Some of these functions are defined in modules
that you have to import yourself explicitly. You can use the following
resources to find more information about those functions:

We encourage you to actually go and find information about the functions that
are mentioned in the hints!

Sudokus

Sudoku is a logic puzzle originating in Japan. In the West
it has caught on in popularity enormously over the last five years or so. Most
newspapers now publish a daily Sudoku puzzle for the readers to solve.

A Sudoku puzzle consists of a 9x9 grid. Some of the cells in the grid have
digits (from 1 to 9), others are blank. The objective of the puzzle is to fill
in the blank cells with digits from 1 to 9, in such a way that every row, every
column and every 3x3 block has exactly one occurrence of each digit 1 to 9.

Here is an example of a Sudoku puzzle:

And here is the solution:

In this lab assignment, you will write a Haskell program that can read in a
Sudoku puzzle and solve it.

To implement a Sudoku-solving program, we need to come up with a way of
modelling Sudokus. A Sudoku is a matrix of digits or blanks. The natural way of
modelling a matrix is as a list of lists. The outer list represents all the
rows, and the elements of the list are the elements of each row. Digits or
blanks can be represented by using the Haskell Maybe type. Digits are
simply represented by Int.

Summing up, a natural way to represent Sudokus is using the following Haskell
datatype:

data Sudoku = Sudoku [[Maybe Int]]

Since it is convenient to have a function that extracts the actual rows from the
Sudoku, we actually use the following equivalent datatype definition:

data Sudoku = Sudoku { rows :: [[Maybe Int]] }

For example, the above Sudoku puzzle has the following representation in Haskell:

Now, a number of assignments follows, which will lead you step-by-step towards
an implementation of a Sudoku-solver.

Some Basic Sudoku Functions

To warm up, we start with a number of basic functions on Sudukos.

Assignment A

A1. Implement a function

allBlankSudoku :: Sudoku

that represents a Sudoku that only contains blank cells (this means that no
digits are present).

Do not use copy-and-paste programming here! Your definition does not need to be longer than
a few short lines.

A2. The Sudoku type we have defined allows for more things than Sudokus.
For example, there is nothing in the type definition that says that a
Sudoku has 9 rows and 9 columns,
or that digits need to lie between 1 and 9. Implement a function

A3. Our job is to solve Sudokus. So, it would be handy to know
when a Sudoku is solved or not. We say that a Sudoku is solved if there
are no blank cells left to be filled in anymore. Implement the following
function:

isSolved :: Sudoku -> Bool

Note that we do not check here if the Sudoku is a valid solution; we
will do this later. This means that any Sudoku without blanks (even Sudokus with
the same digit appearing twice in a row) is considered
solved by this function!

Hints

To implement the above, use list comprehensions! Also, the following standard
Haskell functions might come in handy:

that, given a filename, creates instructions that read the Sudoku from the
file, and deliver it as the result of the instructions. You may decide yourself
what to do when the file does not contain a representation of a Sudoku.

To implement the above, you will need to be able to convert between characters
(type Char) and digits/integers (type Int). The standard functions
chr and ord (import the module Data.Char) will come in handy
here. Think about the following problems:

Given a character representing a digit, for example '3' :: Char. How do you compute
the integer value 3?

Given a digit represented as an integer, for example 3 :: Int. How do
you compute the character '3'?

sudokus.zip, a ZIPped collection of sudokus,
both easy and hard ones. The easy ones should all be solvable by your final program
within minutes; the hard ones will probably take a very long time (unless you do
extra Assignment X and/or Y)!.

Generating Sudokus as Test Data

Finally, we need to be able to test properties about the functions related to
our Sudokus. In order to do so, QuickCheck needs to be able to generate
arbitrary Sudokus.

Let us split this problem into a number of smaller problems. First, we need to
know how to generate arbitrary cell values (of type Maybe Int). Then,
we need to know how to generate 81 such cells, and compose them all into a
Sudoku.

Assignment C

C1. Implement a function:

cell :: Gen (Maybe Int)

that, contains instructions for generating a Sudoku cell. You have to think
about the following:

Cells either contain a digit between 1 and 9 (for example Just 3) or
are empty (Nothing),

We would like our generated Sudokus to resemble realistic Sudoku puzzles.
Therefore, the distribution should be around 10% probability non-empty cells
vs. 90% probability for empty cells. (This is not something strict; you can play
around with this if you like.)

Note that some of the above functions only appear when you import
Data.List.

You might want to take a look at the exercises and answers on lists and list
comprehensions.

Positions and Finding Blanks

We are getting closer to the final solving function. Let us start thinking about
how such a function would work.

Given a Sudoku, if there are no blanks left in the Sudoku, we are done.
Otherwise, there is at least one blank cell that needs to be filled in somehow.
We are going to write functions to find and manipulate blank cells.

It is quite natural to start to talk about positions. A position is a
coordinate that identifies a cell in the Sudoku. Here is a way of modelling
coordinates:

type Pos = (Int,Int)

We use positions as indicating first the row and then the column.
For example, the position (3,5) denotes the 5th cell in the 3rd row.

Note: It is common in programming languages to start counting at 0! Therefore,
the position that indicates the upper left corner is (0,0), and the position
indicating the lower right corner is (8,8).

Assignment E

E1. Implement a function:

blanks :: Sudoku -> [Pos]

that, given a Sudoku returns a list of the positions in the
Sudoku that are still blank. You may decide on the order in which the
positions appear.

This, in combination with list comprehensions, should be very useful for this
assignment!

When testing a property that is polymorphic (meaning that it has type variables
in its type), you need to add a type signature that picks an arbitrary type. For
example, when testing properties for the function (!!=), which works for lists
of any type, you have to fix the type when testing, for example lists of
Integers. Do this by adding a type signature to your properties.

Here are some more useful functions:

head :: [a] -> a
(!!) :: [a] -> Int -> a
zip :: [a] -> [b] -> [(a,b)]

Solving Sudokus

Finally, we have all the bits in place to attack our main problem: Solving a
given Sudoku.

Our objective is to define a Haskell function

solve :: Sudoku -> Maybe Sudoku

The basic idea is as follows. Function solve must first check that its argument
is not already a bad Sudoku. This means that (1) it represents a 9x9 sudoku, (2) it has no blocks
(rows, columns, 3x3 blocks) that contain the same digit twice.
We will only do this check once. If the argument is bad then
solve must return Nothing

Now if we have such a Sudoku sud that we would like to solve, we give it to a recursive helper function solve'.

The solve' function must
consider all the blanks in sud. If this list is empty then
by (1) and (2) above we are done, and the answer of solve' (and hence solve)
must be Just sud.

Otherwise there is at least one blank position. We choose one of them.
For this blank position we
we try to recursively solve sud, once for each possible candidate; in each recursive case we update the
blank cell with a candidate. The first recursive attempt that
does not give Nothing provides our solution. But if none of the recursive attempts succeed, we return
Nothing.
This method of problem solving is called
backtracking.

that checks, given two Sudokus, whether the first one is a solution (i.e. all blocks
are okay, there are no blanks), and also whether the first one is a solution of the
second one (i.e. all digits in the second sudoku are maintained in the first one).

that says that the function solve is sound. Soundness means that every
supposed solution
produced by solve actually is a valid solution of the original problem.

Hints

All the work we did in the assignments A -- E should be used in order to implement
the function solve.

QuickChecking the property prop_SolveSound will probably take a long time. Be patient!
Alternatively, there are a number of things you can do about this.

You can test on fewer examples (using the QuickCheck function
quickCheckWith). You can for example define:

fewerChecks prop = quickCheckWith stdArgs{ maxSuccess = 30 } prop

and then write fewerChecks prop_SolveSound when you want to QuickCheck the property.

You can also generate Sudokus with a different probability distribution. Try increasing
the amount of digits in an arbitrary Sudoku by fiddling with the frequencies in the cell function
from Assignment C1 and see what happens.

Sudoku.hs, containing your solution.
It should contain enough comments to understand what is going
on.

Before you submit your code, Clean It Up! Remember, submitting clean code
is Really Important, and simply the polite thing to do. After you feel you are done,
spend some time on cleaning your code; make it simpler, remove unneccessary things, etc.
We will reject
your solution if it is not clean. Clean code: