Advent of Code
This module provides a command line interface for running the solution to a
specific day for a specific year, run as advent $YEAR $DAY
.
This runner will automatically locate the appropriate input file and then run
the selected day's solution against it. See Runner for the
definitions of the Day
, Year
, and Advent
data types.
module Main
import System
import System.Path
import System.File
import System.Directory
import System.Console.GetOpt
import Data.String
import Data.Vect
import Control.Eff
import Derive.Prelude
import Structures.Dependent.FreshList
import Runner
import Util
import Util.Eff
import Years.Y2015;
%language ElabReflection
%default total
Command Line Interface
Since this is a simple application, I am using GetOpt
from contrib
for
argument parsing. Here we define our options list, a data type to describe our
flags, and the usage message header.
data Flag = UseExample | Verbose
%runElab derive "Flag" [Eq, Show]
options : List (OptDescr Flag)
options =
[
MkOpt ['u'] ["use-example"] (NoArg UseExample) "Use example input instead of main input"
, MkOpt ['v'] ["verbose"] (NoArg Verbose) "Enable logging to stderr"
]
header : String
header = "Usage: advent YEAR DAY [OPTION...]"
Error Type
We will be using one unified error type for the entire Main
module, but we
must go ahead and define it before we go any further with command line arguments
parsing, as we are about to do some things that might fail.
data Error : Type where
OptionsError : (errs : List String) -> Error
ArgumentsError : (count : Nat) -> Error
InvalidYear : (year : String) -> Error
InvalidDay : (year : Nat) -> (day : String) -> Error
NoSuchDay : (year : Nat) -> (day : Nat) -> Error
NoCurrentDir : Error
InputRead : (path : String) -> (err : FileError) -> Error
SolveError : Show e => (year, day, part : Nat) -> (err : e) -> Error
%name Error err
A Show
implementation for Error
is provided, hidden in this document for
brevity.
Extract the year and day
After peeling off our options from the arguments using GetOpt
, we will be left
with a list of non-option arguments, which we must then parse into our year
number/day number pair. We perform some pattern matching to make sure the list
is the correct length, and then parse the individual integers, throwing an error
if anything goes wrong
||| Convert the non-option arguments into a Year/Day pair
argumentsToPair : Has (Except Error) fs =>
List String -> Eff fs (Nat, Nat)
argumentsToPair [] = throw $ ArgumentsError 0
argumentsToPair [x] = throw $ ArgumentsError 1
argumentsToPair [year, day] = do
year <- note (InvalidYear year) $ parsePositive year
day <- note (InvalidDay year day) $ parsePositive day
pure (year, day)
argumentsToPair xs = throw $ ArgumentsError (length xs)
The Advent
Construct our top-level Advent
record from the imported Year
modules. The
argument to the MkAdvent
constructor is a FreshList
, with a freshness
criteria ensuring that the list is in ascending order and does not include any
duplicate Year
s.
advent : Advent
advent = MkAdvent [
y2015
]
The Hard Part
Now we must glue everything together, parse our arguments, handle any supplied
options, locate the relevant year and day, locate the corresponding input file,
read it, and then run the selected Day
against it.
Because we are building our application around effects, the actual core of the
application is the effectful start
computation, that the main
function will
then apply IO
based handlers to.
Our reader and writer aren't ready yet, we need to do some setup first, so the top level type signature doesn't include them, so we'll add them to the effects list as they become available.
covering
||| Actual main, as an effectful computation
start : Eff [IO, Except Error] ()
start = do
Parsing the arguments
Feed the augments from System.getArgs
into getOpt
, making sure to drop the
name the binary was invoked with, then check for any errors in the result,
throwing our own error if any are encountered.
-- Read and parse arguments/options
args <- getArgs
let opts = getOpt Permute options (drop 1 args)
when (not . null $ opts.errors) $
throw (OptionsError opts.errors)
Use our argumentsToPair
function to extract the selected year and day numbers
from the positional arguments, implicitly throwing an error if there are any
failures doing so.
(year, day_n) <- argumentsToPair opts.nonOptions
Handling the arguments and finding the input
Handle the verbosity flag, if it is set, hook our logger up to stderr, otherwise
blackhole the logs. Afterwards, use logHandler
to introduce the Logger
into
the effects list.
-- If the verbose flag is set, hook up the logging writer to stderr
let verbosity = count (== Verbose) opts.options
let logHandler : Eff [IO, Except Error, Logger] () -> Eff [IO, Except Error] () =
handleLog (Warn >+ verbosity)
-- Add the logging writer to the effect
logHandler $ do
Find the current directory and append to it to find the input file. If the
--use-example/-u
flag is set, look for the input file at
inputs/examples/$year/$day
, otherwise look for it at inputs/real/$year/$day
.
After we've located it, attempt to read from it, throwing an InputRead
error
if any errors occur, then insert the contents into our Reader
and inject it
into the effects list.
-- Locate and read in the input file
Just cwd <- currentDir | _ => throw NoCurrentDir
let cwd = parse cwd
let use_example = any (== UseExample) opts.options
let base_dir = cwd /> "inputs" /> (if use_example then "examples" else "real");
let input_path = show $ base_dir /> (show year) /> (show day_n)
info "Reading input from \{input_path}"
Right contents <- readFile input_path | Left err => throw $ InputRead input_path err
-- Now add the reader that provides the input to the effect
runReaderAt "input" contents {fs = PartEff Error} $ do
Try and locate the Day
in our Advent
record, throwing an error if we are
unable to locate it
-- Attempt to locate the provided day
Just day <- pure $ locate year day_n advent | _ => throw $ NoSuchDay year day_n
Executing the parts
Run the selected Day
's part 1, wrapping any errors that it returns in a
SolveError
. Collect both the result and the opaque context value, then print
out the result.
-- Run part 1
part_1 <- lift $ runExcept day.part1
(part_1, ctx) <- rethrow . mapLeft (SolveError year day_n 1 @{day.showErr1}) $ part_1
putStrLn "\{show year} day \{show day_n} part 1:"
putStrLn $ unlines . map (" " ++) . lines . show @{day.showOut1} $ part_1
Check and see if the selected Day
has a part 2, if it does, feed the opaque
context value collected from part 1 into it, wrap any errors that it returns in
a SolveError
, then print out the result, then return, closing out the program.
-- Run part 2 if we have it
case day.part2 of
Nothing => pure ()
Just part_2_f => do
part_2 <- lift . runExcept $ part_2_f ctx
part_2 <- rethrow . mapLeft (SolveError year day_n 2 @{day.showErr2}) $ part_2
putStrLn "\{show year} day \{show day_n} part 2:"
putStrLn $ unlines . map (" " ++) . lines . show @{day.showOut2} $ part_2
Helper functions
where
Lower logging into the IO component of the effect
Makes use of Logger
's handleLoggerIO
function to "lower" logging actions
into IO
within the effect.
-- Lowers logging into IO within the effect using the given IO function
handleLog :
Has Logger fs => Has IO (fs - Logger) =>
(max_log : Level) -> Eff fs a -> Eff (fs - Logger) a
handleLog max_log x =
handle
(\msg, f =>
do _ <- send $ handleLoggerIO max_log msg
f $ ignore msg) x
Handle the effectful action as an IO action
Use runEff
to translate the effects in start
's effects list into IO actions,
and return the resulting IO ()
.
The IO
component of the effects list is handled via id
, translating IO
actions into themselves, and we construct a helper function to handle errors as
IO actions by printing the error and crashing the program.
covering
||| Shim main, which simply executes the effectful computation
main : IO ()
main = runEff start [id, handleError]
where
handleError : Except Error a -> IO a
handleError (Err err) = do
printLn err
exitFailure