Liyang/sudoku.hs
Jump to navigation
Jump to search
It's slow. That makes me sad.
sudoku.hs
{-# OPTIONS -cpp -fglasgow-exts -fno-monomorphism-restriction #-}
{- -}
#define EXHAUSTIVE 0
#define FORK_WORKERS 1
#define FORK_GUESSES 2
#define FORK_OS 4
#define FLAG(flag) (1 << flag)
#define OPTION(flag) (OPTIONS & FLAG(flag))
#ifndef OPTIONS
# define OPTIONS ( FLAG(FORK_WORKERS) )
{- |FLAG(FORK_GUESSES) |FLAG(EXHAUSTIVE) -}
#endif
module Main where
import Prelude hiding ( all, any, concat, elem, foldl, pi )
import Control.Arrow ( (***), (&&&) )
import Control.Applicative
import Control.Monad
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Data.Bits
import Data.Char
import Data.List ( intersperse, unfoldr )
import Data.Maybe
import Data.Monoid
import Data.Foldable
import Data.Traversable
import Data.Ix
--{{{ Tensor/Cartesian product on collections.
tensor :: (Applicative f, Applicative g) => (alpha -> beta -> gamma) -> f alpha -> g beta -> f (g gamma)
tensor f a b = (\ x -> f <$> pure x <*> b) <$> a
--}}}
--{{{ Indexable collections.
class Indexable f i | f -> i where
indexModify :: i -> (alpha -> (beta, alpha)) -> f alpha -> (beta, f alpha)
-- Sometimes we only want to index.
pi :: Indexable f i => i -> f alpha -> alpha
pi i = fst . indexModify i (id &&& id)
-- Or to just modify.
modify :: Indexable f i => i -> (alpha -> alpha) -> (f alpha -> f alpha)
modify i f = snd . indexModify i (id &&& f)
--}}}
--{{{ Three is the magic number: base three ordinals.
data Ti = T0 | T1 | T2 deriving (Show, Read, Bounded, Eq, Enum, Ord, Ix)
type TiTi = (Ti, Ti)
type Coord = (TiTi, TiTi)
boundedSize :: (Bounded alpha, Ix alpha) => alpha -> Int
boundedSize = rangeSize . (m0 &&& m1) where
m0, m1 :: Bounded alpha => alpha -> alpha
m0 w = minBound; m1 w = maxBound
fromPair :: (Bounded alpha, Ix alpha, Enum alpha) =>
(beta -> Int) -> (beta, alpha) -> Int
fromPair f (y, x) = boundedSize x * f y + fromEnum x
toPair :: (Bounded alpha, Ix alpha, Enum alpha) =>
(Int -> beta) -> Int -> (beta, alpha)
toPair f n = result where -- this is sick:
result = (f *** toEnum) (n `divMod` boundedSize (snd result))
instance
( Bounded alpha, Ix alpha, Enum alpha
, Bounded beta, Ix beta, Enum beta )
=> Enum (beta, alpha) where
fromEnum = fromPair fromEnum
toEnum = toPair toEnum
--}}}
--{{{ Triple: 3.
data Triple alpha = T alpha alpha alpha deriving (Show, Bounded, Eq, Ord, Ix)
instance Functor Triple where
fmap = fmapDefault
instance Foldable Triple where
foldMap = foldMapDefault
instance Traversable Triple where
traverse f (T x y z) = T <$> f x <*> f y <*> f z
iTriple = T T0 T1 T2
instance Indexable Triple Ti where
indexModify i f (T a b c) = case i of
T0 -> (x, T a' b c ) where (x, a') = f a
T1 -> (x, T a b' c ) where (x, b') = f b
T2 -> (x, T a b c' ) where (x, c') = f c
instance Applicative Triple where
pure x = T x x x
T f g h <*> T x y z = T (f x) (g y) (h z)
instance (Bounded alpha, Ix alpha, Enum alpha) => Enum (Triple alpha) where
fromEnum (T a b c) = (fromPair . fromPair) fromEnum ((a, b), c)
toEnum n = T a b c where ((a, b), c) = (toPair . toPair) toEnum n
instance Read alpha => Read (Triple alpha) where
readsPrec _ s = [ (T a b c, v) |
(a, t) <- reads s, (b, u) <- reads t, (c, v) <- reads u ]
showTriple :: String -> (alpha -> String) -> Triple alpha -> String
showTriple d s = concat . intersperse d . toList . fmap s
--}}}
--{{{ Region: 3x3; aka rows, cols, boxes, cell constraints... whatever.
newtype Region alpha = Region { unRegion :: Triple (Triple alpha) }
deriving Eq
instance Functor Region where
fmap = fmapDefault
instance Foldable Region where
foldMap = foldMapDefault
instance Traversable Region where
traverse f (Region tt) = Region <$> traverse (traverse f) tt
iRegion = Region (tensor (,) iTriple iTriple)
instance Indexable Region TiTi where
indexModify (j, i) f (Region tt) = (id *** Region) $
indexModify j (indexModify i f) tt
instance Applicative Region where
pure = Region . pure . pure
Region ttf <*> Region ttx = Region ((<*>) <$> ttf <*> ttx)
instance Read alpha => Read (Region alpha) where
readsPrec p s = (Region *** id) <$> readsPrec p s
instance Show alpha => Show (Region alpha) where
showsPrec _ = (++) . showTriple " " (showTriple " " show) . unRegion
--}}}
--{{{ Cell: 3x3; different Read/Show, but otherwise identical to Regions.
newtype Cell alpha = Cell { unCell :: Region alpha }
deriving Eq
instance Functor Cell where
fmap = fmapDefault
instance Foldable Cell where
foldMap = foldMapDefault
instance Traversable Cell where
traverse f (Cell r) = Cell <$> traverse f r
iCell = Cell iRegion
instance Indexable Cell TiTi where
indexModify ji f (Cell r) = (id *** Cell) (indexModify ji f r)
instance Applicative Cell where
pure = Cell . pure
Cell rf <*> Cell rx = Cell (rf <*> rx)
instance Read (Cell Bool) where
readsPrec _ s = case dropWhile isSpace s of
'{' : '-' : c : '-' : '}' : t -> reads (c : t)
'{' : a : b : c : '}' : t | all isOctDigit mask ->
[(partialCell, t)] where
mask = T a b c
partialCell = (Cell . Region . fmap (toEnum . digitToInt)) mask
c : t | c `elem` ".0_-?" ->
[(emptyCell, t)] where
emptyCell = (Cell . Region . pure . pure) True
c : t | c >= '1' && c <= '9' ->
[(fullCell, t)] where
fullCell = (Cell . Region . toEnum . (2 ^) . (8 -) . pred . read) [c]
_ -> []
instance Show (Cell Bool) where
show cell@(Cell (Region tt)) = case solutions cell of
[n] -> "{-" ++ show (fromEnum n + 1) ++ "-}"
_ -> "{" ++ showTriple "" (show . fromEnum) tt ++ "}"
--}}}
--{{{ Grid: 3x3 x 3x3
newtype Grid alpha = Grid { unGrid :: Region (Region alpha) }
deriving Eq
instance Functor Grid where
fmap = fmapDefault
instance Foldable Grid where
foldMap = foldMapDefault
instance Traversable Grid where
traverse f (Grid rr) = Grid <$> traverse (traverse f) rr
iGrid = Grid (tensor (,) iRegion iRegion)
instance Indexable Grid Coord where
indexModify (lk, ji) f (Grid rr) = (id *** Grid) $
indexModify lk (indexModify ji f) rr
instance Applicative Grid where
pure = Grid . pure . pure
Grid rrf <*> Grid rrx = Grid ((<*>) <$> rrf <*> rrx)
instance Read alpha => Read (Grid alpha) where
readsPrec p s = (Grid *** id) <$> readsPrec p s
instance Show alpha => Show (Grid alpha) where
show = showTriple "\n\n" (showTriple "\n" show) . unRegion . unGrid where
--}}}
--{{{ Cube: 3x3 x 3x3 x 3x3; grids with cell constraints.
newtype Cube alpha = Cube { unCube :: Grid (Cell alpha) }
deriving Eq
instance Functor Cube where
fmap = fmapDefault
instance Foldable Cube where
foldMap = foldMapDefault
instance Traversable Cube where
traverse f (Cube gc) = Cube <$> traverse (traverse f) gc
instance Applicative Cube where
pure = Cube . pure . pure
Cube gcf <*> Cube gcx = Cube ((<*>) <$> gcf <*> gcx)
instance Read (Cube Bool) where
readsPrec p s = (Cube *** id) <$> readsPrec p s
instance Show (Cube Bool) where
show = show . unCube
--}}}
--{{{ views: returns alternative representations of the grid.
type Auto alpha = alpha -> alpha
type Rank6 t alpha = t (t (t (t (t (t alpha)))))
views :: Cube alpha -> Region (Cube alpha)
views cube = (assemble . ($ disassemble cube)) <$> mkViews where
mkViews :: (Applicative t, Traversable t) => Region (Auto (Rank6 t alpha))
mkViews = (fmap traversals . Region) $ T
-- 0 1 2 3 4 5
-- J I l k n m -- row
-- l k J I n m -- col
-- l k n m J I -- bit
(T [0,1] [2,3] [4,5])
-- J l I k n m -- box
-- l k J n I m -- ?
-- I l k n J m -- ?
(T [0,2] [2,4] [4,0])
-- l J k I n m -- ?
-- l k n J m I -- ?
-- l I k n m J -- ?
(T [1,3] [3,5] [5,1])
disassemble :: Cube alpha -> Rank6 Triple alpha
disassemble = unRegion . fmap unRegion . unGrid . fmap (unRegion . unCell) . unCube
assemble :: Rank6 Triple alpha -> Cube alpha
assemble = Cube . fmap (Cell . Region) . Grid . fmap Region . Region
traversals :: (Applicative t, Traversable t) => [Int] -> Auto (Rank6 t alpha)
traversals = appEndo . fold . fmap (trs !!) . reverse . reindex where
trs :: (Applicative t, Traversable t) => [Endo (Rank6 t alpha)]
trs = Endo <$> [tr0, tr1, tr2, tr3, tr4, tr5] where
tr0 = id :: (Applicative t, Traversable t) => Auto (t alpha)
tr1 = traverse tr0 :: (Applicative t, Traversable t) => Auto (t (t alpha))
tr2 = traverse tr1 :: (Applicative t, Traversable t) => Auto (t (t (t alpha)))
tr3 = traverse tr2 :: (Applicative t, Traversable t) => Auto (t (t (t (t alpha))))
tr4 = traverse tr3 :: (Applicative t, Traversable t) => Auto (t (t (t (t (t alpha)))))
tr5 = traverse tr4 :: (Applicative t, Traversable t) => Auto (t (t (t (t (t (t alpha))))))
-- Calculate the traversals needed to obtain the required reindexing.
-- Not always the most efficient in terms of operations needed...
reindex :: [Int] -> [Int]
reindex = unfoldr shifts . reverse where
shifts [] = Nothing
shifts (0:t) = shifts t
shifts (1:0:t) = shifts t
shifts (h:t) = Just (h, flip fmap t $ \ d -> if h > d then d + 1 else d)
-- Just the rows, columns and boxes plzkthx.
decompose :: Cube alpha -> Triple (Cube alpha)
decompose grid = flip pi vs <$> T (T0, T0) (T0, T1) (T1, T0) where
vs = views grid
-- Select the relevalt row/col/box, given an (y, x) offset.
select :: Triple (Cube alpha) -> Coord -> Triple (Region (Cell alpha))
select rcbs ((l, k), (j, i)) = pi <$>
T (l, k) (j, i) (l, j) <*> (unGrid . unCube <$> rcbs)
--}}}
--{{{ solutions: produce a list of candidates, given a cell.
solutions :: Cell Bool -> [TiTi]
solutions = map fst . filter snd . (fmap (,) iCell <*>)
--}}}
--{{{ collapse: locate first non-solved cell with fewest possibilities and collapse it down.
collapse :: (Functor f, Foldable f, Enum i, Indexable f i) =>
f (Bool, Cell Bool) -> Maybe [f (Cell Bool)]
collapse cells = makeGrids <$> collapseMin cells where
makeGrids (sols, coord) =
[ modify coord (const cell) (snd <$> cells) | cell <- sols ]
collapseMin :: (Functor f, Foldable f, Enum i, Indexable f i) =>
f (Bool, Cell Bool) -> Maybe ([Cell Bool], i)
collapseMin = uncurry (fmap . const . fmap toEnum) . snd .
foldl minCell (0, (undefined, Nothing)) . fmap collapseCell where
-- undefined? Madness! But I want Ord (Maybe Int) on length and the
-- (fmap . const . fmap . toEnum) ensures the insanity never escapes
-- (counter :: Int, ((solutions :: [Cell Bool], index :: Int), length :: Maybe Int))
minCell :: (Int, (([Cell Bool], Int), Maybe Int)) ->
Maybe [Cell Bool] -> (Int, (([Cell Bool], Int), Maybe Int))
minCell (i, min@(_, lenThat)) = (,) (succ i) . maybe min choose where
choose this | lenThis > lenThat = ((this, i), lenThis)
| otherwise = min
where
-- |Ord alpha => Ord (Maybe alpha)| considers Nothing to be $\bot$,
-- rather than $\top$. We get the latter by flipping the ordering
-- on |alpha| (and swapping |(<)| with |(>)|), hence the |negate|.
lenThis = case length this of
1 -> Nothing
n -> Just (negate n) -- zero slips through!
collapseCell :: (Bool, Cell Bool) -> Maybe [Cell Bool]
collapseCell (True, cell) = Nothing
collapseCell (False, cell) =
Just (flip pi masks <$> solutions cell) where
masks = Cell <$> tensor (==) iRegion iRegion
--}}}
--{{{ Software Transactional Memory; forking.
instance Applicative STM where
pure = return
(<*>) = ap
type TBool = TVar Bool
#if FORK_OS
fork = forkOS
#else
fork = forkIO
#endif
--}}}
--{{{ eliminator: mark non-solutions from cells in the same row/col/box.
type Eliminator = TiTi -> STM ()
-- Make an eliminator, given a cell and its associated row/col/box.
-- Be careful not to mark the solution itself though.
eliminator :: Cell TBool -> Triple (Region (Cell TBool)) -> Eliminator
eliminator solution rcb n = elimRCB ((pi n <$>) <$> rcb) where
elimRCB :: Triple (Region TBool) -> STM ()
elimRCB = traverse_ . traverse_ $ \ tflag ->
unless (pi n solution == tflag) $ do
flag <- readTVar tflag
when flag (writeTVar tflag False)
--}}}
--{{{ worker: check each cell for changes in constraints.
worker :: TBool -> Cell TBool -> Eliminator -> STM ()
worker tsolved tcell elim = do
cell <- traverse readTVar tcell
case solutions cell of
[] -> do -- Bad End.
#if OPTION(FORK_WORKERS)
return ()
#else
retry
#endif
[n] -> do elim n; writeTVar tsolved True
_ -> retry
--}}}
--{{{ launchMissiles, waitMissiles: constraint propagation.
type State alpha = (Grid alpha, Cube alpha)
#if OPTION(FORK_WORKERS)
waitMissiles :: State TBool -> STM ()
waitMissiles (tsolved, Cube tgrid) = do
let boo tmarked tcell = do
marked <- readTVar tmarked
cell <- solutions <$> traverse readTVar tcell
case cell of
[] -> return False
[_] -> do unless marked retry; return True
_ -> return True
let elseThen _ t True = t
elseThen e _ False = e
foldrM (elseThen (return False)) True (boo <$> tsolved <*> tgrid :: Grid (STM Bool))
return ()
#endif
launchMissiles :: State Bool -> IO (State Bool)
launchMissiles (solved, cube) = do
tsolved <- atomically $ traverse newTVar solved
tcube@(Cube tgrid) <- atomically $ traverse newTVar cube
let tcubes = decompose tcube :: Triple (Cube TBool)
let elims = {-# SCC "elims" #-} eliminator <$> tgrid <*> (select tcubes <$> iGrid) :: Grid Eliminator
let workers = {-# SCC "workers" #-} worker <$> tsolved <*> tgrid <*> elims :: Grid (STM ())
#if OPTION(FORK_WORKERS)
let whip True _ = {-# SCC "whip" #-} return Nothing
whip False w = Just <$> fork (atomically w)
tids <- {-# SCC "whipping" #-} sequenceA (whip <$> solved <*> workers)
-- Wait for all the missiles to hit their target. Or any to malfunction.
atomically (waitMissiles (tsolved, tcube))
traverse_ (traverse_ killThread) tids
#else
tdone <- atomically (newTVar False)
let whip rest (tdone, w) = do
done <- readTVar tdone
if done then rest
else w `orElse` rest
let loop = do
atomically (foldl whip (writeTVar tdone True) ((,) <$> tsolved <*> workers))
done <- atomically (readTVar tdone)
unless done loop
loop
#endif
cube' <- atomically (traverse readTVar tcube)
solved' <- atomically (traverse readTVar tsolved)
return (solved', cube')
--}}}
--{{{ mad: make guesses.
mad :: State Bool -> IO Bool
mad boo = do
(solved', cube') <- launchMissiles boo
case fmap Cube <$> collapse ((,) <$> solved' <*> unCube cube') of
Nothing -> do
print cube'
return True
Just next -> case next of
[] -> return False -- Bad End.
_ -> do
#if OPTION(FORK_GUESSES)
children <- for next $ \ c -> do
sem <- atomically (newTVar Nothing)
tid <- fork $ mad (solved', c) >>= atomically . writeTVar sem . Just
return (sem, tid)
let wait = do
done <- atomically (traverse (readTVar . fst) children)
# if OPTION(EXHAUSTIVE)
if any isNothing done
then wait
else return (any (fromMaybe False) done)
# else
if any (fromMaybe False) done
then do
traverse_ (killThread . snd) children
return True
else if case any isNothing done
then wait
else return False
# endif
wait
#else
# if OPTION(EXHAUSTIVE)
traverse_ (mad . (,) solved') next
return True -- don't care
# else
let cow True _ = return True
cow False c = mad (solved', c)
foldlM cow False next
# endif
#endif
--}}}
--{{{ go, main: solver entry point.
go :: Cube Bool -> IO Bool
go = mad . (,) (pure False)
main :: IO ()
main = do
go . read =<< getContents
return ()
--}}}
--{{{ easy, gentle, diabolical, unsolvable, minimal :: Cube Bool
easy, gentle, diabolical, unsolvable, minimal :: Cube Bool
easy = read "\
2....1.38\
........5\
.7...6...\
.......13\
.981..257\
31....8..\
9..8...2.\
.5..69784\
4..25...."
gentle = read "\
.1.42...5\
..2.71.39\
.......4.\
2.71....6\
....4....\
6....74.3\
.7.......\
12.73.5..\
3...82.7."
diabolical = read "\
.9.7..86.\
.31..5.2.\
8.6......\
..7.5...6\
...3.7...\
5...1.7..\
......1.9\
.2.6..35.\
.54..8.7."
unsolvable = read "\
1..9.7..3\
.8.....7.\
..9...6..\
..72.94..\
41.....95\
..85.43..\
..3...7..\
.5.....4.\
2..8.6..9"
minimal = read "\
.98......\
....7....\
....15...\
1........\
...2....9\
...9.6.82\
.......3.\
5.1......\
...4...2."
--}}}
Makefile
TARGETS := sudoku HSFLAGS := $(DEFINES) -O3 .PHONY: all alternatives all: $(TARGETS) %: %.hs ghc $(HSFLAGS) -threaded -o $@ --make $< %: %.lhs ghc $(HSFLAGS) -threaded -o $@ --make $< profile-%: %.hs ghc $(HSFLAGS) -prof -auto-all -o $@ --make $< profile-%.prof: profile-% ./$< +RTS -p -i0.02 -RTS .PHONY: clean clean: rm -f $(TARGETS:=.o) $(TARGETS:=.hi) $(TARGETS)
build-alternatives
#! /bin/bash NAME=sudoku for ((i = 0; i < 16; i++)) ; do EXT="" [ "$(($i & 1))" = "0" ] || EXT="$EXT-exhaustive" [ "$(($i & 2))" = "0" ] || EXT="$EXT-fork_workers" [ "$(($i & 4))" = "0" ] || EXT="$EXT-fork_guesses" [ "$(($i & 8))" = "0" ] || EXT="$EXT-forkOS" EXT="${EXT:--boring}" make clean make DEFINES="-DOPTIONS=$i" "$NAME" mv "$NAME" "$NAME$EXT" done