Sunday, June 2, 2013

Sudoku ReRedux

Ok, this is why I'm less than proud of that actually, factually working solution.

import List
 
main = putStr . unlines . map disp . solve . return . input =<< getContents
 
solve s = foldr (\p l -> [mark (p,n) s | s <- l, n <- s p]) s idx
 
mark (p@(i,j),n) s q@(x,y)
    | p == q                             = [n]
    | x == i || y == j || e x i && e y j = delete n (s q)
    | otherwise                          = s q
    where e a b = div (a-1) 3 == div (b-1) 3
 
disp s = unlines [unwords [show $ head $ s (i,j) | j <- [1..9]] | i <- [1..9]]
 
input s = foldr mark (const [1..9]) $
  [(p,n) | (p,n) <- zip idx $ map read $ lines s >>= words, n>0]
 
idx = [(i,j) | i <- [1..9], j <- [1..9]]

Except, as I mentioned, that one cheats by omitting the type signatures[1], so here's the original on which it was based:

import Data.List

type T = (Int,Int) -> [Int]

main = do
  s <- getContents
  putStr $ unlines $ map disp $ solve [input s]

solve :: [T] -> [T]
solve s = foldr search s idx where
    search p l = [mark (p,n) s | s <- l, n <- s p]

mark :: ((Int,Int),Int) -> T -> T
mark (p@(i,j),n) s q@(x,y) =
  if p==q then [n] else
  if x==i || y==j || e x i && e y j then delete n $ s q else s q
  where e a b = div (a-1) 3==div (b-1) 3

disp :: T -> String
disp s  = unlines [unwords [show $ head $ s (i,j) | j <- [1..9]] | i <- [1..9]]

input :: String -> T
input s = foldr mark (const [1..9]) $
  [(p,n) | (p,n) <- zip idx $ map read $ lines s >>= words, n>0]

idx :: [(Int,Int)]
idx = [(i,j) | i <- [1..9], j <- [1..9]]

This is not the most readable code ever; its goal is supreme elegance[2], not instant clarity. It took me a couple of days thinking on-and-off, as well as a read-through of this almost equivalent Python transliteration[3] to finally understand what the hell is going on here.

Lets get the obvious out of the way.

disp :: T -> String
disp s  = unlines [unwords [show $ head $ s (i,j) | j <- [1..9]] | i <- [1..9]]

This takes a board (whose type is named T for some reason), and returns its string representation.

input :: String -> T
input s = foldr mark (const [1..9]) $
  [(p,n) | (p,n) <- zip idx $ map read $ lines s >>= words, n>0]

This takes a string representation and returns a board.

idx :: [(Int,Int)]
idx = [(i,j) | i <- [1..9], j <- [1..9]]

This returns all the (y, x) coordinates in a 9x9 board.

main = do
  s <- getContents
  putStr $ unlines $ map disp $ solve [input s]

This takes from standard in, tries to interpret the result as a board, solve it and print it.

type T = (Int,Int) -> [Int]

And finally, this is how a board is represented; it's a function of one argument, an Int, Int tuple, and returns a list of possible values, a [Int].

Before we go any further, there are a lot of naming conventions here that are aimed at terseness rather than comprehensibility of the resulting code. So lets just do a naive renaming for now.

import Data.List

type Board = (Int,Int) -> [Int]

main = do
  boardString <- getContents
  putStr . unlines . map disp $ solve [input boardString]

solve :: [Board] -> [Board]
solve boards = foldr search boards idx where
    search (x, y) boards = [mark ((x, y),val) brd | brd <- boards, val <- brd (x, y)]

mark :: ((Int,Int),Int) -> Board -> Board
mark (p@(x,y),val) board p'@(x',y') = 
  if p==p' then [val] else 
    if x==x' || y==y' || blockBound x x' && blockBound y y' then delete val $ board p' else board p'
  where blockBound a b = div (a-1) 3==div (b-1) 3

disp :: Board -> String
disp board = unlines [unwords [show . head $ board (x,y) | y <- [1..9]] | x <- [1..9]]

input :: String -> Board
input boardString = foldr mark (const [1..9]) $
  [((x, y),val) | ((x, y),val) <- zip idx . map read $ lines boardString >>= words, val>0]

idx :: [(Int,Int)]
idx = [(x,y) | y <- [1..9], x <- [1..9]]

Granted, we can no longer claim "707 bytes", but even this minor renaming makes the end result a bit more understandable. On to the difficult parts.

