Skip to content

Commit 1fe2763

Browse files
committed
starting on day 18 reflections
1 parent d035570 commit 1fe2763

File tree

2 files changed

+277
-17
lines changed

2 files changed

+277
-17
lines changed

reflections.md

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,6 +1789,237 @@ Day 18
17891789

17901790
[d18c]: https://github.com/mstksg/advent-of-code-2017/blob/master/src/AOC2017/Day18.hs
17911791

1792+
Day 18 was pretty fun, and I'm probably going to write a blog post on my final
1793+
solution at some point. It's nice because you can basically "compile" your
1794+
code to run on an abstract machine, and the difference between Part 1 and Part
1795+
2 is pretty much just the interpretation of that machine.
1796+
1797+
### The Language
1798+
1799+
First, we can look at an encoding of our language, as a simple ADT describing
1800+
each of the commands.
1801+
1802+
```haskell
1803+
type Addr = Either Char Int
1804+
1805+
addr :: String -> Addr
1806+
addr [c] | isAlpha c = Left c
1807+
addr str = Right (read str)
1808+
1809+
data Op = OSnd Addr
1810+
| OBin (Int -> Int -> Int) Char Addr
1811+
| ORcv Char
1812+
| OJgz Addr Addr
1813+
1814+
parseOp :: String -> Op
1815+
parseOp inp = case words inp of
1816+
"snd":(addr->c):_ -> OSnd c
1817+
"set":(x:_):(addr->y):_ -> OBin (const id) x y
1818+
"add":(x:_):(addr->y):_ -> OBin (+) x y
1819+
"mul":(x:_):(addr->y):_ -> OBin (*) x y
1820+
"mod":(x:_):(addr->y):_ -> OBin mod x y
1821+
"rcv":(x:_):_ -> ORcv x
1822+
"jgz":(addr->x):(addr->y):_ -> OJgz x y
1823+
_ -> error "Bad parse"
1824+
1825+
parse :: String -> Tape Op
1826+
parse = unsafeTape . map parseOp . lines
1827+
```
1828+
1829+
Here I'm using `Tape Op` to represent the current program memory and position
1830+
of the program counter -- it's a list of commands, essentially, "focused" on a
1831+
specific point with O(1) access to that focus and O(n) jumps. It's probably
1832+
better as a vector paired with an Int, but I already had my `Tape` code from
1833+
earlier!
1834+
1835+
### The Abstract Machine
1836+
1837+
Now to define the abstract machine (the "IO", so to speak) that we run our
1838+
program on.
1839+
1840+
There are really only two ways that our program interacts with an outside
1841+
world:
1842+
1843+
1. `Snd`-ing, which takes a single `Int` as a parameter and has no result
1844+
2. `Rcv`-ing, which takes a single `Int` as a parameter and has an `Int`
1845+
result
1846+
1847+
The `Rcv` `Int` parameter will be the value of the register being `Rcv`'d. It
1848+
is used by Part 1, but not by Part 2.
1849+
1850+
```haskell
1851+
data Command :: Type -> Type where
1852+
CRcv :: Int -> Command Int -- ^ input is current value of buffer
1853+
CSnd :: Int -> Command () -- ^ input is thing being sent
1854+
1855+
type Machine = Prompt Command
1856+
```
1857+
1858+
Here I am using the great *MonadPrompt* library, which allows us to create an
1859+
abstract `Monad` from a GADT of commands. Our `Machine` (a type synonym of
1860+
`Prompt Command`) will have a `Functor`, `Applicative`, and `Monad` instance
1861+
(so `fmap`, `return`, etc.), but also two "effectful commands":
1862+
1863+
```haskell
1864+
(prompt . CRcv) :: Int -> Machine Int
1865+
(prompt . CSnd) :: Int -> Machine ()
1866+
```
1867+
1868+
You can think of it as primitives for our monads, like `putStrLn` and `getLine`
1869+
for `IO`.
1870+
1871+
I find it convenient to alias these:
1872+
1873+
```haskell
1874+
rcvMachine :: Int -> Machine Int
1875+
rcvMachine = prompt . CRcv
1876+
1877+
sndMachine :: Int -> Machine ()
1878+
sndMachine = prompt . CSnd
1879+
```
1880+
1881+
The *MonadPrompt* library gives us the ability to "run" a `Prompt Command` by
1882+
giving an *interpreter function*:
1883+
1884+
```haskell
1885+
runPromptM
1886+
:: Monad m
1887+
=> (forall x. Command x -> m x)
1888+
-> Prompt Command a
1889+
-> m a
1890+
```
1891+
1892+
Essentially, given a way to "interpret" any `Command` in the context of a monad
1893+
of our choice, `m`, it will "run" the `Prompt Command` for us, firing our
1894+
interpreter whenever necessary.
1895+
1896+
### Language Logic
1897+
1898+
Now to implement the language itself:
1899+
1900+
```haskell
1901+
data ProgState = PS { _psTape :: Tape Op
1902+
, _psRegs :: M.Map Char Int
1903+
}
1904+
makeClassy ''ProgState
1905+
1906+
type TapeProg = MaybeT (StateT ProgState Machine)
1907+
```
1908+
1909+
Our stepping of our program needs some monad to work with, so we use `MaybeT
1910+
(StateT ProgState Machine)`. The `MaybeT` parameter tells us if our program
1911+
leaves the bounds of the tape, and `StateT` keeps track of the `ProgState`,
1912+
which contains the current tape with position and the values in all of the
1913+
registers.
1914+
1915+
We write an action to execute a single command:
1916+
1917+
```haskell
1918+
stepTape :: TapeProg ()
1919+
stepTape = use (psTape . tFocus) >>= \case
1920+
OSnd x -> do
1921+
lift . lift . sndMachine =<< addrVal x
1922+
advance 1
1923+
OBin f x y -> do
1924+
yVal <- addrVal y
1925+
psRegs . at x . non 0 %= (`f` yVal)
1926+
advance 1
1927+
ORcv x -> do
1928+
y <- lift . lift . rcvMachine
1929+
=<< use (psRegs . at x . non 0)
1930+
psRegs . at x . non 0 .= y
1931+
advance 1
1932+
OJgz x y -> do
1933+
xVal <- addrVal x
1934+
moveAmt <- if xVal > 0
1935+
then addrVal y
1936+
else return 1
1937+
advance moveAmt
1938+
where
1939+
addrVal (Left r) = use (psRegs . at r . non 0)
1940+
addrVal (Right x) = return x
1941+
advance n = do
1942+
Just t' <- move n <$> use psTape
1943+
psTape .= t'
1944+
```
1945+
1946+
Sorry for the gratuitous usage of `lens`! It's just so convenient for a
1947+
`State` context :) `use` is basically a way to *get* a specific part of our
1948+
state (`psTape . tFocus` gets us the focus of our state's tape). `%=` allows
1949+
us to modify values in our state with a given function. `.=` allows us to set
1950+
values in our state to a given value.
1951+
1952+
`psRegs . at x . non 0` is an interesting lens (that we can give to `use` or
1953+
`%=`), and does most of our heavy lifting in managing our registers. This gets
1954+
the value in the `_psRegs` register of our state, at the *key* `x`, *but*
1955+
treating it as 0 if the key is not found.
1956+
1957+
So, something like:
1958+
1959+
```haskell
1960+
psRegs . at x . non 0 .= y
1961+
```
1962+
1963+
Will set the register map's key `x` to be `y`. (Also an interesting benefit:
1964+
if `y` is 0, it will delete the key `x` from the map for us)
1965+
1966+
And, something like:
1967+
1968+
```haskell
1969+
psRegs . at x . non 0 %= (`f` yVal)
1970+
```
1971+
1972+
Will modify the register map's key `x` value with the function ``(`f` yVal)``.
1973+
1974+
```haskell
1975+
use (psRegs . at r . non 0)
1976+
```
1977+
1978+
Will give us the current register's key `r` value, giving us 0 if it does not
1979+
exist.
1980+
1981+
Knowing this, you should be able to see most of the logic going on here. I had
1982+
a bit of fun with the definition of `advance`. `move n` is from our `Tape`
1983+
API, and returns `Nothing` if you move out of the tape bounds. Pattern
1984+
matching on `Just` lets us trigger the "failure" case if the pattern match
1985+
fails, which for `MaybeT m` is `MaybeT (return Nothing)` -- a "`Maybe`
1986+
failure".
1987+
1988+
Now, `stepTape` is an action (in `State` and `Maybe`) that uses an underlying
1989+
`Machine` monad to step our tape one single step. We can "run" it purely to
1990+
get the underlying `Machine` action using:
1991+
1992+
```haskell
1993+
execTapeProg :: TapeProg a -> ProgState -> Machine ProgState
1994+
execTapeProg tp ps = flip execStateT ps . runMaybeT $ tp
1995+
```
1996+
1997+
Which will "run" a `TapeProg a`, with a given state, to produce the `Machine`
1998+
action (basically, a tree of nested `CRcv` and `CSnd`).
1999+
2000+
Conceptually, this is similar to how `execStateT :: StateT s IO a -> IO s`
2001+
produces an `IO` action that computes the final state that the `execStateT`
2002+
encodes.
2003+
2004+
We now have an action to take our tape a single step, but our Part 1 program
2005+
actually wants us to repeat the action until we go out of bounds. This looks
2006+
like a job for `many`, from the very popular `Alternative` typeclass (from
2007+
*Control.Applicative*)):
2008+
2009+
```haskell
2010+
many :: MaybeT m a -> MaybeT m [a]
2011+
many :: TapeProg a -> TapeProg [a]
2012+
```
2013+
2014+
`many` essentially repeats an action several times until it fails. For the
2015+
case of `TapeProg`, this means that it repeats an action several times until
2016+
the tape head goes out of bounds:
2017+
2018+
```haskell
2019+
stepTape :: TapeProg ()
2020+
many stepTape :: TapeProg [()]
2021+
```
2022+
17922023
### Day 18 Benchmarks
17932024

