This is a case where the formulation of the algorithm using higher order functions actually looks clearer than with a list comprehensions--based one:
picks [] = []
picks (x:xs) = -- (x,xs) : [(y, x:ys) | (y,ys) <- picks xs]
(x,xs) : map (second (x:)) (picks xs)
In case you don't understand what second (x:)
is, you can read it as a pseudocode: it applies (x:)
to the second part of a pair, so that second (x:) (a,b) = (a, x:b)
. And map
does so for every element in its argument list (the map
's second argument).
Thus we have, building our understanding from the ground up, stating with one-element lists, then two elements, three, and so on, to see the pattern:
picks ([1]) = picks (1:[]) =
-- picks (x:xs) --
= (x,xs) : map (second (x:)) (picks xs)
= (1,[]) : map (second (1:)) (picks [])
= (1,[]) : map (second (1:)) []
= (1,[]) : []
= [(1,[])]
picks [2,1] = picks (2:[1]) =
= (2,[1]) : map (second (2:)) (picks [1])
= (2,[1]) : map (second (2:)) [(1, [])]
= (2,[1]) : [(1,[2])]
= [(2,[1]) , (1,[2])]
picks [3,2,1] =
= (3,[2,1]) : map (second (3:)) (picks [2,1])
= (3,[2,1]) : map (second (3:)) [(2, [1]) , (1, [2])]
= [(3,[2,1]) , (2,[3,1]) , (1,[3,2])]
picks [4,3,2,1] =
= (4,[3,2,1]) : map (second (4:)) [(3,[2,1]) , (2,[3,1]) , (1,[3,2])]
= [(4,[3,2,1]) , (3,[4,2,1]) , (2,[4,3,1]) , (1,[4,3,2])]
picks [5,4,3,2,1] =
= [([5,[4,3,2,1]), (4,[5,3,2,1]), (3,[5,4,2,1]), (2,[5,4,3,1]), (1,[5,4,3,2])]
....
Putting them together to better see the pattern, the results are:
picks [ ] = [ ]
picks [ 1] = [ (1,[ ])]
picks [ 2,1] = [ (2,[ 1]) , (1,[ 2])]
picks [ 3,2,1] = [ (3,[ 2,1]) , (2,[ 3,1]) , (1,[ 3,2])]
picks [ 4,3,2,1] = [ (4,[ 3,2,1]) , (3,[ 4,2,1]) , (2,[ 4,3,1]) , (1,[ 4,3,2])]
picks [5,4,3,2,1] =
= [([5,[4,3,2,1]) , (4,[5,3,2,1]) , (3,[5,4,2,1]) , (2,[5,4,3,1]) , (1,[5,4,3,2])]
....
And so picks
produces all the ways to pick an element, pairing it up with the remaining elements in the list after the element is removed from it.
It evidently does so for the length 0 (empty) list case, []
, and the length 1 (singleton) case []
and the length 2 case [2,1]
as seen above; and if it does so for a list of length n
, then for the n+1
we know that it's right as well since it starts with the first pick, and then the map
adds the first element into each of the remainders in the result produced for the n
case. Which is correct.
Yes, you can read this as both "the n
case is correct" and "hence, n+1
is correct". Thus (and given the correctness of the 0
case) by the induction principle the results are correct for any n
. That is to say, for all of them. Yes there are infinitely many of them but each n
in itself is finite.
If the starting point is right, and each step is right, then the whole journey must be right as well. We don't need to understand how exactly it does what it does for an n
case, unrolling all the layers of recursion. That's hard. Instead, we prove the inductive step is right, the base case is right, and that's that.
Recursion is a leap of faith.
The three most important rules in trying to understand how does a recursive function exactly do what it does, are:
- The first rule is, we do not talk about how does a recursive function exactly do what it does.
- The second rule is, we do not talk about how does a recursive function exactly do what it does.
- The third rule is, we do not talk about how does a recursive function exactly do what it does.
Of course this version of picks
is not too good. It is quadratic, and it destroys information.
We can address both flaws at once with
-- unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
picks3 :: [a] -> [([a], a, [a])]
picks3 xs = unfoldr (\case { (_,[]) -> Nothing ;
(a,x:xs) -> Just ((a,x,xs), (x:a,xs)) })
([],xs)
So that
> picks3 [1..4]
[([],1,[2,3,4]),([1],2,[3,4]),([2,1],3,[4]),([3,2,1],4,[])]
Now this is linear, and it is easy to produce the output of picks
from it, if we so choose and are willing to pay the price.