Year 2015 Day 13
This day exhibits a naive, Vect
based implementation of a ring buffer, as well
as our first introduction to parameters
blocks.
import Data.String
import Data.List1
import Data.List.Lazy
import Data.Vect
import Data.Maybe
import Data.SortedMap.Dependent
import Decidable.Equality
import Util
%default total
Parsing and Data Structures
Name : Type
Name = String
Happiness : Type
Happiness = Integer
Describe a change in happiness from a change in seating arrangement as data structure, indexed by the name of the individual whose happiness it describes, and provide some projections.
data Change : (changee : Name) -> Type where
NextTo : (changee : Name) -> (other : Name) -> (amount : Happiness)
-> Change (changee)
(.changee) : Change changee -> Name
(.changee) (NextTo changee _ _) = changee
(.other) : Change changee -> Name
(.other) (NextTo _ other _) = other
(.amount) : Change changee -> Happiness
(.amount) (NextTo _ _ amount) = amount
Collect the list of changes provided as input into a structure that encodes our assumptions at the type level.
The changes are stored in a in a dependent map, with the name of the individual as the key, and lists of potential changes to their happiness as the values.
This problem is a bit nicer to express in terms of a collection of known size,
and we don't want to be constantly converting the keys list to a Vect
, so we
instead store it in Changes
as a Vect
. We don't want to accidentally store
the wrong thing here, so we store an auto-implicit proof of equality,
keys_prf
, proving that the names
list is exactly the list of keys in
change_map
converted to a Vect with fromList
.
It will also make things a bit nicer if we can assume that our names
list is
non-empty, after all it really doesn't make sense to talk about seating
arrangements at a table with 0 people at it, so we store an auto-implict
nonempty
proof establishing that the length of change_map
's keys list, and
thus names
, is at least 1.
record Changes where
constructor MkChanges
change_map : SortedDMap Name (\n => List (Change n))
names : Vect (length (keys change_map)) Name
{auto keys_prf : names = fromList (keys change_map)}
{auto nonempty : IsSucc (length (keys change_map))}
Our usual pattern-matching based parsing of one element of the input, returning a dependent pair of the name of the individual this record describes, and the change described by that record.
parseChange : Has (Except String) fs =>
String -> Eff fs (name ** Change name)
parseChange str = do
changee ::: [_, direction, amount, _, _, _, _, _, _, other]
<- pure $ split (== ' ') str
| _ => throw "Invalid input string \{str}"
amount <- note "Invalid amount \{amount} in \{str}" $ parseInteger amount
amount : Happiness <-
case direction of
"gain" => pure amount
"lose" => pure $ negate amount
x => throw "Invalid direction \{x} in \{str}"
let other = pack . filter (/= '.') . unpack $ other
pure (_ ** (changee `NextTo` other) amount)
Parse the entire list of changes in the input, collecting them into a dependent
map as we go along, and performing the checks needed for Idris to be satisfied
that the conditions encoded by the auto-implict proofs in Changes
are met.
parseChanges : Has (Except String) fs =>
List String -> (seed : SortedDMap Name (\n => List (Change n)))
-> Eff fs Changes
parseChanges strs seed = do
changes <- traverse parseChange strs
let change_map = insertChanges changes seed
case isItSucc (length (keys change_map)) of
Yes prf => pure $ MkChanges change_map (fromList (keys change_map))
No contra => throw "Empty table, not very interesting"
where
insertChanges : List (name ** Change name)
-> (acc : SortedDMap Name (\n => List (Change n)))
-> SortedDMap Name (\n => List (Change n))
insertChanges [] acc = acc
insertChanges ((name ** change) :: xs) acc =
case lookupPrecise name acc of
Nothing => insertChanges xs (insert name [change] acc)
Just ys => insertChanges xs (insert name (change :: ys) acc)
Solver functions
All of these functions are about to take the same first argument,
(cs : Changes)
. This is a really common occurrence, especially when dealing
with dependent proof types, so Idris has syntax sugar to avoid repeating your
self in theses situations, parameters
blocks1.
A parameters
block adds the provided arguments to the start of every top level
signature contained within it, in this case, making the first argument of all of
these functions have type (cs : Changes)
. The arguments to the parameters
blocks are also added to the front of the arguments list, using the names
provided in the signature.
parameters
blocks also provide another fun bit of functionality that makes
code within them more concise, within a parameters
block, the parameters are
implicitly passed as arguments to calls to functions in the same block.
parameters (cs : Changes)
Calculate the happiness change for a given person in a seating arrangement, use
finS
and unfinS
to get the indexes of the parties seated to either side of
us, and look them up in our map, adding the amount of change described by them
together.
Notice how cs
appears neither in the arguments list, nor the type signature,
yet we can still refer to it as if it was included at the start of both.
happinessFor :
(arrangement : Vect (length (keys cs.change_map)) Name)
-> (idx : Fin (length (keys cs.change_map)))
-> Happiness
happinessFor arrangement idx =
let name = idx `index` arrangement
in case name `lookupPrecise` cs.change_map of
Nothing => 0
Just changes =>
let name_right = (finS idx) `index` arrangement
change_right =
fromMaybe 0 . map (.amount) . find ((== name_right) . (.other)) $
changes
name_left = (unfinS idx) `index` arrangement
change_left =
fromMaybe 0 . map (.amount) . find ((== name_left) . (.other)) $
changes
in change_right + change_left
Calculate the overall happiness change for a given arrangement by mapping our
happinessFor
function over a list of all possible indexes to the arrangement
vect, and summing the results.
Notice how the cs
parameter is implicitly passed to happinessFor
, as we are
inside the same parameters
block as it.
happinessChange :
(arrangement : Vect (length (keys cs.change_map)) Name)
-> Happiness
happinessChange arrangement =
let idxes = List.allFins (length (keys cs.change_map))
changes = map (happinessFor arrangement) idxes
in sum changes
Find the arrangement with the maximum total change in happiness by mapping
happinessChange
over a list of all the possible permutations of our seed
arrangement described by names
, and using maxBy
to identify the largest
positive change in overall happiness.
maxHappiness : Has (Except String) fs =>
Eff fs (Happiness, Vect (length (keys cs.change_map)) Name)
maxHappiness =
let arrangements = permutations cs.names
changes = map happinessChange arrangements
pairs = zip changes arrangements
in case pairs of
[] => throw "No arrangements"
(x :: xs) => pure $ maxBy (compare `on` fst) x xs
Part Functions
Part 1
Parse our input and feed it into our maxHappiness
function.
Notice how, since we are outside the parameters
block, we have to provide the
cs
argument to maxHappiness
explicitly.
part1 : Eff (PartEff String) (Happiness, ())
part1 = do
input <- map lines $ askAt "input"
changes <- parseChanges input empty
(max, arrangement) <- maxHappiness changes
pure (max, ())
Part 2
Our implementation already replaces missing relationships with 0, so we can
cheese this by injecting ourself with an empty relationship list into the
change_map : SortedDMap Name (\n => (List n))
.
The overall Changes
data structure isn't easy to modify, and since our data
set is quite small here, we'll just inject this into parsing and reparse our
data.
part2 : () -> Eff (PartEff String) Happiness
part2 x = do
input <- map lines $ askAt "input"
let seed = insert "ME!!!!" [] empty
changes <- parseChanges input seed
(max, arrangement) <- maxHappiness changes
pure max