Year 2015 Day 7
This day provides us a gentle introduction to dependent maps.
Per the problem specification, each wire can only be output to by one gate. To
encode this property in the type, we'll tag Gate
with the output wire in the
type, and then store our ciruct as a dependent map from wires to Gate
s. This
ensures that the circut only contains one gate outputting to each wire, and that
looking up a wire in the ciruct produces exactly the gate that outputs to it
through type checking.
Ensuring that the input contains only one gate outputting for each wire is done through throwing a runtime error in the parsing function if a second gate outputting to a given wire is found in the input.
import Data.Bits
import Data.Fin
import Data.String
import Data.List1
import Data.SortedMap
import Data.SortedMap.Dependent
import Decidable.Equality
Parsing and data structures
Problem structure
Define type aliases for wires and literals, and specify that an input can be either a literal or a wire.
Wire : Type
Wire = String
Literal : Type
Literal = Bits16
Input : Type
Input = Either Literal Wire
Description of a Gate, tagged in the type with the name of the output wire.
This creates what is referred to as an "indexed type family", in this case a
family of Gate
types indexed by values of type Wire
.
data Gate : Wire -> Type where
Constant : (x : Input) -> {output : Wire} -> Gate output
And : (x, y : Input) -> {output : Wire} -> Gate output
Or : (x, y : Input) -> {output : Wire} -> Gate output
Not : (x : Input) -> {output : Wire} -> Gate output
LShift : (x : Input) -> (y : Index {a = Literal}) -> {output : Wire}
-> Gate output
RShift : (x : Input) -> (y : Index {a = Literal}) -> {output : Wire}
-> Gate output
Parsing functions
Parse a shift value, making sure its bounded properly to index a Bits16
parseShift : Has (Except String) fs => String -> Eff fs (Index {a = Literal})
parseShift str =
note "Invalid shift: \{str}" $ parsePositive str
Make sure that this string consists only of lowercase letters
parseWire : Has (Except String) fs => String -> Eff fs Wire
parseWire str =
if all isLower (unpack str)
then pure str
else throw "Invalid wire \{str}"
Parse either a wire or a literal
parseInput : Has (Except String) fs => String -> Eff fs Input
parseInput str =
case parsePositive str of
Nothing => map Right . parseWire $ str
Just x => pure $ Left x
Split the string into components and pattern match on them to extract a gate
parseGate : Has (Except String) fs => Has Logger fs =>
String -> Eff fs (wire : Wire ** Gate wire)
parseGate str = do
debug "Parsing gate: \{str}"
case split (== ' ') str of
x ::: ["->", output ] => do
x <- parseInput x
output <- parseWire output
pure (output ** Constant x)
"NOT" ::: [x, "->", output] => do
x <- parseInput x
output <- parseWire output
pure (output ** Not x)
x ::: ["AND", y, "->", output] => do
x <- parseInput x
y <- parseInput y
output <- parseWire output
pure (output ** And x y)
x ::: ["OR", y, "->", output] => do
x <- parseInput x
y <- parseInput y
output <- parseWire output
pure (output ** Or x y)
x ::: ["RSHIFT", y, "->", output] => do
x <- parseInput x
y <- parseShift y
output <- parseWire output
pure (output ** RShift x y)
x ::: ["LSHIFT", y, "->", output] => do
x <- parseInput x
y <- parseShift y
output <- parseWire output
pure (output ** LShift x y)
_ => throw "Invalid Gate: \{str}"
Break the input into lines, traverse parseGate
over the lines, and collect the
results into our circut DMap.
The type of a value in a SortedDMap
depends on the value of the key that
refers to it, in the type constructor SortedDMap k v
, v
is of type
k -> Type
, this is the function the map calls to calculate the type of the
value from the value of the key. This, not so coincidentally, is the type
signature of our Gate
type constructor, which we can slot in here to get a map
from a wire to the gate outputting to that wire, preventing us from accidentally
associating a wire to the wrong gate.
parseGates : Has (Except String) fs => Has Logger fs =>
String -> Eff fs (SortedDMap Wire Gate)
parseGates input = do
gates : List (wire : Wire ** Gate wire) <-
traverse parseGate . lines . trim $ input
foldlM insertGate empty gates
where
insertGate : SortedDMap Wire Gate -> (wire : Wire ** Gate wire)
-> Eff fs $ SortedDMap Wire Gate
insertGate map (wire ** gate) =
if isJust $ lookup wire map
then throw "Duplicate gate for \{wire}"
else pure $ insert wire gate map
Solver Functions
Recursively solve for the value on a wire, keeping a cache of already solved wires
covering
solveWire : Has (Except String) fs => Has (State (SortedMap Wire Literal)) fs =>
Has (Reader (SortedDMap Wire Gate)) fs =>
(wire : Wire) -> Eff fs Literal
solveWire wire = do
-- Short circut if we are already in the cache
Nothing <- map (lookup wire) get
| Just cached => pure cached
-- Find the gate that outputs to this wire in our circut
Just gate <- map (lookupPrecise wire) ask
| _ => throw "No output for wire \{wire}"
-- Recursively solve for the inputs and apply this gate's operation
res : Literal <- case gate of
Constant x => fromInput x
And x y => do
x <- fromInput x
y <- fromInput y
pure $ x .&. y
Or x y => do
x <- fromInput x
y <- fromInput y
pure $ x .|. y
Not x => do
x <- fromInput x
pure $ complement x
LShift x y => do
x <- fromInput x
pure $ x `shiftL` y
RShift x y => do
x <- fromInput x
pure $ x `shiftR` y
-- Add this gate's output to the cache
modify $ insert wire res
pure res
where
fromInput : (i : Input) -> Eff fs Literal
fromInput (Left x) = pure x
fromInput (Right x) = solveWire x
Part Functions
Part 1
Parse the input, then feed it and an initial empty cache into our solveWire
function, passing the produced value as the output and the circut, represented
as a dependent map from wires to gates, as well as the output signal from wire
'a' as the context for part 2.
covering
part1 : Eff (PartEff String) (Bits16, (SortedDMap Wire Gate, Literal))
part1 = do
circut <- askAt "input" >>= parseGates
(value, _) <-
runState empty . runReader circut $ solveWire "a"
{fs = State (SortedMap Wire Literal)
:: Reader (SortedDMap Wire Gate)
:: PartEff String }
pure (value, (circut, value))
Part 2
Override the value for the 'b' wire to the output from the 'a' wire in part 1, then rerun our calcuation to find the new output for the 'a' wire.
covering
part2 : (SortedDMap Wire Gate, Literal) -> Eff (PartEff String) Bits16
part2 (circut, value) = do
let circut = insert "b" (Constant (Left value)) circut
(value, _) <-
runState empty . runReader circut $ solveWire "a"
{fs = State (SortedMap Wire Literal)
:: Reader (SortedDMap Wire Gate)
:: PartEff String }
pure value