1 -- Writeup at https://work.njae.me.uk/2023/01/02/optimising-haskell-example-2/
9 import Control.Monad.State.Strict
10 import Control.Monad.ST
11 import qualified Data.Array.IArray as A
12 import qualified Data.Array.MArray as M
17 type Position = V2 Int -- x, y
19 data Direction = North | South | West | East
20 deriving (Show, Eq, Ord, Enum, Bounded)
22 newtype Elf = Elf Position
23 deriving (Eq, Ord, Show)
25 type Population = A.Array Position (Maybe Elf)
27 type MPopulation s = STArray s Position (Maybe Elf)
28 type MClashCounts s = STArray s Position Int
30 data Grove = Grove { currentGrove :: Population, proposalDirections :: [Direction], elapsedRounds :: Int}
33 instance Show Grove where
34 show grove = (showElves $ currentGrove grove) ++ ", " ++ (show $ take 4 $ proposalDirections grove) ++ ", e = " ++ (show $ elapsedRounds grove)
35 where showElves g = "Grove " ++ (show $ A.bounds g) ++ " " ++ (show $ filter (isJust . snd) $ A.assocs g)
37 type GroveState = State Grove
41 do dataFileName <- getDataFileName
42 text <- readFile dataFileName
43 let grove = Grove (mkGrove text) (cycle [North .. East]) 0
45 -- print $ runState simulateOnce grove
46 -- print $ execState (simulateN 4) grove
50 part1, part2 :: Grove -> Int
51 part1 grove = countEmpty grove' bounds
52 where grove' = currentGrove $ execState (simulateN 10) grove
53 bounds = findBounds grove'
55 part2 grove = elapsedRounds grove'
56 where grove' = execState simulateToCompletion grove
59 simulateToCompletion, simulateOnce, growGrove, updateDirections, updateCount :: GroveState ()
61 simulateToCompletion =
62 do oldGrove <- gets currentGrove
64 newGrove <- gets currentGrove
65 if oldGrove == newGrove
67 else simulateToCompletion
69 simulateN :: Int -> GroveState ()
70 simulateN 0 = return ()
76 do grove <- gets currentGrove
77 proposalsInf <- gets proposalDirections
78 let proposals = take 4 proposalsInf
81 do mPopulation <- M.thaw grove
82 mCounts <- M.mapArray (const 0) mPopulation
83 proposeMoves mPopulation mCounts proposals
84 removeClashes mPopulation mCounts
87 modify' (\g -> g { currentGrove = newGrove})
93 do grove <- gets currentGrove
94 let (b0, b1) = findBounds grove
95 let bounds' = (b0 ^+^ (V2 -1 -1), b1 ^+^ (V2 1 1))
96 let grove' = A.accumArray (flip const) Nothing bounds' $ filter ((inRange bounds') . fst ) $ A.assocs grove
97 modify' (\g -> g { currentGrove = grove'})
99 updateDirections = modify' (\g -> g { proposalDirections = tail (proposalDirections g)})
100 updateCount = modify' (\g -> g { elapsedRounds = (elapsedRounds g) + 1})
102 -- position changing utilities
104 anyNeighbour :: [Position]
105 anyNeighbour = [ V2 dx dy
108 , not ((dx == 0) && (dy == 0))
111 directionNeighbour :: Direction -> [Position]
112 directionNeighbour North = filter (\(V2 _x y) -> y == 1) anyNeighbour
113 directionNeighbour South = filter (\(V2 _x y) -> y == -1) anyNeighbour
114 directionNeighbour West = filter (\(V2 x _y) -> x == -1) anyNeighbour
115 directionNeighbour East = filter (\(V2 x _y) -> x == 1) anyNeighbour
117 stepDelta :: Direction -> Position
118 stepDelta North = V2 0 1
119 stepDelta South = V2 0 -1
120 stepDelta West = V2 -1 0
121 stepDelta East = V2 1 0
123 noElves :: MPopulation s -> [Position] -> ST s Bool
124 noElves elves tests =
125 do others <- mapM (M.readArray elves) tests
126 return $ all isNothing others
128 isolated :: MPopulation s -> Position -> ST s Bool
129 isolated elves here = noElves elves $ fmap (here ^+^) anyNeighbour
131 -- get elves to make proposals
133 proposeMoves :: MPopulation s -> MClashCounts s -> [Direction] -> ST s ()
134 proposeMoves mPopulation mCounts proposals =
135 do assocs <- M.getAssocs mPopulation
136 mapM_ (makeProposal mPopulation mCounts proposals) assocs
138 makeProposal :: MPopulation s -> MClashCounts s -> [Direction] -> (Position, Maybe Elf) -> ST s ()
139 makeProposal elves clashes directions (here, elf)
140 | isNothing elf = return ()
141 | otherwise = do isIsolated <- isolated elves here
143 do proposals <- mapM (proposedStep elves here) directions
144 let step = fromMaybe (V2 0 0) $ getFirst $ mconcat $ fmap First proposals
145 let there = here ^+^ step
146 thereCount <- M.readArray clashes there
147 M.writeArray clashes there (thereCount + 1)
148 M.writeArray elves here (Just (Elf there))
150 proposedStep :: MPopulation s -> Position -> Direction -> ST s (Maybe Position)
151 proposedStep elves here direction =
152 do isFree <- noElves elves interfering
154 then return $ Just $ stepDelta direction
156 where interfering = fmap (here ^+^) $ directionNeighbour direction
158 -- find clashing elves and prevent them moving
160 removeClashes :: MPopulation s -> MClashCounts s -> ST s ()
161 removeClashes elves counts =
162 do cts <- M.getAssocs counts
163 let clashes = fmap fst $ filter ((> 1) . snd) cts
164 stopClashingElves clashes elves
166 stopClashingElves :: [Position] -> MPopulation s -> ST s ()
167 stopClashingElves clashes elves = mapM_ stopClash targets
168 where targets = concatMap findNbrs clashes
169 findNbrs c = fmap (^+^ c) $ fmap stepDelta [North .. East]
171 do target <- M.readArray elves here
172 when (isJust target) $ M.writeArray elves here (Just (Elf here))
176 moveElves :: MPopulation s -> ST s ()
178 do assocs <- M.getAssocs elves
179 mapM_ (moveElf elves) assocs
181 moveElf :: MPopulation s -> (Position, Maybe Elf) -> ST s ()
182 moveElf _elves (_here, Nothing) = return ()
183 moveElf elves (here, Just (Elf there)) =
184 do M.writeArray elves here Nothing
185 M.writeArray elves there (Just (Elf there))
187 -- reset the array bounds
189 findBounds :: Population -> (Position, Position)
190 findBounds grove = boundsR
191 where bounds0 = A.bounds grove
192 boundsT = shrink grove topStrip topShrink bounds0
193 boundsB = shrink grove bottomStrip bottomShrink boundsT
194 boundsL = shrink grove leftStrip leftShrink boundsB
195 boundsR = shrink grove rightStrip rightShrink boundsL
198 -> ((Position, Position) -> (Position, Position))
199 -> (Position, Position)
200 -> (Position, Position)
201 -> (Position, Position)
202 shrink grove findStrip stripDirection currentBounds
203 | emptyStrip grove (findStrip currentBounds) =
204 shrink grove findStrip stripDirection (shiftBounds currentBounds stripDirection)
205 | otherwise = currentBounds
206 where shiftBounds (b0, b1) (d0, d1) = (b0 ^+^ d0, b1 ^+^ d1)
208 emptyStrip :: Population -> (Position, Position) -> Bool
209 emptyStrip grove strip = all isNothing $ fmap (grove A.!) $ range strip
211 topStrip, bottomStrip, leftStrip, rightStrip :: (Position, Position) -> (Position, Position)
212 topStrip (V2 minX _minY, V2 maxX maxY) = (V2 minX maxY, V2 maxX maxY)
213 bottomStrip (V2 minX minY, V2 maxX _maxY) = (V2 minX minY, V2 maxX minY)
214 leftStrip (V2 minX minY, V2 _maxX maxY) = (V2 minX minY, V2 minX maxY)
215 rightStrip (V2 _minX minY, V2 maxX maxY) = (V2 maxX minY, V2 maxX maxY)
217 topShrink, bottomShrink, leftShrink, rightShrink :: (Position, Position)
218 topShrink = (V2 0 0, V2 0 -1)
219 bottomShrink = (V2 0 1, V2 0 0)
220 leftShrink = (V2 1 0, V2 0 0)
221 rightShrink = (V2 0 0, V2 -1 0)
223 countEmpty :: Population -> (Position, Position) -> Int
224 countEmpty grove bounds = length $ filter isNothing $ fmap (grove A.!) cells
225 where cells = range bounds
227 -- Parse the input file
229 mkGrove :: String -> Population
230 mkGrove text = A.accumArray
233 (V2 -1 -1, V2 maxX maxY)
234 [ mkElf x y -- Elf (V2 x y) (V2 x y)
235 | x <- [0..(maxX - 1)], y <- [0..(maxY - 1)]
238 where rows = reverse $ lines text
240 maxX = (length $ head rows)
242 | ((rows !! y) !! x) == '#' = ((V2 x y), Just ( Elf (V2 x y) ))
243 | otherwise = ((V2 x y), Nothing)