mark :: ((Int,Int),Int) -> Board -> Board
mark (p@(x,y),val) board p'@(x',y') = 
  if p==p' then [val] else 
    if x==x' || y==y' || blockBound x x' && blockBound y y' then delete val $ board p' else board p'
  where blockBound a b = div (a-1) 3==div (b-1) 3

input :: String -> Board
input boardString = foldr mark (const [1..9]) $
  [((x, y),val) | ((x, y),val) <- zip idx . map read $ lines boardString >>= words, val>0]

solve :: [Board] -> [Board]
solve boards = foldr search boards idx where
  search (x, y) boards = [mark ((x, y),val) brd | brd <- boards, val <- brd (x, y)]

The high level of what's going on here is that you're representing a board as a function of (Int, Int) -> [Int], and marking spaces by wrapping that function up in a dispatch/delete which returns pruned results in some circumstances.

mark :: ((Int,Int),Int) -> Board -> Board
mark (p@(x,y),val) board p'@(x',y') = 
  if p==p' then [val] else 
    if x==x' || y==y' || blockBound x x' && blockBound y y' then delete val $ board p' else board p'
  where blockBound a b = div (a-1) 3==div (b-1) 3

This function uses some uncommon notation, and isn't really structured the way you'd expect in a Haskell program. That initial 12-line solution actually does a marginally better job of it. Here's a slightly revised, but equivalent version[7]

mark :: ((Int,Int),Int) -> Board -> Board
mark (p@(x,y),val) board p'@(x',y') 
  | p == p' = 
    [val]
  | x==x' || y==y' || blockBound x x' && blockBound y y' = 
    delete val $ board p'
  | otherwise =
    board p'
  where blockBound a b = div (a-1) 3==div (b-1) 3

That uses the more common guard statements rather than a cascaded if/then/else. The input line and type signature on this one is what threw me for the longest time, so I'm going to linger there for a moment.

mark :: ((Int,Int),Int) -> Board -> Board
mark (p@(x,y),val) board p'@(x',y') 

Remember, our Board is defined as ((Int, Int) -> [Int]), so that type signature could also be written

mark :: ((Int,Int),Int) -> (Int, Int) -> [Int] -> (Int, Int) -> [Int]

which should ironically clarify things. The actual arguments aren't doing anyone any favors either. The @s there are applying labels to some destructured constructs. The end result is that you can use the name p instead of (x, y) and p' instead of (x', y'). The following code is equivalent, but very slightly longer[8]:

mark ((x,y),val) board (x',y') 
  | (x, y) == (x', y') = 
    [val]
  | x==x' || y==y' || blockBound x x' && blockBound y y' = 
    delete val $ board (x', y')
  | otherwise =
    board (x', y')
  where blockBound a b = div (a-1) 3==div (b-1) 3

Right, so that's how you mark a value. Except it doesn't really make sense in isolation. Not until we take a look at, at minimum, input

input :: String -> Board
input boardString = foldr mark (const [1..9]) $
  [((x, y),val) | ((x, y),val) <- zip idx . map read $ lines boardString >>= words, val>0]

This is the function that takes a board string and returns an actual board constructed from it. The main operation there is foldr, and I'm going to assume you understand how a fold works for this exercise. If you don't, read this and this, then do some googling. (const [1..9]) is a function that always returns the list of integers from 1 to 9, inclusive. It's equivalent to (\_ -> [1,2,3,4,5,6,7,8,9])[9]. What it produces... is a bit trickier. It has to do with an inherent property of Haskell, and that type signature for mark I showed earlier.

mark :: ((Int,Int),Int) -> (Int, Int) -> [Int] -> (Int, Int) -> [Int]

First off, Haskell partially applies everything by default. Meaning that if you pass fewer than 4 arguments to mark, what you actually get back is a function that takes the next argument, and returns either the next partial or the final result. If you take a look at foldr, its type is

foldr :: (a -> b -> b) -> b -> [a] -> b

which means that it'll be treating mark as a function of two arguments. Note that the second argument is itself a function. Specifically, a (Int, Int) -> [Int], which means that mark will be getting three of its arguments filled. It might be easier to think about it like this

mark :: ((Int,Int),Int) -> ((Int, Int) -> [Int]) -> (Int, Int) -> [Int]

but since every function in Haskell can be applied partially, those are equivalent types. The end result of that fold operation is another function of (Int, Int) -> [Int]. Lets take a real close look at what's going on there.

