Prevent inadvertently using different type class instance

  • A+
Category:Languages

I expect the following behavior from the applicative instance of my ZipList':

zipListApplyTest = fs <*> xs   where fs = ZipList' [negate, id]         xs = ZipList' [1..5]  -- Result: ZipList' [-1,2] 

This was my first attempt:

newtype ZipList' a = ZipList' [a]                    deriving (Eq, Show)  instance Functor ZipList' where   fmap f (ZipList' xs) = ZipList' $ fmap f xs  instance Applicative ZipList' where   pure x = ZipList' [x]    ZipList' (f:fs) <*> ZipList' (x:xs) =      ZipList' $ f x : (fs <*> xs) -- <-- the bug is here   ZipList' [] <*> _ = ZipList' []   _ <*> ZipList' []  = ZipList' []  -- Unexpected result: ZipList' [-1,2,3,4,5] 

After some head scratching, I realized that inside the applicative instance of ZipList' I accidentally used the wrong <*>:

In the line marked with the bug is here, I applied the <*> that belongs to the built-in list type [] instead of applying <*> of ZipList' recursively.

This is why the second function id was applied to the entire rest of the list, instead of only the second element, 2.

This yielded the expected result:

ZipList' fs <*> ZipList' xs = ZipList' $ zipApply fs xs   where zipApply :: [(a -> b)] -> [a] -> [b]         zipApply (f:fs) (x:xs) = f x : zipApply fs xs         zipApply _ _           = [] 

Is there a compiler flag, language idiom, or other technique that would have prevented this bug or would have made it easier to spot?

I'm on GHC 8.2.2.


We can do this:

{-# LANGUAGE PatternSynonyms, ViewPatterns #-} -- at very top of file ^ -- ... -- pick whatever names/operators you want -- synonym signatures are given in GADT-like syntax -- ZCons decomposes a ZipList' a into an a and a ZipList' a -- (assuming it succeeds). This is the syntax even for pattern synonyms that -- can only be used as patterns -- (e.g. pattern Fst :: a -> (a, b); pattern Fst a <- (a, _)). pattern ZCons :: a -> ZipList' a -> ZipList' a -- xs needs to be a ZipList', but it's only a [a], so we uglify this synonym -- by using the newtype wrapper as a view pattern ZCons x xs <- ZipList' (x:(ZipList' -> xs)) -- views aren't in general invertible, so we cannot make this an automatically -- bidirectional synonym (like ZNil is). We can give an explicit version   where ZCons x (ZipList' xs) = ZipList' $ x:xs -- simple enough that we can use one definition for both pattern and expression pattern ZNil :: ZipList' a pattern ZNil = ZipList' [] {-# COMPLETE ZNil, ZCons #-} -- ZNil and ZCons cover all ZipLists  instance Applicative ZipList' where   pure x = ZipList' $ repeat x   -- these are bidirectional   (ZCons f fs) <*> (ZCons x xs) = ZCons (f x) (fs <*> xs)   _ <*> _ = ZNil 

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: