First, what you really want is probably Default
, not Monoid
- you have no use for mappend
.
I don't think anything useful is possible in Applicative
itself. That said, I can define a version of (<*>)
(called (<#>)
) with extra constraints that lets me do what I think you have in mind.
Why there is no point in making a new data type
First, suppose we were to take the ExistentialQuantification
route in hopes of pushing our constraints into the data
and having legitimate instances of Functor
and Applicative
. That blows up as soon as we try to define fmap
:
{-# LANGUAGE ExistentialQuantification #-}
data ZipMonList a = Default a => ZipMonList [a]
-- Woops, we still need a constraint for `Default b`
fmap :: Default b => (a -> b) -> ZipMonList a -> ZipMonList b
fmap f (ZipMonList xs) = ZipMonList (f xs)
So, with that settled, let's stick to the ZipList
type (since we want the same (<$>)
anyways) and just define our new constrained version of (<*>)
, called (<#>)
.
Make (<#>)
for ZipList
Underlying ZipList
s (<*>)
is the zipWith
function. We need something similar for (<#>)
, but that extends lists. Then, (<#>)
looks a lot like (<*>)
:
import Control.Applicative (ZipList(..))
import Data.Default
-- Like 'zipWith', but has maximum length
zipWith' :: (Default a, Default b) => (a -> b -> c) -> [a] -> [b] -> [c]
zipWith' f [] [] = []
zipWith' f (x:xs) [] = f x def : zipWith' f xs []
zipWith' f [] (y:ys) = f def y : zipWith' f [] ys
zipWith' f (x:xs) (y:ys) = f x y : zipWith' f xs ys
-- same fixity as <*>
infixl 4 <#>
-- like '(<*>)', but uses 'zipWith'' instead of 'zipWith'
(<#>) :: (Default a, Default b) => ZipList (a -> b) -> ZipList a -> ZipList b
ZipList fs <#> ZipList xs = ZipList (zipWith' id fs xs)
And I can do a test run on tuples:
ghci> (,,) <$> ZipList [1.2,3.4,5.6,7.8,9.1] <#> ZipList [[(),()],[()],[(),(),()]] <#> ZipList [1,2,3,4]
ZipList {getZipList = [(1.2,[(),()],1),(3.4,[()],2),(5.6,[(),(),()],3),(7.8,[],4),(9.1,[],0)]}
Key takeaway point: this is not an Applicative
, but still doable.