This is the empty board

0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0

Because it only contains zeros, it'll be represented as (const [1..9]). Of course, it also has to be encoded as

"0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0"

but that first one is easier to read.

GHCi, version 7.4.1: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> :load "/home/inaimathi/projects/code-retreat/sudoku/sudoku-elegant.hs"
[1 of 1] Compiling Main             ( /home/inaimathi/projects/code-retreat/sudoku/sudoku-elegant.hs, interpreted )
Ok, modules loaded: Main.
*Main> let b = input "0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0"
*Main> b (1, 2)
[1,2,3,4,5,6,7,8,9]
*Main> b (1, 3)
[1,2,3,4,5,6,7,8,9]
*Main> b (6, 3)
[1,2,3,4,5,6,7,8,9]
*Main> 

Now, adding a value makes sure it recurs once.

*Main> let b2 = input "4 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0"
*Main> b2 (1, 1)
[4]
*Main> b2 (1, 2)
[1,2,3,5,6,7,8,9]
*Main> b2 (2, 1)
[1,2,3,5,6,7,8,9]
*Main> b2 (2, 3)
[1,2,3,5,6,7,8,9]
*Main> b2 (5, 5)
[1,2,3,4,5,6,7,8,9]
*Main> 

That's the key to understanding this. Lets do the Little Schemer thing, and break input down. Not necessarily the way GHC does it, but so that we can conceptually understand what's happening here.

00}} input "4 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0"

01}} foldr mark (const [1..9]) $ 
                [((x, y),val) | 
                 ((x, y),val) <- zip idx . 
                                 map read $ 
                                     lines boardString 
                                     >>= words, 
                                     val>0]

02}} foldr mark (const [1..9]) $ 
                [((x, y),val) | 
                 ((x, y),val) <- zip idx . 
                                 map read $ 
                                     ["4 0 0 0 0 0 0 0 0",
                                      "0 0 0 0 0 0 0 0 0",
                                      "0 0 0 0 0 0 0 0 0",
                                      "0 0 0 0 0 0 0 0 0",
                                      "0 0 0 0 0 0 0 0 0",
                                      "0 0 0 0 0 0 0 0 0",
                                      "0 0 0 0 0 0 0 0 0",
                                      "0 0 0 0 0 0 0 0 0",
                                      "0 0 0 0 0 0 0 0 0"]
                                     >>= words, 
                                     val>0]

03}} foldr mark (const [1..9]) $ [((1, 1), 4)]

04}} foldr (\((x,y),val) board (x',y') 
             | (x, y) == (x', y') = 
               [val]
             | x==x' || y==y' || blockBound x x' && blockBound y y' = 
               delete val $ board (x', y')
             | otherwise =
               board (x', y')
             where blockBound a b = div (a-1) 3==div (b-1) 3)
           (const [1..9]) $
           [((1, 1), 4)]

05}} (\board (x',y') 
        | (1, 1) == (x', y') = 
          [4]
        | 1==x' || 1==y' || blockBound 1 x' && blockBound 1 y' = 
          delete 4 $ board (x', y')
        | otherwise =
          board (x', y')
        where blockBound a b = div (a-1) 3==div (b-1) 3) (const [1..9])

06}} (\(x',y') 
        | (1, 1) == (x', y') = 
          [4]
        | 1==x' || 1==y' || blockBound 1 x' && blockBound 1 y' = 
          delete 4 $ (const [1..9]) (x', y')
        | otherwise =
          (const [1..9]) (x', y')
        where blockBound a b = div (a-1) 3==div (b-1) 3)

And there. If we added another space, it would unfold another level, with the entire step 06}} there being slotted in instead of (const [1..9]). Ok, last bit.

solve :: [Board] -> [Board]
solve boards = foldr search boards idx where
  search (x, y) boards = [mark ((x, y),val) brd | brd <- boards, val <- brd (x, y)]

Hopefully, now that I've unfoldrd the definition of input, this is intuitively obvious. search is an internal function that takes a list of boards and a space (x, y), and attempts to solve for them. It does this by taking each possibility for that space on each board and marking them, collecting all the results. If you look carefully, and have read those foldr links from earlier, this also explains why the Haskell version starts returning answers very quickly. The way iteration unfolds here, the first board is going to be solved quite a while before the complete sequence is solved, which means it'll be returned and printed quickly and thereafter not take further resources from the program.

