do notation and bind signature
I think the reason it's confusing is because you're working on this inside-out, by trying to think of the inner bind as producing a list of partially applied functions. It doesn't: a
and b
are closed over, not arguments waiting to be applied. Instead, start from the outside of the expression and work inwards:
[1, 2, 3] >>= \a -> (...)
For each item in the list, produce a list somehow, with access to a
as a name for an item in the original list
... [4, 5, 6] >>= \b -> (...)
To produce the list needed by the previous step, produce a new list with access to both a
and b
, one from each of the two numbered lists.
... return $ b * 10 + a
To produce the list needed by the previous step, create a list of a single item, whose value is b * 10 + a
.
You ask why the type of return $ b * 10 + a
is different from the type of [40 + a, 50 + a, 60 + a]
, but they're not: both are of type [Int]
. Neither involves any functions. Rather, they both are lists of numbers, constructed by referring to already-closed-over variables. And indeed (>>=)
has exactly the type that it should: it takes a list of int, and a function for producing a list of int from a single int, and gives back a different list of int:
(>>=) :: [Int] -> (Int -> [Int]) -> [Int]
Here’s how it desugars and works operationally. You’re right that this:
do
a <- [1, 2, 3]
b <- [4, 5, 6]
return $ a * 10 + b
Desugars to this:
[1, 2, 3] >>= \a ->
[4, 5, 6] >>= \b ->
return $ b * 10 + a
Which in turn is using the list instance of Monad
, whose definitions of >>=
and return
(or pure
) we can inline:
concatMap
(\a -> concatMap
(\b -> [b * 10 + a])
[4, 5, 6])
[1, 2, 3]
We can break apart concatMap
into concat
and map
:
concat
(map
(\a -> concat
(map
(\b -> [b * 10 + a])
[4, 5, 6]))
[1, 2, 3])
Now we can reduce this, and here I think is where you were encountering difficulty: reduction happens from the outside in, and doesn’t produce partially applied functions in this case; rather, it captures a
in the closure of the inner lambda (\b -> …)
. First, we map (\a -> …)
over [1, 2, 3]
:
concat
[ (\a -> concat
(map
(\b -> [b * 10 + a])
[4, 5, 6])) 1
, (\a -> concat
(map
(\b -> [b * 10 + a])
[4, 5, 6])) 2
, (\a -> concat
(map
(\b -> [b * 10 + a])
[4, 5, 6])) 3
]
==
concat
[ let a = 1
in concat
(map
(\b -> [b * 10 + a])
[4, 5, 6])
, let a = 2
in concat
(map
(\b -> [b * 10 + a])
[4, 5, 6])
, let a = 3
in concat
(map
(\b -> [b * 10 + a])
[4, 5, 6])
]
Then we can reduce the inner map
s:
concat
[ let a = 1
in concat
[ (\b -> [b * 10 + a]) 4
, (\b -> [b * 10 + a]) 5
, (\b -> [b * 10 + a]) 6
]
, let a = 2
in concat
[ (\b -> [b * 10 + a]) 4
, (\b -> [b * 10 + a]) 5
, (\b -> [b * 10 + a]) 6
]
, let a = 3
in concat
[ (\b -> [b * 10 + a]) 4
, (\b -> [b * 10 + a]) 5
, (\b -> [b * 10 + a]) 6
]
]
==
concat
[ let a = 1
in concat
[ let b = 4 in [b * 10 + a]
, let b = 5 in [b * 10 + a]
, let b = 6 in [b * 10 + a]
]
, let a = 2
in concat
[ let b = 4 in [b * 10 + a]
, let b = 5 in [b * 10 + a]
, let b = 6 in [b * 10 + a]
]
, let a = 3
in concat
[ let b = 4 in [b * 10 + a]
, let b = 5 in [b * 10 + a]
, let b = 6 in [b * 10 + a]
]
]
Which we can then simplify by replacing variables with their values:
concat
[ concat
[ [4 * 10 + 1]
, [5 * 10 + 1]
, [6 * 10 + 1]
]
, concat
[ [4 * 10 + 2]
, [5 * 10 + 2]
, [6 * 10 + 2]
]
, concat
[ [4 * 10 + 3]
, [5 * 10 + 3]
, [6 * 10 + 3]
]
]
And reducing the calls to concat
:
concat
[ [ 4 * 10 + 1
, 5 * 10 + 1
, 6 * 10 + 1
]
, [ 4 * 10 + 2
, 5 * 10 + 2
, 6 * 10 + 2
]
, [ 4 * 10 + 3
, 5 * 10 + 3
, 6 * 10 + 3
]
]
==
[ 4 * 10 + 1
, 5 * 10 + 1
, 6 * 10 + 1
, 4 * 10 + 2
, 5 * 10 + 2
, 6 * 10 + 2
, 4 * 10 + 3
, 5 * 10 + 3
, 6 * 10 + 3
]
And of course the individual expressions:
[ 41, 51, 61
, 42, 52, 62
, 43, 53, 63
]
A case where you will see a list of partially applied functions is when using the Applicative
instance of lists, for example, the equivalent to your code:
(\a b -> b * 10 + a) <$> [1, 2, 3] <*> [4, 5, 6]
The definition of <$>
/fmap
for lists is just map
, so we partially apply the first argument of the lambda, producing a list of type [Int -> Int]
, then (<*>) :: (Applicative f) => f (a -> b) -> f a -> f b
, here at type [Int -> Int] -> [Int] -> [Int]
, applies each function in its left operand to each value in its right operand.