Applicative cubes: back to AoC 2023 day 2

    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 Revelations 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 Monoids 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.

    Neil Smith

    Read more posts by this author.