The page "explaining" this code claims that it's "neither fast nor clever", and the Python version states that it's "Not the ideal way to solve Sudoku", but I'm honestly having a hard time imagining one that would give you any kind of gain, either in terms of performance or elegance[10].

Possibly the most interesting thing about this solution for me is that, since it generates a list of all possible boards given a solution, you write a generator fairly simply[11] using something along the lines of choice $ solve [input . take 161 $ cycle "0 "], then redacting the results to a desired difficulty level. That might be another thing for me to throw some time at.


Footnotes

1 - [back] -Which, judging by the responses I get whenever I ask for comments on my Haskell code, is somewhere between grossly impolite and crime-against-humanatee.

2 - [back] -Which it hits, in my opinion.

3 - [back] -Python doesn't have the same approach to partial functions that Haskell does, so the transliteration is both slightly easier to understand and slightly clunkier.[4] It also uses foldl instead of foldr, because Python only comes with an implementation of foldl. Something tells me this kneecaps the .py versions' performance. Testing it out on the sample data listed at the bottom of this page, after manually sanitizing for spaces, seems to confirm that suspicion. On my machine, it spun up to 100% usage on one core until it occupied all of my memory, then sat there paging until I killed it. The Haskell solution, by contrast, starts producing results very close to instantly, puts all 4 cores to good use, and utterly fails to mem-rape my laptop before computing all possible solutions, which it does well before the Python version produces any solutions[5].

4 - [back] -That's Python for you far as I can tell, in case you were wondering. It could almost be their slogan. "Python: Easier to understand and fatter than Haskell.".

5 - [back] -So I guess that slogan should really be "Python: Easier to understand, fatter and much slower than Haskell."[6].

6 - [back] -Ok, that isn't entirely fair; this example wasn't optimized in any sense of the word. It uses list comprehensions instead of generators, and could probably implement a lazy foldr equivalent to start returning results right away. I'll put a bit of time into that later.

7 - [back] -As an aside here, it's mildly frustrating that every single gain in clarity in this exercise adds lines to the final count. I wish there was a way of being clearer while being more succinct.

8 - [back] -which I get the feeling is why the author chose to use the @s.

9 - [back] -though in this particular case, it'll be treated as (\(_, _) -> [1,2,3,4,5,6,7,8,9]) :: ((Int, Int) -> [Int]) because of how Board is defined.

10 - [back] -Though, obviously, I think clarity could be somewhat improved.

11 - [back] -I was going to say "Trivially", and then promptly lost 40 minutes to trying to figure out why exactly it is that an RVar [Int] can't be shown by default, or have its contents putStrd no matter how much liftM was applied. "Easier to understand, but fatter and slower than Haskell", also happens to be why I've been using Python at work. Haskell makes certain, very well understood things supremely easy, but as soon as I sit down to do something like output or randomness that's trivial in other languages, I find myself suffering a few hours of headaches before figuring it out. I also happen to agree with Sussman about the core point; implementing things that are very well understood is not the primary goal of programming.

Sudoku Redux

I was in a particularly blah mood today, so I decided to sharpen my teeth on a problem I had half-solved from earlier. Solving Sudoku in Haskell. The code for the solution is up at the appropriate github.

Interlude

Before we get to the actual code though, do you remember that page I linked, chock full of Sudoku solvers written in Haskell? Well, there aren't as many there as I thought. About half the links from that page actually lead to 404 pages of various intricacies instead of to the examples they promise. The ones you can see source for are all there, but that's really all you can guarantee.

Also, I'll have to take it back.

The appropriate Rosetta Code page doesn't have any solutions that leave me gobsmacked by elegance the way that the Clojure Game of Life did. Inaimathi

That's false. Specifically, once I sat down to actually read read the examples there instead of just flipping through them, I got caught by one that I passed over the first time. The code actually on the Haskellwiki page is even shorter than that, but it does it by omitting the type declarations, which is borderline cheating in Haskell. It took me an embarrassingly long time to understand the approach in my bones, so I'll go over it in depth in a follow-up article just in case I'm not the only one.

Sudoku

So all these people are using Haskell to commit sudoku? Oh what a world... Anonymous
Inaimathi

Like I said, we did Sudoku solvers at the last Toronto Code Retreat. The group of three I worked in for the Haskell attempt came up with this. And I've since expanded that to a solver that works in the general case, although admittedly, very slowly[1].

