5
picks :: [a] -> [(a, [a])]
picks [] = []
picks (x:xs) = (x,xs) : [(y,x:ys)| (y,ys) <- picks xs]

picks [1..4] = [(1,[2,3,4]),(2,[1,3,4]),(3,[1,2,4]),(4,[1,2,3])]

This Haskell function works like a charm, but why? The first two tuples in the list are obvious enough, but how is the rest build up, just cracks my brain.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
Lau Sandt
  • 119
  • 7
  • 3
    Why are the first two tuples in the list obvious to you? Could you explain your reasoning as to why the first two tuples are what they are? – Mor A. Sep 06 '21 at 14:29
  • So `picks [1..4]`, matching `(x:xs)`, is a list where first tuple is `(x, xs)` and the rest of the list are tuples of the form `(y,x:ys)`; that is, `x` will be the head of the list -second element of the tuple- in all of them; `y` and `ys` will come from matching `pics [2..4]`; **now repeat the exercise above remembering that the results will form tuples of the type `(y,x:ys)`**. – rturrado Sep 06 '21 at 14:39
  • 1
    the first rule about understanding how a recursive function works is, we do not talk about _how_ a recursive function works. – Will Ness Sep 06 '21 at 15:09
  • the second rule about understanding how a recursive function works is, we *do not talk* about ***how*** a recursive function works. – Will Ness Sep 06 '21 at 15:29
  • 3
    Or as one of my college professors put it, *trust* your recursion. `picks` chooses an element from its argument. How do you choose an element from a part of the argument? Trust that `picks` works and use it! – chepner Sep 06 '21 at 15:35
  • @Willem the title is ungrammatical. Haskell picks a function? the use of backticks in titles _is_ sanctioned, on meta, was my understanding. – Will Ness Sep 07 '21 at 04:22
  • @WillNess: it has been asked several times to allow Markup syntax for titles, and each time it was declined, see https://meta.stackoverflow.com/questions/285727/allow-some-markdown-in-question-titles https://meta.stackoverflow.com/questions/405985/whats-wrong-with-quoting-code-or-code-snippets-in-question-titles-with-markdo?noredirect=1&lq=1 and https://meta.stackoverflow.com/questions/363913/support-backtick-code-in-titles – Willem Van Onsem Sep 07 '21 at 08:20
  • @WillemVanOnsem hmm. your first link doesn't apply (talks about rendering, not presence). your 2nd link I saw before and interpreted as advocating for the md syntax in titles (it has a healthy uv balance in the +30s, and the top answer doesn't say it should be forbidden, nor does the 2nd top answer). the entry as a whole seems to me to say "if it adds clarity, do it". the 3rd link again does not apply at all, talks about rendering the md. but your edit was a definite improvement. – Will Ness Sep 07 '21 at 09:17
  • @WillNess: well it does not make much sense to add backticks if these are not rendered as code fragments, then it is just "pollution". As for the second link, it says that it is a form of code smell, since the title of a question should not contain code in order to make itself clear. The fact that things have a healty reputation balance is not that odd. Usually after a while the only peoople that do a +1 is people that search the same. I don't really understand why titles should be decorated with backquotes, if it is clear that in the (near) future, it will not do code rendering. – Willem Van Onsem Sep 07 '21 at 09:21
  • @WillemVanOnsem I interpret it as adding clarity while being very short. every reader of SO knows the backticks indicate a function (/ variable /) name. without it we're forced to writing much words instead of just two characters. – Will Ness Sep 07 '21 at 09:24
  • @WillNess: unfortunately that is not true. That is one of the main reasons why a lot of questions need all sorts of editing. Especially since there are dialects of Markdown like *reStructuredText* and *Textile*, and the fact that, oddly enough, there is no standardized grammar for Markdown. – Willem Van Onsem Sep 07 '21 at 09:28
  • @WillemVanOnsem you mean not every reader of SO knows about the backticks use to enclose code? hmm... perhaps, but then I assume it's still clear for them it is some form of quotation, just like with the regular double quote chars.... – Will Ness Sep 07 '21 at 09:32

5 Answers5

6

