1

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]
developer_hatch
  • 15,898
  • 3
  • 42
  • 75
  • 1
    That's basically the definition of the Applicative instance for lists. It's not special to `(++)` or to lists of lists: `liftA2 f xs ys` is always equivalent to `[f x y | x <- xs, y <- ys]`. – Robin Zigmond Nov 05 '19 at 21:31
  • In fact, on checking the documentation, I'm slightly surprised to find that [explicitly stated in the source code](http://hackage.haskell.org/package/base-4.12.0.0/docs/src/GHC.Base.html#line-972). (I had expected only `<*>` to be explicitly defined in the instance, and to have to derive the correct formula for `liftA2` from it.) – Robin Zigmond Nov 05 '19 at 21:32
  • GHC implements the `[]` instance in terms of list comprehensions, making the required equivalence trivial. – chepner Nov 05 '19 at 22:08
  • 1
    Though tedious, you can derive the equivalence using section 3.11 of the [Haskell Report](https://www.haskell.org/definition/haskell2010.pdf) and the definition of the `Applicative` instance for `[]`. – chepner Nov 06 '19 at 00:01

2 Answers2

3

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 (.)
luqui
  • 59,485
  • 12
  • 145
  • 204
1

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.

Redu
  • 25,060
  • 6
  • 56
  • 76
  • 1
    I'm not sure why you lean so much on Monads when `liftA2` (or `<$>` and `<*>` if you prefer, in terms of which `liftA2` can be defined, as you said) is available for any Applicative. – Robin Zigmond Nov 05 '19 at 22:49
  • 2
    And I find it hard to understand what your (opinionated) last paragraph is really saying. What exactly "should be avoided"? Lists only form an applicative/monad because an instance is supplied, and that instance can be written in terms of a list comprehension. There are other ways of writing it down but they all come to the same thing. – Robin Zigmond Nov 05 '19 at 22:51
  • @Robin Zigmond It's OK to use `liftAn` functions of course only after one clearly understands what they simplify and their limitations. And yes... it's opiniated true.. but i don't like list comprehensions. I think there is no need to turn Haskell into JS where you have tons of different ways to do the very same thing, especially designed per type base. Wouldn't you agree..? – Redu Nov 05 '19 at 23:02
  • 2
    Are you getting Javascript confused with Python? JS has no list comprehensions or anything resembling them. And I don't see anything wrong with having different ways to achieve the same thing (that's one area where I disagree with the "Zen of Python", which thankfully I don't think the language really achieves). Do you dislike `do` notation for the same reason? (In regards to your first point, I probably wouldn't ever use a `liftAn` for `n` greater than 2, certainly not more than 3, but I don't have a particularly strong opinion.) – Robin Zigmond Nov 05 '19 at 23:14
  • 4
    `liftA2` is not an abstraction over monad; it's a *relaxation* of the monad abstraction. Monads make promises that applicatives do not, allowing additional optimizations available to the compiler. Haskell itself doesn't even define how a list comprehension should be implemented; the Haskell report only provides some equivalences (none of which involve monads or applicatives) that a list comprehension must obey. – chepner Nov 05 '19 at 23:21
  • 3
    And Python *borrowed* list comprehensions from Haskell, despite the fact that they didn't provide anything you couldn't already write in Python, in apparent violation of the Zen of Python. That should tell you something about the general perception of the expressiveness of list comprehensions. – chepner Nov 05 '19 at 23:24
  • @Robin Zigmond I had actually meant JS, not because of the list comprehensions but the mess it ended up with by the introduction of tons of BS just to make it feel *more* functional but also *more* comfy at the same time for some coders with old/other habbits. Yes i know list comprehensions is something original to Haskell.. however somehing ugly. And yes.. you have got me... about the do notation. I don't like it too. I think it is harmful. (may be i should keep this to myself) – Redu Nov 05 '19 at 23:26
  • I definitely think `do` notation *can* be "harmful", and is definitely overused (every time I see something like `main = do putStrLn "hello world"` in some beginners' text, as I often do, I die a little inside), but I think there's no denying that for a lot of monadic code it makes a vast improvement in readability. [And regarding JS, I'll commit blasphemy and admit I actually like it, despite its obvious warts - it's more consistent than most developers realise, and it's been "functional" (not in the Haskell sense o/c) since the very beginning. Scheme was one of its direct ancestors.] – Robin Zigmond Nov 05 '19 at 23:49
  • @chepner The *relaxation* part is a nice point. I will think about it. Thanks. – Redu Nov 06 '19 at 00:01