Here's the code

module Main where

import Data.Set (Set(..), toList, fromList, difference, member)
import qualified Data.Set as Set
import Data.List (sort, sortBy, intercalate, group, find)
import Data.List.Split (chunksOf)
import Data.Ord (comparing)
import Data.Char (intToDigit)
import Data.Maybe (fromJust)

---------- Class Definition, constructors and sample data
data Board = Board { values :: [[Int]], 
                     empty :: Set (Int, Int),
                     size :: Int, 
                     ixs :: [Int],
                     blockSize :: Int } deriving (Eq)

instance Show Board where
  show board = (:) '\n' $ unlines . intercalate hdelim . split . lns $ values board
    where lns = map (intercalate "|" . split . map sq)
          split = chunksOf bs
          sq n = if n == 0 then ' ' else intToDigit n
          hdelim = [replicate (size board + (bs - 1)) '-']
          bs = blockSize board

sampleSmall = toBoard [[1, 0, 3, 0],
                       [0, 4, 0, 2],
                       [0, 3, 4 ,0],
                       [4, 0, 2, 3]]

sample = toBoard [[0,7,1,4,0,0,0,0,5],
                  [0,0,0,0,5,0,0,8,0],
                  [0,0,3,9,0,7,6,0,0],
                  [0,0,0,0,0,1,0,0,0],
                  [0,9,0,8,0,6,0,0,3],
                  [0,0,0,0,0,0,8,2,0],
                  [0,6,0,0,4,0,7,0,8],
                  [3,0,0,0,0,0,0,9,0],
                  [0,0,0,0,8,5,0,0,0]]

sampleHard = toBoard [[0,7,1,4,0,0,0,0,5],
                      [0,0,0,0,5,0,0,8,0],
                      [0,0,3,9,0,7,6,0,0],
                      [0,0,0,0,0,1,0,0,0],
                      [0,9,0,0,0,6,0,0,3],
                      [0,0,0,0,0,0,8,2,0],
                      [0,0,0,0,4,0,0,0,8],
                      [3,0,0,0,0,0,0,9,0],
                      [0,0,0,0,8,5,0,0,0]]

sampleDevilish = toBoard [[0,7,1,4,0,0,0,0,0],
                          [0,0,0,0,5,0,0,0,0],
                          [0,0,3,9,0,7,6,0,0],
                          [0,0,0,0,0,0,0,0,0],
                          [0,9,0,0,0,6,0,0,3],
                          [0,0,0,0,0,0,0,0,0],
                          [0,0,0,0,4,0,0,0,8],
                          [0,0,0,0,0,0,0,9,0],
                          [0,0,0,0,8,5,0,0,0]]

toBoard :: [[Int]] -> Board
toBoard values = findEmpties $ Board { values = values, empty = fromList [],
                                       size = len, ixs = [0..len - 1], blockSize = bs }
  where bs = fromEnum . sqrt . toEnum $ length values
        len = length values

findEmpties :: Board -> Board
findEmpties board = board { empty = fromList [(x, y) | y <- is, x <- is, blank (x, y)] }
  where blank (x, y) = 0 == ((values board) !! y !! x)
        is = ixs board

---------- The solver
main = putStr . show $ solve sampleDevilish

solve :: Board -> Board
solve board = rec [naiveSolve [obvious, blockwise] board]
  where solved board = 0 == (Set.size $ empty board)
        impossible board = any ((==0) . length) . map (toList . possibilities board) . toList $ empty board
        rec [] = board -- Failed
        rec boards = case find solved $ boards of
          Just b -> b
          Nothing -> rec . map (naiveSolve [obvious, blockwise]) . concatMap guess $ filter (not . impossible) boards

naiveSolve :: [(Board -> Board)] -> Board -> Board
naiveSolve functions board = rec functions board
  where rec [] board = board
        rec fns board = case Set.size $ empty new of
          0 -> new
          _ -> rec nextFns new
          where new = (head fns) $ board
                nextFns = if new == board then tail fns else functions
        
---------- The solve stages
obvious :: Board -> Board
obvious board = findEmpties $ board { values = newVals }
  where newVals = [[newVal (x, y) | x <- ixs board] | y <- ixs board]
        ps x y = toList $ possibilities board (x, y)
        newVal (x, y) = case ((values board) !! y !! x, ps x y) of
          (0, [val]) -> val
          (val, _) -> val

