daniberg.com

posts github chip8 rtc guitar

Haskell Sudoku solver

This is an alternative Haskell implementation of Solving Every Sudoku Puzzle by Peter Norvig. The source code can be found on github.

The implementation follows function definitions and name conventions of the original post whenever possible. The source code comments can also be found in the python implementation to make it easier the comparison of both implementations.

The first step is to define the main entities: units, peers and squares.

cross :: String -> String -> [String]
cross a b = [ x : y : [] | x <- a, y <- b ]

digits = "123456789"
rows   = "ABCDEFGHI"
cols   = digits

type Square = String
type Digit = Char

-- [
--   "A1","A2","A3","A4","A5","A6","A7","A8","A9",
--   "B1","B2","B3","B4","B5","B6","B7","B8","B9",
--   "C1","C2","C3","C4","C5","C6","C7","C8","C9",
--   "D1","D2","D3","D4","D5","D6","D7","D8","D9",
--   "E1","E2","E3","E4","E5","E6","E7","E8","E9",
--   "F1","F2","F3","F4","F5","F6","F7","F8","F9",
--   "G1","G2","G3","G4","G5","G6","G7","G8","G9",
--   "H1","H2","H3","H4","H5","H6","H7","H8","H9",
--   "I1","I2","I3","I4","I5","I6","I7","I8","I9"
-- ]
squares :: [Square]
squares = cross rows cols

-- [
--   ["A1","B1","C1","D1","E1","F1","G1","H1","I1"],
--   ["A2","B2","C2","D2","E2","F2","G2","H2","I2"],
--   ["A3","B3","C3","D3","E3","F3","G3","H3","I3"],
--   ["A4","B4","C4","D4","E4","F4","G4","H4","I4"],
--   ["A5","B5","C5","D5","E5","F5","G5","H5","I5"],
--   ["A6","B6","C6","D6","E6","F6","G6","H6","I6"],
--   ["A7","B7","C7","D7","E7","F7","G7","H7","I7"],
--   ["A8","B8","C8","D8","E8","F8","G8","H8","I8"],
--   ["A9","B9","C9","D9","E9","F9","G9","H9","I9"],
--
--   ["A1","A2","A3","A4","A5","A6","A7","A8","A9"],
--   ["B1","B2","B3","B4","B5","B6","B7","B8","B9"],
--   ["C1","C2","C3","C4","C5","C6","C7","C8","C9"],
--   ["D1","D2","D3","D4","D5","D6","D7","D8","D9"],
--   ["E1","E2","E3","E4","E5","E6","E7","E8","E9"],
--   ["F1","F2","F3","F4","F5","F6","F7","F8","F9"],
--   ["G1","G2","G3","G4","G5","G6","G7","G8","G9"],
--   ["H1","H2","H3","H4","H5","H6","H7","H8","H9"],
--   ["I1","I2","I3","I4","I5","I6","I7","I8","I9"],
--
--   ["A1","A2","A3","B1","B2","B3","C1","C2","C3"],
--   ["A4","A5","A6","B4","B5","B6","C4","C5","C6"],
--   ["A7","A8","A9","B7","B8","B9","C7","C8","C9"],
--   ["D1","D2","D3","E1","E2","E3","F1","F2","F3"],
--   ["D4","D5","D6","E4","E5","E6","F4","F5","F6"],
--   ["D7","D8","D9","E7","E8","E9","F7","F8","F9"],
--   ["G1","G2","G3","H1","H2","H3","I1","I2","I3"],
--   ["G4","G5","G6","H4","H5","H6","I4","I5","I6"],
--   ["G7","G8","G9","H7","H8","H9","I7","I8","I9"]
-- ]
unitlist :: [[Square]]
unitlist =
  [ cross rows (c:[]) | c <- cols ] ++
  [ cross (r:[]) cols | r <- rows ] ++
  [ cross rs cs | rs <- ["ABC", "DEF", "GHI"], cs <- ["123", "456", "789" ] ]

-- Map where each square is the key and values are lists of units that the
-- square belongs to.
units :: Map Square [[Square]]
units = toMap [(s, u) | s <- squares, u <- unitlist, elem s u]

toMap :: [(Square, [Square])] -> Map Square [[Square]]
toMap xs = foldl addToMap Map.empty xs where
  addToMap m (k, ys) = case Map.lookup k m of
    Just zs -> Map.insert k (ys : zs) m
    Nothing -> Map.insert k [ys] m

-- Map where the each square is the key and the value is a list of the peers
-- which does not include the key. Each square has 20 peers.
peers :: Map Square (Set Square)
peers = Map.mapWithKey f units where
  f k xss = Set.fromList $ filter (\x -> x /= k)(concat xss)

I've introduced the aliases types Square and Digit and while they don't offer stronger guarantees they make the code easier to read.

The squares and unitlist function comments include their values to help readers to understand how the values are generated.

Next are the functions parseGrid and gridValues.

-- Textual representation of the puzzle.
type Grid = String

