A comment on my solution to day 2 suggested that the Applicative
typeclass would let me better express my solution to the problem. I've not really defined my own Applicative
much, so I thought I'd give it a try. (You'll need to read the Day 2 solution to make much sense of the detail of this problem.)
The typeclasses
I start by making the Revelation
type polymorphic, and deriving Functor
and Foldable
instances for it. The Applicative
instance is nothing unusual: applying a Revelation
of functions to a Revelation
of values applies the functions element-wise.
data Revelation a = Revelation a a a
deriving (Show, Functor, Foldable)
instance Applicative Revelation where
pure n = Revelation n n n
(Revelation rf gf bf) <*> (Revelation r g b) =
Revelation (rf r) (gf g) (bf b)
Next comes the definition of Monoid
for the Revelation
, which relies on Monoid
being defined for the contained type. Again, nothing special: combining two Revelation
s combines them element-wise.
instance (Semigroup a) => Semigroup (Revelation a) where
rev1 <> rev2 = liftA2 (<>) rev1 rev2
instance (Monoid a) => Monoid (Revelation a) where
mempty = Revelation mempty mempty mempty
How they're used
The polymorphism of Revelation
allows me to take advantage of all the pre-defined Monoid
s in Data.Monoid
, such as Max
and Sum
. That makes the conversion of Cube
to Revelation
a little different: more noise from the conversion to and from the Sum
monoid, but no need for a newtype
. (I need the fmap (fmap Sum)
to apply the Sum
to each element of each Revelation
in the list.)
revealify :: Showings -> Revelation Int
revealify = fmap getSum . mconcat . fmap (fmap Sum) . fmap reveal
reveal :: Cube -> Revelation Int
reveal (Cube Red n) = Revelation n 0 0
reveal (Cube Green n) = Revelation 0 n 0
reveal (Cube Blue n) = Revelation 0 0 n
I can define required
in a similar way: the minimal requirement is the maximum of each element of the Revelation
:
required :: Game -> Revelation Int
required = fmap getMax . mconcat . fmap (fmap Max) . getRevelations
I can use that to direcly define possible
without needing to define compatibleWith
. A Revelation
is possible if the elements are no greater than the limit. required
find the requirements for this game, and liftA2 (>=)
means the comparison (between the limit and this revelation) is done element-wise. Because Revelation
is Foldable
, I can combine those three results into one. There are two definitions here, one using the all
function and one using the All
monoid; pick which one you like.
possible :: Revelation Int -> Game -> Bool
-- possible limit = all id . liftA2 (>=) limit . required
possible limit = getAll . fold . fmap All . liftA2 (>=) limit . required
Finally, I use Foldable
in concert with the Product
monoid to find the product of the elements of a Revelation
.
power :: Revelation Int -> Int
power = getProduct . fold . fmap Product
Conclusion
Was this effort worth it? As a learning exercise, yes: I got a bit more practice with Applicative
and Monoid
. As a software development technique, I'm far from certain. This version is certainly more flexible than the original, but I don't think this problem really requires it. The noise of coercing values into and out of different monoids doesn't make for the tersest implementation.
Code
You can get the code from my locally-hosted Git repo, or from Gitlab, in advent02/MainApplicative.hs
.