0

I am trying to combine a list of pairs in scheme to get all possible combinations. For example:

((1 2) (3 4) (5 6)) --> ((1 3 5) (1 3 6) (1 4 5) (1 4 6) (2 3 5) (2 3 6) (2 4 5) (2 4 6))

I've been able to solve it (I think) using a "take the first and prepend it to the cdr of the procedure" with the following:

(define (combine-pair-with-list-of-pairs P Lp)
  (apply append
         (map (lambda (num)
                (map (lambda (pair)
                       (cons num pair)) Lp)) P)))

(define (comb-N Lp)
  (if (null? Lp)
      '(())
      (combine-pair-with-list-of-pairs (car Lp) (comb-N (cdr Lp)))))

(comb-N '((1 2)(3 4)(5 6)))
; ((1 3 5) (1 3 6) (1 4 5) (1 4 6) (2 3 5) (2 3 6) (2 4 5) (2 4 6))

However, I've been having trouble figuring out how I can use a procedure that only takes two and having a wrapper around it to be able to define comb-N by calling that function. Here it is:

(define (combinations L1 L2)
  (apply append
         (map (lambda (L1_item)
                (map (lambda (L2_item)
                       (list L1_item L2_item))
                     L2))
              L1)))
(combinations '(1) '(1 2 3))
; ((1 1) (1 2) (1 3))

I suppose the difficulty with calling this function is it expects two lists, and the recursive call is expecting a list of lists as the second argument. How could I call this combinations function to define comb-N?

David542
  • 104,438
  • 178
  • 489
  • 842
  • 2
    You should use recursion to solve problems because there is a natural way to think recursively about that problem. Your first solution, [which is the same as the one I wrote for you here](https://stackoverflow.com/a/67955197/6879826), does break the problem down in a natural way by combining the first list with the combinations for the remaining lists. It does not seem natural to combine an arbitrary number of lists with the combinations for a pair of lists. I can see a way to do it, using a `prepend` procedure again, but that is a lot less elegant that the solution we already have. – ad absurdum Jun 25 '21 at 03:01
  • Also note that `(1 2 3)` in `(combinations ((1) (1 2 3))` is not a pair; it is in the trivial sense that all lists are pairs in Scheme, but it isn't in any meaningful way when talking about combinatorics. `combine-pair-with-list-of-pairs` isn't a very good name, since it is designed to work with lists and lists of lists. – ad absurdum Jun 25 '21 at 03:04
  • @adabsurdum ah that's a great answer, I think I'm finally at the point where I can read your answer and it'll hopefully make sense to me now (I remember when asking it a lot of the terms/techniques you used were quite over my head) so I'll take a look at that one again tomorrow. Thanks again! – David542 Jun 25 '21 at 03:45
  • Feel free to ping me over there if you have any more questions ;) – ad absurdum Jun 25 '21 at 03:48
  • @adabsurdum wow I just read your answer and, minus my silly naming, the two answers are almost identical! The only difference, of course, is mine took about a week for me to figure it out and for you maybe 10 minutes :) – David542 Jun 25 '21 at 03:56
  • 1
    Since you've asked questions about `flatmap` somewhat recently, I'll point out that you've missed a chance to use it here. `(apply append (map f xs))` is always better written as `(flatmap f xs)`. – amalloy Jun 25 '21 at 07:53
  • @amalloy `(flatmap func series)` is the same as doing `(apply append...(map func series)` thought right? I like adding the `apply append` at the outermost level based on the number of times I need to unnest, but yes I'll test that out, thank you for the suggestion. – David542 Jun 25 '21 at 19:16
  • 1
    I think that's what I said. They do the same thing, but flatmap expresses the idea more clearly. – amalloy Jun 25 '21 at 19:18

2 Answers2

3

difficulty? recursion? where?

You can write combinations using delimited continuations. Here we represent an ambiguous computation by writing amb. The expression bounded by reset will run once for each argument supplied to amb -

(define (amb . lst)
  (shift k (append-map k lst)))

(reset
  (list (list (amb 'a 'b) (amb 1 2 3))))
((a 1) (a 2) (a 3) (b 1) (b 2) (b 3))

how it works

The expression is evaluated through the first amb where the continuation is captured to k -

k := (list (list ... (amb 1 2 3)))

Where applying k will supply its argument to the "hole" left by amb's call to shift, represented by ... above. We can effectively think of k in terms of a lambda -

