From d6330fec9bac4671741f60d94045f5ab67901d03 Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Wed, 22 Jan 2025 22:40:32 -0500 Subject: [PATCH 1/8] Add refined dependency --- advent.ipkg | 1 + 1 file changed, 1 insertion(+) diff --git a/advent.ipkg b/advent.ipkg index e915d93..c9776ba 100644 --- a/advent.ipkg +++ b/advent.ipkg @@ -21,6 +21,7 @@ depends = base , ansi , if-unsolved-implicit , c-ffi + , refined -- modules to install modules = Runner From 9489721e2905c920f61a7b466c1aade268779bec Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Wed, 22 Jan 2025 22:29:38 -0500 Subject: [PATCH 2/8] Year 2015 Day 11 Part 1 --- README.md | 1 + src/Years/Y2015.md | 7 ++ src/Years/Y2015/Day11.md | 225 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 src/Years/Y2015/Day11.md diff --git a/README.md b/README.md index 95cd2ee..2ce05fb 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,4 @@ solution. - [Day 8](src/Years/Y2015/Day8.md) - [Day 9](src/Years/Y2015/Day9.md) - [Day 10](src/Years/Y2015/Day10.md) + - [Day 11](src/Years/Y2015/Day11.md) diff --git a/src/Years/Y2015.md b/src/Years/Y2015.md index 8a7b278..f745517 100644 --- a/src/Years/Y2015.md +++ b/src/Years/Y2015.md @@ -17,6 +17,7 @@ import Years.Y2015.Day7 import Years.Y2015.Day8 import Years.Y2015.Day9 import Years.Y2015.Day10 +import Years.Y2015.Day11 --> # Days @@ -87,6 +88,12 @@ y2015 = MkYear 2015 [ , day10 ``` +## [Day 11](Y2015/Day11.md) + +```idris + , day11 +``` + ```idris ] ``` diff --git a/src/Years/Y2015/Day11.md b/src/Years/Y2015/Day11.md new file mode 100644 index 0000000..73820f1 --- /dev/null +++ b/src/Years/Y2015/Day11.md @@ -0,0 +1,225 @@ +# Year 2015 Day 11 + +This day provides a gentle introduction to refinement types, types which augment +other types with a predicate that must hold for all the values of the refined +type, which allow easily defining types as subsets of other types based on some +property of the acceptable elements. + +While refinement types are quite easy to implement in Idris, and we easily could +implement the one we need for today's task as a throw away data structure just +for this module, we will be using the `refined`[^1] library's implementation for +the sake of getting on with it. + + + +```idris +import Data.Vect +import Data.String +import Data.Refined.Char + +import Util +``` + +## Data Structures and Parsing + +Provide a predicate which establishes that a char is a lowercase alphabetic +character, the only type of character that passwords are allowed to contain. We +use the `FromTo` predicate from the `refined`[^1] library to restrict chars to +within the range from `a` to `z`. + +This predicate has multiplicity 0, a full discussion of multiplicites and linear +types is out of scope for today, but put simply this enforces that a value of +this predicate type can be "used" at most 0 times, having the effect of erasing +the value at runtime, making this more or less a zero-cost abstraction. + +```idris +0 IsPasswordChar : Char -> Type +IsPasswordChar = FromTo 'a' 'z' +``` + +Combine a `Char` with its corresponding `IsPasswordChar` predicate into a +combined "refined" type, whose elements are the subset of `Char`s that are +lowercase alphabetic characters. + +```idris +record PasswordChar where + constructor MkPC + char : Char + {auto 0 prf : IsPasswordChar char} +%name PasswordChar pc +``` + + + +A function to fallible convert `Char`s into refined `PasswordChar`s, this will +return `Just` if the `Char` satisfies the predicate, and `Nothing` otherwise, +aligning with the type-level guarantees of the `PasswordChar` type. + +```idris +refineChar : Char -> Maybe PasswordChar +refineChar c = map fromSubset $ refine0 c + where + fromSubset : Subset Char IsPasswordChar -> PasswordChar + fromSubset (Element char prf) = MkPC char +``` + +Convenience function returning `a` as a `PasswordChar` + +```idris +lowest : PasswordChar +lowest = MkPC 'a' +``` + +"Increment" a `PasswordChar`, changing it to the next letter (`a` becomes `b`, +`b` becomes `c`, so on), returning nothing if we go past `z`, corresponding to a +carry. + +We do this by converting the internal `Char` to an integer, adding one to it, +then converting back to a `Char`. This low-level conversion loses the refinement +context, forcing us to call `refineChar` on the new value to bring it back into +the refined type, providing us type-level assurance that this function will +return `Nothing` if an overflow occurs. + +```idris +incriment : PasswordChar -> Maybe PasswordChar +incriment (MkPC char) = + let next = chr $ ord char + 1 + in refineChar next +``` + +A `Password` is a `Vect` of 8 `PasswordChar`s. This `Vect` is sorted in reverse +order compared to the `String` it corresponds to, with the right-most letter +first, to make implementing the `incrimentPassword` function a little easier and +cleaner. + +We also provide conversion to/from a `String` + +```idris +Password : Type +Password = Vect 8 PasswordChar + +parsePassword : Has (Except String) fs => String -> Eff fs Password +parsePassword str = do + cs <- note "Password has incorrect number of characters: \{str}" + . toVect 8 . reverse . unpack $ str + cs <- note "Password contained invalid characters: \{str}" + $ traverse refineChar cs + pure cs + +passwordToString : Password -> String +passwordToString = pack . toList . reverse . map char +``` + +Define a function to increment a `Password`, this function will "roll over", +producing `aaaaaaaa` if provided `zzzzzzzz`. + +```idris +incrimentPassword : Vect n PasswordChar -> Vect n PasswordChar +incrimentPassword [] = [] +incrimentPassword (x :: xs) = + case incriment x of + Nothing => lowest :: incrimentPassword xs + Just x => x :: xs +``` + +### Password validity + +A password must contain a run of at least 3 incrementing characters, check this +by converting the `PasswordChar`s to their integer representations. Remember +that our `Password` `Vect` is backwards compared to the string representation. + +```idris +incrimentingChars : Vect n PasswordChar -> Bool +incrimentingChars (z :: next@(y :: (x :: xs))) = + let [x, y, z] : Vect _ Int = map (ord . char) [x, y, z] + in if y == x + 1 && z == y + 1 + then True + else incrimentingChars next +incrimentingChars _ = False +``` + +A password may not contain `i`, `o`, or `l` + +```idris +noInvalidChars : Vect n PasswordChar -> Bool +noInvalidChars = not . (any (flip contains $ ['i', 'o', 'l'])) . map char +``` + +A password contains at least two different non-overlapping pairs. + +We check this by pattern matching our password two characters at a time, +consuming both characters if a matched pair is found, and tacking on the `Char` +the list is composed of to an accumulator list as we go. This list is then +reduced to only its unique elements (it's `nub`), and checking to see if it's +length is at least 2. + +```idris +containsPairs : Vect n PasswordChar -> Bool +containsPairs xs = length (nub $ pairs (reverse xs) []) >= 2 + where + pairs : Vect m PasswordChar -> (acc : List Char) -> List Char + pairs [] acc = acc + pairs (x :: []) acc = acc + pairs (x :: (y :: xs)) acc = + if x == y + -- If there is a pair, consume it to prevent detecting overlapping pairs + then pairs xs (x.char :: acc) + -- If there isn't a pair, only consume one character + else pairs (y :: xs) acc +``` + +Combine our password criteria into one function + +```idris +part1Critera : Vect n PasswordChar -> Bool +part1Critera xs = incrimentingChars xs && noInvalidChars xs && containsPairs xs +``` + +### Find the next password + +Find the next password based on a criteria function + +```idris +findNextPassword : + (f : Vect n PasswordChar -> Bool) -> (password : Vect n PasswordChar) + -> Vect n PasswordChar +findNextPassword f password = + let next = incrimentPassword password + in if f next + then next + else findNextPassword f next +``` + +## Part Functions + +### Part 1 + +```idris +part1 : Eff (PartEff String) (String, ()) +part1 = do + input <- map trim $ askAt "input" + password <- parsePassword input + info "Starting password: \{show password} -> \{passwordToString password}" + let next_password = findNextPassword part1Critera password + pure (passwordToString next_password, ()) +``` + + + +[^1]: https://github.com/stefan-hoeck/idris2-refined/ From 407149dd4a6053482c64986b9fbf67b623717a8c Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Thu, 23 Jan 2025 00:17:19 -0500 Subject: [PATCH 3/8] Documentation tweaks and fixes --- README.md | 5 +++++ src/Main.md | 8 ++++---- src/Runner.md | 6 +++--- src/Util/Eff.md | 23 +++++++++++++++++------ 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2ce05fb..db8f4f9 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,11 @@ solution. Extend the functionality of the effects included in the [eff](https://github.com/stefan-hoeck/idris2-eff/) library + - [Util.Digits](src/Util/Digits.md) + + Provide views that enable recursively pattern matching numbers as lists of + digits, in both ascending and descending order of significance. + # Index of years and days - 2015 diff --git a/src/Main.md b/src/Main.md index a913135..f602362 100644 --- a/src/Main.md +++ b/src/Main.md @@ -181,8 +181,8 @@ failures doing so. ## 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 logging -`Writer` into the effects list. +blackhole the logs. Afterwards, use `logHandler` to introduce the `Logger` into +the effects list. ```idris -- If the verbose flag is set, hook up the logging writer to stderr @@ -259,8 +259,8 @@ a `SolveError`, then print out the result, then return, closing out the program. ### Lower logging into the IO component of the effect -This function uses the provided `String -> IO ()` to remove the `Writer` from -the effects list by translating `tell` calls to IO actions within the effect. +Makes use of `Logger`'s `handleLoggerIO` function to "lower" logging actions +into `IO` within the effect. ```idris -- Lowers logging into IO within the effect using the given IO function diff --git a/src/Runner.md b/src/Runner.md index 500821b..201baff 100644 --- a/src/Runner.md +++ b/src/Runner.md @@ -28,9 +28,9 @@ function to have a single source of truth for this. The `err` type can be any type with a `Show` implementation, but that constraint will be tacked on in the next step. -A `Writer` effect is provided for logging, and a `Reader` effect is provided to -pass in the input, just to make the top level API a little bit cleaner. `IO` is -also provided, even though the part solutions themselves shouldn't really be +The `Logger` effect is provided for logging, and a `Reader` effect is provided +to pass in the input, just to make the top level API a little bit cleaner. `IO` +is also provided, even though the part solutions themselves shouldn't really be doing any IO, this may come in handy if a part needs `IO` for performance reasons. diff --git a/src/Util/Eff.md b/src/Util/Eff.md index 1b51cba..1baa453 100644 --- a/src/Util/Eff.md +++ b/src/Util/Eff.md @@ -22,9 +22,18 @@ import System.File Basic enumeration describing log levels, we define some (hidden) utility functions for working with these. +The `Other n` log level is restricted to `n` greater than 4, to prevent +ambiguity between custom log levels and predefined log levels. + ```idris public export -data Level = Err | Warn | Info | Debug | Trace | Other Nat +data Level : Type where + Err : Level + Warn : Level + Info : Level + Debug : Level + Trace : Level + Other : (n : Nat) -> {auto _ : n `GT` 4} -> Level ``` +## References + [^1]: https://github.com/stefan-hoeck/idris2-refined/ From c47f522c50f7f7a764afa5f786697df5ed6acc96 Mon Sep 17 00:00:00 2001 From: Nathan McCarty Date: Thu, 23 Jan 2025 00:57:16 -0500 Subject: [PATCH 8/8] General revisions, improve README --- README.md | 58 +++++++++++++++++++++++++++++++++++++++-- src/Years/Y2015/Day6.md | 6 ++++- src/Years/Y2015/Day7.md | 9 +++++-- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 59d1b13..6dfd540 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,22 @@ The goal of this project is to get all 500 currently available stars in the form of one single Idris application, and thoroughly document the results as literate Idris files. -# Index of non-day modules +## Authors Note + +The solutions contained in this project are intended to be read in sequential +order, though can reasonably be read in any order if you have a good level of +familiarity with more advanced functional programming topics. + +The solutions will involve progressively more advanced topics as day and year +number increase, though I try not to introduce too much within the scope of any +one day. + +Suggestions and other feedback are highly welcome, please reach out to me via +any platform you know me on, or send an email to the +[~thatonelutenist/public-inbox](https://lists.sr.ht/~thatonelutenist/public-inbox) +mailing list on source hut. + +## Index of non-day modules - [Runner](src/Runner.md) @@ -31,17 +46,56 @@ solution. Provide views that enable recursively pattern matching numbers as lists of digits, in both ascending and descending order of significance. -# Index of years and days +## Index of years and days - 2015 - [Day 1](src/Years/Y2015/Day1.md) + + Warm up problem, breaks in our new runner and not much else interesting. + - [Day 2](src/Years/Y2015/Day2.md) + + An early hint of effectful parsing. + - [Day 3](src/Years/Y2015/Day3.md) + + Peculiarities of writing mutually recursive functions in dependently typed + languages. + - [Day 4](src/Years/Y2015/Day4.md) + + Basic FFI to openssl to steal its MD5 function for Idris's use. + - [Day 5](src/Years/Y2015/Day5.md) + + First introduction to views and dependent pattern matching[^1]. + - [Day 6](src/Years/Y2015/Day6.md) + + Naive approach to handling the first 2d grid problem. + - [Day 7](src/Years/Y2015/Day7.md) + + Introduces dependent maps and indexed type families. + - [Day 8](src/Years/Y2015/Day8.md) + + Proper effectful parsers and non-determinism in effect stacks. + - [Day 9](src/Years/Y2015/Day9.md) + + Naive approach to handling the first graph traversal problem. + - [Day 10](src/Years/Y2015/Day10.md) + + Introduce our `Digits`, dependent pattern matching on integers as lists of + digits. + - [Day 11](src/Years/Y2015/Day11.md) + + Introduces refinement types + +## References + +[^1]: Idris 2 Manual: + [Views and the "with" rule](https://idris2.readthedocs.io/en/latest/tutorial/views.html#views-and-the-with-rule) diff --git a/src/Years/Y2015/Day6.md b/src/Years/Y2015/Day6.md index 6534ca5..131be48 100644 --- a/src/Years/Y2015/Day6.md +++ b/src/Years/Y2015/Day6.md @@ -1,4 +1,8 @@ -# [Year 2015 Day 6](https://adventofcode.com/2015/day/6) +# \[Year 2015 Day 6\](https://adventofcode.com/2015/day/ + +6. + +Introduction to the advent of code classic 2d grid problem.