What does picks do? It returns all possible ways to choose one element from the list, in the form of a tuple (choice, rest), where choice is the item you chose and rest is the elements you did not choose. Note that [choice] ++ rest always contains the same elements as the original list, though not necessarily in the same order.

So how does picks work? When the argument is empty, it's simple: there are no ways to choose one element, so we return the empty list.

picks [] = []

When the argument is not empty, we can do one of two things. Either x is the first element of the tuple, or it is part of the second element. The easy thing to do is pick the first element; we unpack the list with (x:xs) and produce (x, xs).

picks (x:xs) = (x, xs) : ?

The other thing we can do is not pick x, but instead pick an element from xs. How do choose an element from xs? We use picks! This time, picks returns a list of tuples of where x is neither the first element nor a member of the second element. We just combine (x, xs) with this list.

-- x != y, x `elem` ys == False
picks (x:xs) = (x, xs) : [ (y, ?) | (y, ys) <- picks xs]

But x does need to be an member of the second element, because it's not the first element. So we have to put it back. The easiest place to put it is at the beginning of ys in each case:

picks (x:xs) = (x, xs) : [ (y, x:ys) | (y, ys) <- picks xs]
chepner
  • 497,756
  • 71
  • 530
  • 681
5

In many cases it helps to expand an expression manually:

picks [1..4] = (1, [2..4]) : [(y, 1:ys) | (y, ys) <- picks [2..4]]

  -- continuing with picks [2..4]
  picks [2..4] = (2, [3..4]) : [(y, 2:ys) | (y, ys) <- picks [3..4]]

    -- continuing with picks [3..4]
    picks [3, 4] = (3, [4]) : [(y, 3:ys) | (y, ys) <- picks [4]]

      -- continuing with picks [4]
      picks [4] = (4, []) : [(y, 4:ys) | (y, ys) <- picks []]
                = (4, []) : [(y, 4:ys) | (y, ys) <- []]
                = (4, []) : []
                = [(4, [])]

    picks [3, 4] = (3, [4]) : [(y, 3:ys) | (y, ys) <- [(4, [])]]
                 = (3, [4]) : [(4, 3:[])]
                 = (3, [4]) : [(4, [3])]
                 = [(3, [4]), (4, [3])]

-- and so one
Micha Wiedenmann
  • 19,979
  • 21
  • 92
  • 137
  • the use of digits is a nice touch, makes it that much clearer. so I swiped it to use in my answer as well! :) – Will Ness Sep 06 '21 at 18:26
  • Indeed it does help a lot, I did so but on a list of [1,2,3,4] it is the reconstruction that get's hairy. – Lau Sandt Sep 07 '21 at 09:12
  • @LauSandt the thing to do is this: try to see, assuming it does the right thing on `[2,3,4]`, does it do the right thing on `[1,2,3,4]`? – Will Ness Sep 07 '21 at 09:26
4

The recursive case will return a list where the first item (x, xs) return a 2-tuple with as first item (the item we have picked) x, and with as remaining items the picks we make on the tail of the list and prepend all these items with x.

If we run this on a singleton list, for example [1], we thus get as options:

picks [1] = (1, []) : [(y, 1: ys) | (y,ys) <- picks []]

since picks [] is equivalent to [], this thus means that we retrieve:

picks [1] = (1, []) : [(y, 1: ys) | (y,ys) <- []]

hence the list comprehension will generate an empty list, and thus the result of picks [1] is:

picks [1] = [(1, [])]

If we now work with lists with two elements, the recursive call will return a list with one element: the only element of the tail and an empty list.

This thus means that if we run picks on picks [1,2], we thus get:

picks [1, 2] = (1, [2]) : [(y, 1:ys) | (y,ys) <- picks [2]]

and since picks [2] returns [(2, [])], we thus will prepend the empty list in the (2, []) tuple with one, and we thus obtain:

picks [1, 2] = (1, [2]) : [(y, 1:ys) | (y,ys) <- [(2, [])]]
             = (1, [2]) : [(2, [1])]
             = [(1, [2]), (2, [1])]