k := (lambda (x) (list (list x (amb 1 2 3)))

amb returns an append-map expression -

(append-map k '(a b))

Where append-map will apply k to each element of the input list, '(a b), and append the results. This effectively translates to -

(append
 (k 'a)
 (k 'b))

Next expand the continuation, k, in place -

(append
 (list (list 'a (amb 1 2 3)))  ; <-
 (list (list 'b (amb 1 2 3)))) ; <-

Continuing with the evaluation, we evaluate the next amb. The pattern is continued. amb's call to shift captures the current continuation to k, but this time the continuation has evolved a bit -

k := (list (list 'a ...))

Again, we can think of k in terms of lambda -

k := (lambda (x) (list (list 'a x)))

And amb returns an append-map expression -

(append
 (append-map k '(1 2 3)) ; <-
 (list (list 'b ...)))

We can continue working like this to resolve the entire computation. append-map applies k to each element of the input and appends the results, effectively translating to -

(append
 (append (k 1) (k 2) (k 3)) ; <-
 (list (list 'b ...)))

Expand the k in place -

(append
  (append
     (list (list 'a 1))  ; <-
     (list (list 'a 2))  ; <-
     (list (list 'a 3))) ; <-
  (list (list 'b (amb 1 2 3))))

We can really start to see where this is going now. We can simplify the above expression to -

(append
 '((a 1) (a 2) (a 3)) ; <-
 (list (list 'b (amb 1 2 3))))

Evaluation now continues to the final amb expression. We will follow the pattern one more time. Here amb's call to shift captures the current continuation as k -

k := (list (list 'b ...))

In lambda terms, we think of k as -

k := (lambda (x) (list (list 'b x)))

amb returns an append-map expression -

(append
 '((a 1) (a 2) (a 3))
 (append-map k '(1 2 3))) ; <-

append-map applies k to each element and appends the results. This translates to -

(append
 '((a 1) (a 2) (a 3))
 (append (k 1) (k 2) (k 3))) ; <-

Expand k in place -

(append
 '((a 1) (a 2) (a 3))
 (append
  (list (list 'b 1))   ; <-
  (list (list 'b 2))   ; <-
  (list (list 'b 3)))) ; <-

This simplifies to -

(append
 '((a 1) (a 2) (a 3))
 '((b 1) (b 2) (b 3))) ; <-

And finally we can compute the outermost append, producing the output -

((a 1) (a 2) (a 3) (b 1) (b 2) (b 3))

generalizing a procedure

Above we used fixed inputs, '(a b) and '(1 2 3). We could make a generic combinations procedure which applies amb to its input arguments -

(define (combinations a b)
  (reset
   (list (list (apply amb a) (apply amb b)))))
(combinations '(a b) '(1 2 3))
((a 1) (a 2) (a 3) (b 1) (b 2) (b 3))

Now we can easily expand this idea to accept any number of input lists. We write a variadic combinations procedure by taking a list of lists and map over it, applying amb to each -

(define (combinations . lsts)
  (reset
     (list (map (lambda (each) (apply amb each)) lsts))))
(combinations '(1 2) '(3 4) '(5 6))
((1 3 5) (1 3 6) (1 4 5) (1 4 6) (2 3 5) (2 3 6) (2 4 5) (2 4 6))

Any number of lists of any length can be used -

(combinations
  '(common rare)
  '(air ground)
  '(electric ice bug)
  '(monster))
((common air electric monster)
 (common air ice monster)
 (common air bug monster)
 (common ground electric monster)
 (common ground ice monster)
 (common ground bug monster)
 (rare air electric monster)
 (rare air ice monster)
 (rare air bug monster)
 (rare ground electric monster)
 (rare ground ice monster)
 (rare ground bug monster))

related reading

In Scheme, we can use Olivier Danvy's original implementation of shift/reset. In Racket, they are supplied via racket/control

(define-syntax reset
  (syntax-rules ()
    ((_ ?e) (reset-thunk (lambda () ?e)))))

(define-syntax shift
  (syntax-rules ()
    ((_ ?k ?e) (call/ct (lambda (?k) ?e)))))

(define *meta-continuation*
  (lambda (v)
    (error "You forgot the top-level reset...")))

(define abort
  (lambda (v)
    (*meta-continuation* v)))

(define reset-thunk
  (lambda (t)
    (let ((mc *meta-continuation*))
      (call-with-current-continuation
        (lambda (k)
      (begin
        (set! *meta-continuation* (lambda (v)
                    (begin
                      (set! *meta-continuation* mc)
                      (k v))))
        (abort (t))))))))

(define call/ct
  (lambda (f)
    (call-with-current-continuation
      (lambda (k)
    (abort (f (lambda (v)
            (reset (k v)))))))))

For more insight on the use of append-map and amb, see this answer to your another one of your questions.

See also the Compoasable Continuations Tutorial on the Scheme Wiki.

remarks

I really struggled with functional style at first. I cut my teeth on imperative style and it took me some time to see recursion as the "natural" way of thinking to solve problems in a functional way. However I offer this post in hopes to provoke you to reach for even higher orders of thinking and reasoning. Recursion is the topic I write about most on this site but I'm here saying that sometimes even more creative, imaginative, declarative ways exist to express your programs.

First-class continuations can turn your program inside-out, allowing you to write a program which manipulates, consumes, and multiplies itself. It's a sophisticated level of control that's part of the Scheme spec but only fully supported in a few other languages. Like recursion, continuations are a tough nut to crack, but once you "see", you wish you would've learned them earlier.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    @adabsurdum thanks for the nudge. I found some time to expand this post. It was challenging to represent my understanding of a complex topic using only ascii, but I will be pleased if others find it helpful. – Mulan Jun 27 '21 at 06:46
  • Thanks for expanding your thoughts in this answer. I do have some _minor_ quibbles, but as a provocation to "_even higher orders of thinking and reasoning_" this seems like a useful answer. I don't mean these quibbles to take away from a good and interesting answer.... – ad absurdum Jun 27 '21 at 15:35
  • The quibbles: 1) Shouldn't `(append-map k '(a b))` --> `(append-map k (list 'a 'b))`, and similarly for `(append-map k '(1 2 3))`? The result is the same in this case, but calling `(amb 'a 'b)` will create a fresh list, evaluating the arguments to `amb` and likely involving more consing than if a list literal were passed. 2) Standard Scheme has continuations, but not delimited continuations; it's a bit of work to get the machinery in place for this. – ad absurdum Jun 27 '21 at 15:35
  • 3) Continuations can come with a performance penalty. For Racket, performance with regular continuations has dramatically improved since Racket CS was rolled out, but unless something has changed recently, Racket delimited continuations are "slow"; I wouldn't expect ad hoc delimited continuations to perform any better than what Racket ships. Calculations involving combinations can blow up quickly with moderately large inputs, and I'd be wary of using an approach that comes with performance disadvantages in such cases. – ad absurdum Jun 27 '21 at 15:35
  • 1
    @adabsurdum combinatorics done by actually creating full output as lists in strict languages, with guaranteed exponential space, is highly problematic anyway. – Will Ness Jun 27 '21 at 18:18
  • 1
    @WillNess -- agreed. Still, continuations can be costly and it is good to be aware of that. Wasn't it Perlis who said: "Lisp programmers know the value of everything and the cost of nothing?" – ad absurdum Jun 27 '21 at 18:21
  • @adabsurdum i appreciate the thorough review. 1) fair point. `(deifne (amb . lst) ...)` gathers the arguments in `lst` so I simplified some of these steps to keep the focus on how the continuations actually work. I could've supplied a different `amb` for this answer, but this technique was introduced to OP in other questions, I chose to keep this one. I approximated some of the other steps like `(append (k 'a) (k 'b))` that may differ depending on the `append-map` implementation used. 2) I agree linked content should inlined. I will supply `shift` and `reset` in an update. – Mulan Jun 29 '21 at 13:40
  • 3) In general I try to cultivate a strong ability to visualize a procedure's process and understand cost. I almost never use `append` and I acknowledge there's a lot of extra `cons`ing happening. When breaching higher level concepts it's sometimes hard to know exactly where I draw the boundaries for a particular answer. I wanted to expand this post to include other uses of continuations, perhaps demonstrating a streaming example as well. I will try to make the time for it. -- Many thanks for the discussion. I highly value your input. – Mulan Jun 29 '21 at 13:56
2

As suggested in the comments you can use recursion, specifically, right fold:

(define (flatmap foo xs)
  (apply append
    (map foo xs)))

(define (flatmapOn xs foo)
  (flatmap foo xs))

(define (mapOn xs foo)
  (map foo xs))

(define (combs L1 L2)  ; your "combinations", shorter name
  (flatmapOn L1 (lambda (L1_item)
     (mapOn L2   (lambda (L2_item)        ; changed this:
             (cons L1_item L2_item))))))  ;   cons     NB!

(display
 (combs '(1 2)
   (combs '(3 4)
     (combs '(5 6) '( () )))))

; returns:
; ((1 3 5) (1 3 6) (1 4 5) (1 4 6) (2 3 5) (2 3 6) (2 4 5) (2 4 6))

So you see, the list that you used there wasn't quite right, I changed it back to cons (and thus it becomes fully the same as combine-pair-with-list-of-pairs). That way it becomes extensible: (list 3 (list 2 1)) isn't nice but (cons 3 (cons 2 (cons 1 '()))) is nicer.

With list it can't be used as you wished: such function receives lists of elements, and produces lists of lists of elements. This kind of output can't be used as the expected kind of input in another invocation of that function -- it would produce different kind of results. To build many by combining only two each time, that combination must produce the same kind of output as the two inputs. It's like +, with numbers. So either stay with the cons, or change the combination function completely.

As to my remark about right fold: that's the structure of the nested calls to combs in my example above. It can be used to define this function as

(define (sequence lists)
  (foldr
     (lambda (list r)   ; r is the recursive result
        (combs list r))
     '(())              ; using `()` as the base
     lists))

Yes, the proper name of this function is sequence (well, it's the one used in Haskell).

Will Ness
  • 70,110
  • 9
  • 98
  • 181