17942025
```

src/AOC2017/Day18.hs

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ import Data.Maybe (fromJust, maybeToList)
2424
import qualified Data.Map as M
2525
import qualified Data.Vector.Sized as V
2626

27+
{-
28+
******************
29+
* The Language *
30+
******************
31+
-}
32+
2733
type Addr = Either Char Int
2834

2935
addr :: String -> Addr
@@ -35,56 +41,67 @@ data Op = OSnd Addr
3541
| ORcv Char
3642
| OJgz Addr Addr
3743

38-
instance Show Op where
39-
show _ = "Op"
40-
4144
parseOp :: String -> Op
4245
parseOp inp = case words inp of
4346
"snd":(addr->c):_ -> OSnd c
4447
"set":(x:_):(addr->y):_ -> OBin (const id) x y
45-
"add":(x:_):(addr->y):_ -> OBin (+) x y
46-
"mul":(x:_):(addr->y):_ -> OBin (*) x y
47-
"mod":(x:_):(addr->y):_ -> OBin mod x y
48+
"add":(x:_):(addr->y):_ -> OBin (+) x y
49+
"mul":(x:_):(addr->y):_ -> OBin (*) x y
50+
"mod":(x:_):(addr->y):_ -> OBin mod x y
4851
"rcv":(x:_):_ -> ORcv x
4952
"jgz":(addr->x):(addr->y):_ -> OJgz x y
5053
_ -> error "Bad parse"
5154

5255
parse :: String -> Tape Op
5356
parse = unsafeTape . map parseOp . lines
5457

55-
data ProgState = PS { _psTape :: Tape Op
56-
, _psRegs :: M.Map Char Int
57-
}
58-
59-
makeClassy ''ProgState
58+
{-
59+
**************************
60+
* The Abstract Machine *
61+
**************************
62+
-}
6063

64+
-- | Abstract data type describing "IO" available to the abstract machine
6165
data Command :: Type -> Type where
6266
CRcv :: Int -> Command Int -- ^ input is current value of buffer
6367
CSnd :: Int -> Command () -- ^ input is thing being sent
6468

69+
type Machine = Prompt Command
70+
71+
rcvMachine :: Int -> Machine Int
72+
rcvMachine = prompt . CRcv
73+
74+
sndMachine :: Int -> Machine ()
75+
sndMachine = prompt . CSnd
76+
77+
data ProgState = PS { _psTape :: Tape Op
78+
, _psRegs :: M.Map Char Int
79+
}
80+
makeClassy ''ProgState
81+
6582
-- | Context in which Tape commands are run. Tape commands have access to
66-
-- an underlying 'Prompt Command' effect monad that allows it to 'Rcv' and
83+
-- an underlying 'Machine' effect monad that allows it to 'Rcv' and
6784
-- 'Snd'.
6885
--
6986
-- Nothing = program terminates by running out of bounds
70-
type TapeProg = MaybeT (StateT ProgState (Prompt Command))
71-
execTapeProg :: TapeProg a -> ProgState -> Prompt Command ProgState
87+
type TapeProg = MaybeT (StateT ProgState Machine)
88+
execTapeProg :: TapeProg a -> ProgState -> Machine ProgState
7289
execTapeProg tp ps = flip execStateT ps . runMaybeT $ tp
7390

7491
-- | Single step through program tape.
7592
stepTape :: TapeProg ()
7693
stepTape = use (psTape . tFocus) >>= \case
7794
OSnd x -> do
78-
lift . lift . prompt . CSnd =<< addrVal x
95+
lift . lift . sndMachine =<< addrVal x
7996
advance 1
8097
OBin f x y -> do
8198
yVal <- addrVal y
8299
psRegs . at x . non 0 %= (`f` yVal)
83100
advance 1
84101
ORcv x -> do
85-
y <- lift . lift . prompt . CRcv
102+
y <- lift . lift . rcvMachine
86103
=<< use (psRegs . at x . non 0)
87-
psRegs . at x .= Just y
104+
psRegs . at x . non 0 .= y
88105
advance 1
89106
OJgz x y -> do
90107
xVal <- addrVal x
@@ -99,6 +116,12 @@ stepTape = use (psTape . tFocus) >>= \case
99116
Just t' <- move n <$> use psTape
100117
psTape .= t'
101118

119+
{-
120+
************************
121+
* Context for Part A *
122+
************************
123+
-}
124+
102125
-- | Context in which to interpret Command for Part A
103126
--
104127
-- State parameter is the most recent sent item. Writer parameter is all
@@ -125,6 +148,12 @@ day18a = show
125148
. execTapeProg (many stepTape) -- stepTape until program terminates
126149
. (`PS` M.empty) . parse
127150

151+
{-
152+
************************
153+
* Context for Part B *
154+
************************
155+
-}
156+
128157
-- | Context in which to interpret Command for Part B
129158
--
130159
-- The State parameter is the input buffer, the Writer parameter is the

0 commit comments

Comments
 (0)