blockwise :: Board -> Board
blockwise board = findEmpties $ board { values = new }
  where new = [[newVal (x, y) | x <- ixs board] | y <- ixs board]
        newVal (x, y) = case find (\(x', y', v) -> (x == x') && (y == y')) uniques of
          Just (_, _, v) -> v
          Nothing -> (values board) !! y !! x
        uniques = concat [uniqueInBlock board (x, y) | y <- bIxs, x <- bIxs]
        bIxs = [0, bs..size board-1]
        bs = blockSize board

guess :: Board -> [Board]
guess board = map (\v -> findEmpties $ board { values = newVals v }) vs
  where (x, y, vs) = head $ sortBy (comparing (length . thd)) posMap
        newVals v = [[if x == x' && y == y' then v else (values board) !! y' !! x' | x' <- ixs board] | y' <- ixs board]
        posMap = [(x, y, toList $ possibilities board (x, y)) | (x, y) <- es]
        es = toList $ empty board


---------- Solver-related utility
possibilities :: Board -> (Int, Int) -> Set Int
possibilities board (x, y) = foldl difference (fromList [1..size board]) sets
  where sets = mapply (board, (x, y)) [row, col, block]

row :: Board -> (Int, Int) -> Set Int
row board (x, y) = fromList $ values board !! y

col :: Board -> (Int, Int) -> Set Int
col board (x, y) = fromList . map (!! x) $ values board

block :: Board -> (Int, Int) -> Set Int
block board (x, y) = fromList . concat . square $ values board
  where square = map (take bs . drop (origin x)) . take bs . drop (origin y)
        origin n = bs * intFloor n bs
        bs = blockSize board

uniqueInBlock :: Board -> (Int, Int) -> [(Int, Int, Int)]
uniqueInBlock board (x, y) = singles $ concatMap (toList . thd) posMap
  where posMap = [(x', y', possibilities board (x', y')) | (x', y') <- es]
        es = blockEmpties board (x, y)
        singles = map (findInMap . head) . filter ((==1) . length) . group . sort
        findInMap n = let (x, y, p) = fromJust $ find (member n . thd) posMap
                      in (x, y, n)

blockEmpties :: Board -> (Int, Int) -> [(Int, Int)]
blockEmpties board (x, y) = [(x', y') | x' <- xs, y' <- ys, blank (x', y')]
  where blank (x, y) = 0 == ((values board) !! y !! x)
        xs = [ox..ox + bs-1]
        ys = [oy..oy + bs-1]
        [ox, oy] = map origin [x, y]
        origin n = bs * intFloor n bs
        bs = blockSize board

---------- General Utility
mapply :: (a, b) -> [(a -> b -> c)] -> [c]
mapply args fns = map (\fn -> uncurry fn $ args) fns

intFloor :: Int -> Int -> Int
intFloor a b = fromEnum . floor . toEnum $ a `div` b

thd :: (a, b, c) -> c
thd (a, b, c) = c

Just over 110 lines of pretty ham-fisted Haskell, not counting the example data and general utility functions. At a high level, the way this is supposed to work is by taking a board, repeatedly solving all the obvious spaces, potentially doing a blockwise analysis then repeatedly solving the new obvious spaces, and potentially guessing if neither of those tactics work out. In other words, this is more or less a formalization of the basic brute-force method a human Sudoku beginner might use to solve a board. If we ever get to a solved board, we return it, if we discover we've been given an impossible board, we return the input instead.

First off, we've changed our definition of a board from a naive 2D array to a more complex type that keeps some needed info around...

data Board = Board { values :: [[Int]], 
                     empty :: Set (Int, Int),
                     size :: Int, 
                     ixs :: [Int],
                     blockSize :: Int } deriving (Eq)

...and we've taken the opportunity to just make it an instance of Show.

instance Show Board where
  show board = (:) '\n' $ unlines . intercalate hdelim . split . lns $ values board
    where lns = map (intercalate "|" . split . map sq)
          split = chunksOf bs
          sq n = if n == 0 then ' ' else intToDigit n
          hdelim = [replicate (size board + (bs - 1)) '-']
          bs = blockSize board

Lets start in the middle this time:

obvious :: Board -> Board
obvious board = findEmpties $ board { values = newVals }
  where newVals = [[newVal (x, y) | x <- ixs board] | y <- ixs board]
        ps x y = toList $ possibilities board (x, y)
        newVal (x, y) = case ((values board) !! y !! x, ps x y) of
          (0, [val]) -> val
          (val, _) -> val

That's how we solve a board with obvious values in it: just return a new board with the appropriate spaces filled with their only possible value, and removed from the empty space set. Nothing special here. Slightly more interesting is how we go to the next step

blockwise :: Board -> Board
blockwise board = findEmpties $ board { values = new }
  where new = [[newVal (x, y) | x <- ixs board] | y <- ixs board]
        newVal (x, y) = case find (\(x', y', v) -> (x == x') && (y == y')) uniques of
          Just (_, _, v) -> v
          Nothing -> (values board) !! y !! x
        uniques = concat [uniqueInBlock board (x, y) | y <- bIxs, x <- bIxs]
        bIxs = [0, bs..size board-1]
        bs = blockSize board

Rather than checking for sets that have only one remaining possibility, this checks whether there's a unique position for any value within a block. To illustrate:

GHCi, version 7.4.1: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> :load "/home/inaimathi/projects/code-retreat/sudoku/sudoku.hs"
[1 of 1] Compiling Main             ( /home/inaimathi/projects/code-retreat/sudoku/sudoku.hs, interpreted )
Ok, modules loaded: Main.
*Main> sample
Loading package array-0.4.0.0 ... linking ... done.
Loading package deepseq-1.3.0.0 ... linking ... done.
Loading package containers-0.4.2.1 ... linking ... done.
Loading package split-0.2.1.2 ... linking ... done.

 71|4  |  5
   | 5 | 8 
  3|9 7|6  
-----------
   |  1|   
 9 |8 6|  3
   |   |82 
-----------
 6 | 4 |7 8
3  |   | 9 
   | 85|   

*Main> let obv board = if o == board then board else obv o where o = obvious board
*Main> obv sample

 71|4 8| 35
   | 53| 8 
  3|9 7|6  
-----------
   |  1|   
 9 |8 6|  3
   |  4|82 
-----------
 6 | 49|7 8
3  |  2| 9 
   | 85|   

*Main> 

This is how far repeatedly solving the obvious blocks gets us. BUT, there are still squares there that have unambiguous solutions. Specifically

 71|4 8| 35
   | 53| 8X
  3|9 7|6  
-----------
   |  1|   
 9 |8 6|  3
   |  4|82 
-----------
 6 | 49|7 8
3  |  2| 9 
   | 85|X  

Those two have only one possible value. If you take a look at their possibilities list, it doesn't look that way

*Main> possibilities (obv sample) (8, 1)
fromList [1,2,4,7,9]
*Main> possibilities (obv sample) (6, 8)
fromList [1,2,3,4]
*Main> 

but if you take a look at only the intersecting values something becomes clear.

 7.|. .| 35
   | ..| 8X
  .|. 7|6  
-----------
   |  .|   
 . |. .|  3
   |  .|.. 
-----------
 . | ..|7 8
3  |  .| 9 
   | ..|X  

Because of the placements of 7s, and the existing values in block 6,0, the only remaining space in that block that could contain a 7 is (8, 1). The same situation is happening with 3s in block 6,6. Because our possibilities function is only doing a set subtraction, it fails to detect this.

I get the feeling that this is what Josh was getting in my first group; what you want in this situation is to figure out whether there's a unique place within a given block that a given value could go. These squares

 71|4 8|X35
   | 53|X8X
  3|9 7|6XX
-----------
   |  1|   
 9 |8 6|  3
   |  4|82 
-----------
 6 | 49|7 8
3  |  2| 9 
   | 85|  

have these possibilities:

*Main> mapM_ (putStrLn . show . toList) $ map (possibilities (obv sample)) [(6, 0), (6, 1), (8, 1), (7, 2), (8, 2)]
[2,9]
[1,2,4,9]
[1,2,4,7,9]
[1,4]
[1,2,4]
*Main> 

As you can see, only one of those possibility sets contains 7, whereas the other values could go in more than one place. What we want, in terms of our existing board definition, is a way to put that value in the place it can uniquely occupy. That's done here:

uniqueInBlock :: Board -> (Int, Int) -> [(Int, Int, Int)]
uniqueInBlock board (x, y) = singles $ concatMap (toList . thd) posMap
  where posMap = [(x', y', possibilities board (x', y')) | (x', y') <- es]
        es = blockEmpties board (x, y)
        singles = map (findInMap . head) . filter ((==1) . length) . group . sort
        findInMap n = let (x, y, p) = fromJust $ find (member n . thd) posMap
                      in (x, y, n)

That function takes a Board and an (x, y), and returns the coordinates and values of each unique value in block board (x, y). In our example board,

*Main> uniqueInBlock (obv sample) (6, 0)
[(8,1,7)]
*Main> uniqueInBlock (obv sample) (6, 6)
[(6,8,3)]
*Main> 

blockwise just takes that result and returns a board which includes those values. Last one:

naiveSolve :: [(Board -> Board)] -> Board -> Board
naiveSolve functions board = rec functions board
  where rec [] board = board
        rec fns board = case Set.size $ empty new of
          0 -> new
          _ -> rec nextFns new
          where new = (head fns) $ board
                nextFns = if new == board then tail fns else functions

I mentioned earlier that the way this works is by trying to repeatedly solve the obvious squares, and resorts to blockwise analysis and guessing only when that doesn't work. This is the part that does the first two. It takes a list of (Board -> Board) functions, and repeatedly calls the first one. If that yields a solved board (one with no empty spaces), it returns that. If that yields an unchanged board, it calls the next function, then repeats that pattern until it runs out of functions to call. The effect is:

*Main> naiveSolve [obvious, blockwise] sample

 71|4 8| 35
   | 53| 87
  3|9 7|6  
-----------
   |  1|   
 9 |8 6|  3
   |  4|82 
-----------
 6 |349|7 8
3  |  2| 9 
   | 85|3 2

*Main> 

Which is a board where the only remaining moves are ones where we need to guess...

guess :: Board -> [Board]
guess board = map (\v -> findEmpties $ board { values = newVals v }) vs
  where (x, y, vs) = head $ sortBy (comparing (length . thd)) posMap
        newVals v = [[if x == x' && y == y' then v else (values board) !! y' !! x' | x' <- ixs board] | y' <- ixs board]
        posMap = [(x, y, toList $ possibilities board (x, y)) | (x, y) <- es]
        es = toList $ empty board

... which is done by picking the space with the fewest number of possibilities, and returning all possible next boards. In other words,

*Main> guess $ naiveSolve [obvious, blockwise] sample
[
 71|4 8| 35
 2 | 53| 87
  3|9 7|6  
-----------
   |  1|   
 9 |8 6|  3
   |  4|82 
-----------
 6 |349|7 8
3  |  2| 9 
   | 85|3 2
,
 71|4 8| 35
 4 | 53| 87
  3|9 7|6  
-----------
   |  1|   
 9 |8 6|  3
   |  4|82 
-----------
 6 |349|7 8
3  |  2| 9 
   | 85|3 2
]
*Main> 

Note space (1, 1) there. Finally, we need to solve that.

solve :: Board -> Board
solve board = rec [naiveSolve [obvious, blockwise] board]
  where solved board = 0 == (Set.size $ empty board)
        impossible board = any ((==0) . length) . map (toList . possibilities board) . toList $ empty board
        rec [] = board -- Failed
        rec !boards = case find solved $ boards of
          Just b -> b
          Nothing -> rec . map (naiveSolve [obvious, blockwise]) . concatMap guess $ filter (not . impossible) boards

That function takes a board, runs naiveSolve on it, and returns it if solved. Otherwise, it repeatedly runs map (naiveSolve [obvious, blockwise]) . concatMap guess on the list of boards that aren't impossible. and there, that solves Sudoku.

*Main> solve sample

971|468|235
624|153|987
853|927|641
-----------
538|291|476
492|876|153
716|534|829
-----------
265|349|718
387|612|594
149|785|362

*Main> 

That particular solution gets returned in under a second, even in GHCi. I mentioned that it works "in the general case". What I mean by that is that it can solve boards which are obvious, and those which require guessing, and those which are non-standard sizes[2]

So there. I'm not at all proud of this because it does fairly poorly on boards that require extensive guessing, even with the -O2 option, and because as I'll show you later today, it's not anywhere near as elegant a solution as you can get.

But first, I need some tea.


Footnotes

1 - [back] - Though still quicker than that bogo-sort solution I described from the actual event.

2 - [back] - Specifically, it handles 4x4, 9x9, 16x16, 25x25, etc. Any board with a block size such that blockSize^2 == boardSize. Most of the solutions both at the Haskellwiki and at Rosetta Code solve 9x9 only. The larger boards obviously take more time and memory to solve.