Year 2015 Day 5
This day provides a nice chance to introduce
views,
specifically String
's
AsList
view, which lets us treat String
s as if they were lazy lists or iterators.
import Data.String
import Util
Nice Strings
Contains at least 3 vowels
First we must define what a vowel is
vowels : List Char
vowels = ['a', 'e', 'i', 'o', 'u']
Now define a function to check if a string contains a minimum number of vowels.
Here we make use of the with
rule and the AsList
view over strings.
The with
rule provides dependently typed pattern matching, allowing us to make
use of the fact that the form of some of the arguments can be determined by the
values of the others.
The AsList
view associates a String
with a lazy list of the characters in
the string by providing incremental proofs that the String
decomposes into the
lazy list. An AsList
of value Nil
/[]
proves that the associated string is
empty, and an AsList
of value c :: xs
proves that the associated string can
be generated by strCons
ing the character c
onto the rest of the string as
captured by the AsList
in xs
.
We use the asList
function, which generates the AsList
view for the provided
string, as the argument to the with
block. Functions like this that produce
views for a value are refereed to as "covering functions". In general, views are
dependently typed constructs whose value provides insight into the structure of
another value.
We implement this function by keeping a counter of the number of vowels we still
need to see to accept the string, returning true when that counter equals zero,
or false if we hit the end of the string (when the string argument is empty and
the associated AsList
is Nil
) while the counter is still non-zero.
If the counter is non-zero and the string is non-empty, we check the current c
character to see if it is a vowel, by checking to see if our vowels
list
contains it, then recurse, decrementing the counter if it was a vowel, and
leaving the counter unchanged if it wasn't.
The use of | xs
after the recursive call to containsVowels
tells Idris to
recurse directly to the with
block, allowing it to reuse the AsList
value in
xs
without having to generate the AsList
view again.
containsVowels : (count : Nat) -> String -> Bool
containsVowels count str with (asList str)
containsVowels 0 str | _ = True
containsVowels (S k) "" | [] = False
containsVowels (S k) (strCons c str) | (c :: xs) =
if any (== c) vowels
then containsVowels k str | xs
else containsVowels (S k) str | xs
At least one letter appears twice in a row
For this one, since we need to inspect two letters at a time, we break out a
nested with
block, with the tail of the AsList
as the argument of the block,
to also pattern match on the next character of the string.
Since the AsList
view is lazy, we need to use force
in the inner with block
to force the evaluation of the next component of the view, this is due to a
bug in the compiler.
The logic here is pretty simple, if we hit the empty string or get down to one
character, return False
, if we have two characters available, compare them,
returning True
if they match, and recursing to the outer with block, only
consuming one of the two characters, if they don't match.
doubleLetter : String -> Bool
doubleLetter str with (asList str)
doubleLetter "" | [] = False
doubleLetter (strCons c str) | (c :: x) with (force x)
doubleLetter (strCons c "") | (c :: x) | [] = False
doubleLetter (strCons c (strCons d str)) | (c :: x) | (d :: y) =
if c == d
then True
else doubleLetter (strCons d str) | x
No Invalid Substrings
First we define the substrings that are disallowed
disallowedSubstrs : List String
disallowedSubstrs = ["ab", "cd", "pq", "xy"]
Idris's standard library does not currently define a function to check if a
string is a substring of another, so we instead use our own isSubstr
function,
defined via use of the AsList
view, from our Util
module.
noDisallowed : String -> Bool
noDisallowed str =
not $ any (`isSubstr` str) disallowedSubstrs
isNice
Combine all three of our above functions together to check if a string is a "nice" string
isNice : String -> Bool
isNice str = containsVowels 3 str && doubleLetter str && noDisallowed str
New niceness
Pair of letters that appears twice
Use a similar approach to doubleLetter
above, but instead of comparing the two
chars for equality, pack them into a string and check if they are a substring of
the rest of the string.
pairTwice : String -> Bool
pairTwice str with (asList str)
pairTwice "" | [] = False
pairTwice (strCons c str) | (c :: x) with (force x)
pairTwice (strCons c "") | (c :: x) | [] = False
pairTwice (strCons c (strCons d str)) | (c :: x) | (d :: y) =
if pack [c, d] `isSubstr` str
then True
else pairTwice (strCons d str) | x
Letter that repeats after one letter
More of the same, but this one gets extra fun because we are matching 3 letters
at a time. This would be a lot nicer if we could get away without nesting the
with
blocks, but due to the aformentioned compiler bug, we need the nesting.
letterRepeatGap : String -> Bool
letterRepeatGap str with (asList str)
letterRepeatGap "" | [] = False
letterRepeatGap (strCons c str) | (c :: x) with (force x)
letterRepeatGap (strCons c "") | (c :: x) | [] = False
letterRepeatGap (strCons c (strCons d str)) | (c :: x) | (d :: y) with (force y)
letterRepeatGap (strCons c (strCons d "")) | (c :: x) | (d :: y) | [] = False
letterRepeatGap (strCons c (strCons d (strCons e str))) | (c :: x) | (d :: y) | (e :: z) =
if c == e
then True
else letterRepeatGap (strCons d (strCons e str)) | x
New isNice
Glue our two new niceness functions together
isNice' : String -> Bool
isNice' str = pairTwice str && letterRepeatGap str
Part Functions
Part 1
Split our input up into a list of its component strings, and then count how many satisfy the "nice" criteria.
part1 : Eff (PartEff String) (Nat, ())
part1 = do
strings <- map (lines . trim) $ askAt "input"
let x = count isNice strings
pure (x, ())
Part 2
Same as above, but with the new niceness criteria
part2 : () -> Eff (PartEff String) Nat
part2 _ = do
strings <- map (lines . trim) $ askAt "input"
let x = count isNice' strings
pure x