I don't quite understand why given two list of lists xss :: [[a]]
and yss :: [[a]]
liftA2 (++) xss yss
is equivalent to
[xs ++ ys | xs <- xss, ys <- yss]
I don't quite understand why given two list of lists xss :: [[a]]
and yss :: [[a]]
liftA2 (++) xss yss
is equivalent to
[xs ++ ys | xs <- xss, ys <- yss]
The reason is right here in the source code.
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <- fs, x <- xs]
liftA2 f xs ys = [f x y | x <- xs, y <- ys]
The liftA2
definition is an optimization, and we could also do it manually with the default definition of liftA2
:
liftA2 f x y = f <$> x <*> y
So
liftA2 (++) xs ys
= (++) <$> xs <*> ys
= fmap (++) xs <*> ys -- definition of <$>
= [ f y | f <- fmap (++) xs, y <- ys ] -- definition of <*> above
= [ (++) x y | x <- xs, y <- ys ] -- some law about fmap/bind
= [ x ++ y | x <- xs, y <- ys ]
There you have it.
"Some law about fmap/bind" is this one:
fmap f x >>= t = x >>= t . f
which applies if you understand how list comprehensions are desugared. The proof is:
fmap f x >>= t = x >>= pure . f >>= t -- fmap = liftM coherence = x >>= (\y -> pure (f y) >>= t) -- definition of (.) = x >>= (\y -> t (f y)) -- left unit monad law = x >>= t . f -- definition of (.)
This is exactly the reason why i don't like using liftA<2..n>
type of functions. They are an abstraction over the monad abstraction. It's just so because applicative is introduced after monads just to simplify to context of monads those contain functional values (functions).
Basically liftA2 (++) xs ys
is (++) <$> xs <*> ys
which makes more sense since it involves functor operator fmap
in it's inline form <$>
. Once you undertand the mechanics of the latter liftA2
starts to make sense.
fmap
just applies the (++)
function to the elements of the xs
list (assume that xs
is [[1,2],[3,4]]
) and turns it into an applicative list (a list that contains functions) such as;
[([1,2] ++), ([3,4] ++)] :: Num a => [[a] -> [a]]
and the applicative operator <*>
is now eligible to apply these functions in our list to another list which contains some other lists such as say, [[1,2],[3,4]]
.
At this very moment we have to know how exactly lists are handled monadically. Lists are undeterministic data types. So every single element of the first list has to be applied to the every single element of the second list. So
[([1,2] ++), ([3,4] ++)] <*> [[1,2],[3,4]]
turns out to be
[[1,2,1,2],[1,2,3,4],[3,4,1,2],[3,4,3,4]]
All in all liftA2 (++)
just lifts the simple (++)
function to the list monad. Simply saying, concatenate inner lists with each other monadically.
Having said that the list comprehensions version of this is a joke in Haskell. It is redundant and should be avoided in my honest opinion. It just takes a whole monad abstraction down to list level only whereas monadical approaches hold for all data types according to their appropriate monad instances.