-- Representation of the puzzle at any state. The key is a Square and the values
-- are a String representing the possible values of the square. If the length of
-- the String is one for all keys the puzzle has been solved.
type GridValues = Map Square String

-- Parse textual representation of the grid.
parseGrid :: Grid -> Maybe GridValues
parseGrid grid = do
  let zero = Just (Map.fromList [(s, digits) | s <- squares])
  xs <- gridValues grid
  let gridValues' = filter (\(_, c) -> c `elem` digits) xs
  foldl (\acc pair -> acc >>= (\m -> assign m pair)) zero gridValues'

-- Convert grid into a dict of {square: char} with '0' or '.' for empties.
-- In our implementation we return a list of pairs and let the caller
-- function convert it into a map.
gridValues :: Grid -> Maybe ([(Square, Char)])
gridValues grid =
  let validChars = '0' : '.' : digits
      chars = [ c | c <- grid, elem c validChars ] in
    if length chars /= 81 then Nothing
    else Just (zip squares chars)

Again, I've introduced type aliases to make the code clearer: Grid and GridValues.

Notice that different from the original python implementation the return type of parseGrid is Maybe GridValues. A contradiction is signaled by returning Nothing.

After that we have the functions assign and eliminate.

-- Constraint propagation
-- 1. If a square has only one possible value, then eliminate that value from the square's peers.
-- 2. If a unit has only one possible place for a value, then put the value there.
assign :: GridValues -> (Square, Digit) -> Maybe GridValues
assign values (s, d) = do
  let otherValues = List.delete d (Maybe.fromMaybe "" (Map.lookup s values))
  foldl eliminate' (Just values) otherValues where
    eliminate' mValues d2 = mValues >>= (\values' -> eliminate values' (s, d2))

-- Eliminate d from values[s]; propagate when values or places <= 2.
-- Return values, except return Nothing if a contradiction is detected.
eliminate :: GridValues -> (Square, Digit) -> Maybe GridValues
eliminate vs (s, d) =
  case Map.lookup s vs of
    -- Our data is messed up
    Nothing -> Nothing
    -- digits as candidates
    Just ds ->
      -- already eliminated if not found
      case List.elemIndex d ds of
        Nothing -> Just vs
        Just _  -> do
          -- remove digit
          let ds' = List.delete d ds
          -- Contradiction if there are zero candidates, otherwise
          -- update the map with the candidate removed
          vs' <- if length ds' == 0 then Nothing
                 else Just $ Map.insert s ds' vs
          -- (1) If a square s is reduced to one value d2, then
          -- eliminate d2 from the peers.
          vs'' <- if length ds' == 1
                  then
                    let d2 = head ds'
                        ss = Maybe.fromMaybe Set.empty (Map.lookup s peers) in
                      -- Short-circuit if any of the eliminate results is Nothing
                      foldl (\mValues s2 -> do
                        values <- mValues
                        eliminate values (s2, d2)) (Just vs') ss
                  else Just vs'
          -- (2) If a unit u is reduced to only one place for a valud d,
          -- then put it here.
          uss   <- Map.lookup s units
          vs''' <- foldl
            (\acc us -> do
                acc' <- acc
                let dplaces = [s | s <- us, d `elem` (Maybe.fromMaybe [] $ Map.lookup s acc')] in
                  case length dplaces of
                    0 -> Nothing -- Contradiction: no place for this value
                    1 -> assign acc' ((head dplaces), d)
                    _ -> acc) (Just vs'') uss
          return vs'''

Again, both functions rely on the Maybe data structure.

The display function is not that interesting.

-- Display these values as 2D grid.
display :: GridValues -> IO ()
display values = do
  let width = 1 + maximum [ Maybe.fromMaybe 0 $
                            fmap length $
                            Map.lookup s values | s <- squares]
      line  = List.intercalate "+" $ replicate 3 $ replicate (width * 3) '-'
      table = [ buildCell r c | r <- rows, c <- cols] where
        buildCell r c =
          let vs  = Maybe.fromMaybe "" $ Map.lookup (r : c : []) values
              pre = replicate (width - length vs) ' '
              pos = if c `elem` "36" then " |" else if c == '9' then "\n" else ""
              ln  = if r `elem` "CF" && c == '9' then (line ++ "\n") else "" in
            pre ++ vs ++ pos ++ ln
  putStrLn $ List.concat table

Finally, we have the search and solve function.

search :: GridValues -> Maybe GridValues
search values =
  let xs            = Map.toList values
      allSizeOne    = List.all (\(_, vs) -> length vs == 1) xs
      -- Choose the infilled square s with the fewest possibilities
      ys            = filter (\(_, ds) -> length ds > 1) xs
      (s, ds)       = List.minimumBy (\(s1, v1) (s2, v2) ->
                                        compare (length v1) (length v2)) ys
      assignments   = fmap (\d -> assign values (s, d) >>= search) ds in
    if allSizeOne then Just values -- Solved
    else join $ List.find (Maybe.isJust) assignments

solve :: Grid -> Maybe GridValues
solve grid = parseGrid grid >>= search

Instructions on how to compile and run the code can be found on github.

©2023 daniberg.com