The x:ys part in the list comprehension will prepend the head of the list, so 1 to the lists returned by picks [2]. Since is not picked by the recursive call (we call picks recursively on the tail of the item), we thus need to insert it somewhere in the list, and the most easiest way is to prepend it.

If we thus work with three items, we will retrieve the data as:

picks [1, 2, 3] = (1, [2, 3]) : [(y, 1:ys) | (y,ys) <- picks [2, 3]]
                = (1, [2]) : [(y, 1:ys) | (y,ys) <- [(2, [3]), (3, [2])]]
                = (1, [2]) : [(2, [1, 3]), (3, [1, 2])]
                = [(1, [2]), (2, [1, 3]), (3, [1, 2])]

and so for lists with more than three items, the recursion each time will prepend the item that is definitely not picked by the recursive tail of picks xs, and prepend the lists of non-picked items with x.

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
  • 1
    This is a very useful answer thanks. To expand the expression manually is still less clear than to see it with a singleton list. Many Thanks! – Lau Sandt Sep 07 '21 at 09:08
4

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.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • 2
    The fight club reference is a cute joke, but you just wrote a whole answer talking about how it works. Recursion isn't magic; beginners need help demystifying it, not to be told that understanding how it works is pointless. – Ben Sep 06 '21 at 21:48
  • 1
    @Ben I think there is wisdom in "we do not talk about *how*". My personal experience is that figuring out the how is often too difficult but assuming the recursive function does what it does helped me demystify it. So I think the rule is the main advice I would give. – Micha Wiedenmann Sep 06 '21 at 23:08
  • 1
    @MichaWiedenmann The advice I would give is: stop thinking of recursive calls as different from any other kind of function calls, they work by exactly the same rules. It's true that a common thought pattern for *building* recursive functions is "assume my function already works and solves a sub-problem, now use that solution to solve the whole problem". But the answer to "how did it solve the subproblem" is the code you just wrote, there's no magic. You might have to unroll several layers to see all of the concrete steps, but how is that any different with other similar complexity functions? – Ben Sep 06 '21 at 23:22
  • @Ben a little cognitive dissonance can be a good motivator for further exploration. I've included a link in the answer to several other answers of mine with a lot of much more detailed explanations in them. "unrolling several layers" is exactly the mystery which I refer to as "_how_", and call to avoid, focusing instead on proving the one inductive step. – Will Ness Sep 07 '21 at 03:18
  • 1
    @Ben I've edited, rewording and restructuring it a bit. swiped your "unrolling the layers", too. :) thanks. – Will Ness Sep 07 '21 at 03:49
  • 1
    Wow this great answer. indeed not using a list comprehension is a much clearer way for achieving the same result and you are right of course about picks being quadratic, I think you solved a conundrum with that so many thanks! – Lau Sandt Sep 07 '21 at 09:19
  • @LauSandt be sure to browse through my other answers that I linked. some contain much discussion about this stuff. :) – Will Ness Sep 07 '21 at 09:22
2

Without brain - cracking, simple to explain (but Eq a =>), has approximately 1.7 slower execution time trade off

mypicks :: Eq a => [a] -> [(a,[a])]
mypicks [] = []
mypicks lst = [ zsplit x lst | x <- lst]

zsplit :: Eq a => a -> [a] -> (a,[a])
zsplit z lst = esplit z lst []
    where
        esplit :: Eq a => a -> [a] -> [a] -> (a, [a])
        esplit e [] lr = (e, lr)
        esplit e (x:xs) lr
            | e == x    = (e, reverse lr ++ xs)
            | otherwise = esplit e xs (x:lr)

--- using
perms2 [] = [[]]
perms2 xs = [x:zs | (x,ys) <- picks xs, zs <- perms2 ys]

perms2' [] = [[]]
perms2' xs = [x:zs | (x,ys) <- mypicks xs, zs <- perms2' ys]
--- and
T.measureTime $ length $ perms2  [1..9]
T.measureTime $ length $ perms2' [1..9]
--- delivers:
e: 362880
Computation time: 0.075562000 sec.
e: 362880
Computation time: 0.125598000 sec.
ludwig
  • 36
  • 1
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Dec 25 '21 at 18:07