2

I'd like to find out concise, functional and tail-recursive (if possible) way of implementing the below specified function:

(define (make-domain digits dimension)
    ;; Implementation)
;; Usage
(make-domain '(0 1) 0) => (())
(make-domain '(0 1) 1) => ((0) (1))
(make-domain '(0 1) 2) => ((0 0) (0 1) (1 0) (1 1))
(make-domain '(0 1) 3) => ((0 0 0) (0 0 1) (0 1 0) (0 1 1) (1 0 0) (1 0 1) (1 1 0) (1 1 1))

I'd prefer Scheme implementation with as few helper or library functions as possible, but SML or Haskell will do as well. I'm trying to find a tail-recursive solution possibly using mutual or nested recursion, but with no luck at the moment.

Thank you very much!

  • This is my hobby and my own investigation. What matters for me is the functional, tail-recursive algorithm (the idea), not the implementation langauge – Volodymyr Prokopyuk Jul 06 '20 at 21:07
  • 2
    It doesn't really matter whether it's a homework question. If it's well-asked, it's on topic here, and it's up to the asker to ensure they follow academic honesty constraints. If it's not well-asked, it should be downvoted, closed, or edited. In this case I don't think it's very well asked, because there is no effort or attempt made. This function has been implemented millions of times - what more can really be said about it "in general"? An answer would only be useful when paired with an explanation of what part you don't understand about the existing implementations. – amalloy Jul 06 '20 at 21:13
  • 2
    Also, this problem isn't suited to tail recursion. You can fake it by building your own stack on the heap, but ultimately you need `|m|^n` memory to hold the result, so avoiding `n` stack frames is not really going to make any serious difference. – amalloy Jul 06 '20 at 21:17
  • 2
    Tail recursion in Haskell is often a bad idea, leading to slower code, especially when producing lists. Anyway, even if it's probably not what you want, you could try `replicateM 5 "abc"` in GHCi, after importing `Control.Monad`. – chi Jul 06 '20 at 21:28
  • @chi maybe here the tail-recursive `foldl` with a lazy combining function [*is* the right tool after all](https://stackoverflow.com/questions/62764261/functional-tail-recursive-way-to-generate-all-possible-combinations-from-a-dict/62765370#comment111185721_62765370), building the *n* "nested loops" strictly, fully, which are then get to be explored lazily. the *strict loop* to build the *lazy* nested loops... (kind of thing Common Lisp prides itself on doing with macros -- build the code first, let it run thereafter) – Will Ness Jul 13 '20 at 13:01

4 Answers4

3

That one, in Haskell, is at least “functional” and concise (I think):

makeDomain :: [α] -> Int -> [[α]]
makeDomain xs 0  =  [[]]
makeDomain xs n  =  let  mdn1 = makeDomain xs (n-1)
                         fn x = map (x:) mdn1
                    in   concat (map fn xs)

Trying it:

 λ> 
 λ> makeDomain [0,1] 2
[[0,0],[0,1],[1,0],[1,1]]
 λ> 
 λ> makeDomain [0,1] 3
[[0,0,0],[0,0,1],[0,1,0],[0,1,1],[1,0,0],[1,0,1],[1,1,0],[1,1,1]]
 λ> 

As mentioned in the comments, going tail-recursive might be a not so good idea, in Haskell at least.

Addendum re: memory efficiency:

You did not list performance concerns in your requirements (was it because you think tail-recursive functions tend to perform better ?).

The above version of makeDomain, as hinted in the comments by amalloy suffers from exponential memory consumption, at least for some compiler versions / optimization levels. This is because the compiler can see makeDomain xs (n-1) as a loop-invariant value to be kept around.

So this is one of these situations where you have to pick a trade-off between elegance and efficiency. The problem has been discussed recently in this related SO question in the context of the very similar replicateM library function; drawing on the answer by K. A. Buhr, one can come up with a version of makeDomain that runs in constant memory, leveraging the Haskell list comprehension construct.

makeDomain1 :: [α] -> Int -> [[α]]
makeDomain1 xs n =
    map reverse (helper xs n)
        where
            helper xs 0 = [[]]
            helper xs n = [ x:ys  |  ys <- helper xs (n-1),  x <- xs ]

Testing: running with an OS-enforced memory hard limit of 1200 MB.

 λ> 
 λ> import Control.Monad (replicateM)
 λ> replicateM 3 [0,1]
[[0,0,0],[0,0,1],[0,1,0],[0,1,1],[1,0,0],[1,0,1],[1,1,0],[1,1,1]]
 λ> 
 λ> makeDomain1 [0,1] 3
[[0,0,0],[0,0,1],[0,1,0],[0,1,1],[1,0,0],[1,0,1],[1,1,0],[1,1,1]]
 λ> 
 λ> length $ replicateM 30 [0,1]
<interactive>: internal error: Unable to commit 1048576 bytes of memory
...
 λ> 
 λ> length $ makeDomain [0,1] 30
<interactive>: internal error: Unable to commit 1048576 bytes of memory
...
 λ> 
 λ> length $ makeDomain1 [0,1] 30
1073741824
 λ> 

Using GHC v8.6.5 with -O2 option, that last version never takes more than 150 MB memory, and runs at a speed of about 30 nsec per output list on a vanilla Intel x86-64 PC. This is perfectly reasonable.

jpmarinier
  • 4,427
  • 1
  • 10
  • 23
  • I wanted to know whether there is at all a concise, tail-recursive solution for this problem. Performance concerns where secondary for me. I've tried to solve the problem exclusively using recursion quite a lot of times, but with no good result. That is why I did not provided any failed attempt in the question. Thank you very much! – Volodymyr Prokopyuk Jul 07 '20 at 05:56
  • 2
    In Haskell, the lazily evaluated nature of the language means that tail recursion has no real advantage over non-tail recursion. – Aplet123 Jul 07 '20 at 21:20
  • @Aplet123 in some situations it doesn't, in other situations it does have an advantage (i.e. when calculating something by strict accumulation, like e.g. with `+`) – Will Ness Jul 07 '20 at 21:38
  • 1
    @VolodymyrProkopyuk if your question contained those failed attempt it most probably wouldn't have received as many downvotes, or at all. you can still edited them in. :) – Will Ness Jul 07 '20 at 21:41
  • 1
    @WillNess thank you for your support! I supposed that if I ask for help this is because I need it and many attempts have been make by myself, not because I'm lazy and want someone to solve my exam problem :). Anyway after some more thorough research I came to the solution that I provided below. – Volodymyr Prokopyuk Jul 08 '20 at 13:58
  • @VolodymyrProkopyuk unfortunately on SO many a time the second case is seen. so the guidelines are clear: always include the relevant code, :) so we [can help better](https://stackoverflow.com/questions/62764261/functional-tail-recursive-way-to-generate-all-possible-combinations-from-a-dict/62765370?noredirect=1#comment110992466_62764261). – Will Ness Jul 08 '20 at 15:02
  • 1
    cf. [this](https://stackoverflow.com/search?q=user%3A849891+callback), [this](https://codereview.stackexchange.com/a/224996/9064), [this](https://stackoverflow.com/a/61216006/849891), and [this](https://stackoverflow.com/questions/tagged/recursive-backtracking). the "shrinking domain" aspect of it doesn't apply but the "*n* nested loops" does. (also, more directly related, [this](https://stackoverflow.com/a/34562122/849891)). that's what Haskell's `replicateM n digits` is doing: it creates *n* nested loops `for d1 in digits: for d2 in digits: ... for dn in digits: yield [d1,d2,...,dn]`. – Will Ness Jul 08 '20 at 15:09
  • 1
    could you please also test whether `makeDomain2 xs n = map reverse . foldl (\xs x -> liftA2 (flip (:)) xs x) (pure []) . replicate n $ xs` runs in constant space?.. (or, for that matter, `makeDomain1b xs n = map reverse . foldr (\x xs -> liftA2 (flip (:)) xs x) (pure []) . replicate n $ xs` which I suspect to be equivalent to the list comprehension...). trying them in GHCi, both seem to be running in constant space, with _2 being x2 faster than the _1b. – Will Ness Jul 13 '20 at 11:27
  • 1
    if this is confirmed, it'd mean that *`foldl`* is the way to build those nested loops, in Haskell. (`sequence`, of course, uses (the equivalent of) `foldr`). ---- incidentally, `foldl` *is* tail-recursive. – Will Ness Jul 13 '20 at 11:44
  • @WillNess - yes, I find that on my machine both of your `makeDomain2` and `makeDomain1b` functions run in constant space, with execution time similar to the list comprehension based `makeDomain1` function. Concise if not tail recursive. Great ! – jpmarinier Jul 13 '20 at 19:15
  • thanks for checking this. it *is* tail-rec though, [in some way](https://stackoverflow.com/questions/62764261/functional-tail-recursive-way-to-generate-all-possible-combinations-from-a-dict/62765370?noredirect=1#comment111188726_62764261) at least. – Will Ness Jul 14 '20 at 02:24
  • @WillNess - So, any plans to put `makeDomain1b` etc... into some formal answer of your own ? Some users might land right here while searching for a fast and Haskell-idiomatic way of computing the nth cartesian power of a list. This is something that `sequence` and `replicateM` unfortunately fail to provide. Thanks in advance. – jpmarinier Jul 14 '20 at 10:46
  • in the end, it [didn't pan out](https://stackoverflow.com/questions/61875763/haskell-running-out-of-memory-with-finite-lists/61896940?noredirect=1#comment111300674_61896940). – Will Ness Jul 16 '20 at 19:28
1

Here is my constructive take on solving the above described problem.

The solution is functional, concise, recursive (but not tail-recursive) implementation in Scheme.

The idea is that the domain has an inductive (recursive) definition: each combination in the domain (first map) is a pair of a digit that is taken one in one from the initial digits dictionary and all combination for a smaller by one dimension (second map)

(define (make-domain digits dimension)
  "Builds all combinations of digits for a dimension"
  ;; There is an empty combination for a dimension 0
  (if [zero? dimension] '(())
      ;; Combine all combinations
      (apply append
             ;; For each digit from digits
             (map (lambda (d)
                    ;; Prepend the digit to each combination
                    ;; for a smaller by one dimension
                    (map (lambda (sd) (cons d sd))
                         (make-domain digits (1- dimension))))
                  digits))))
1

Your answer can be made tail-recursive by the usual trick of using an accumulator. The following is Racket not Scheme, but perhaps only because it uses append* which can be defined, I think, as

(define (append* . args)
  (apply append args))

Here is a tail-recursive version, therefore:

(define (make-domain digits dimension)
  (let mdl ([d dimension] [r '(())])
    (if (zero? d)
        r
        (mdl (- d 1)
             (append* (map (λ (d)
                             (map (λ (sd)
                                    (cons d sd))
                                  r))
                           digits))))))
1

For completeness, here is the Haskell solution translated to Standard ML:

fun curry f x y = f (x, y)
fun concatMap f xs = List.concat (List.map f xs)

fun makeDomain _ 0 = [[]]
  | makeDomain ys n =
    let val res = makeDomain ys (n-1)
    in concatMap (fn x => map (curry op:: x) res) ys
    end

One could apply the usual trick of an accumulator to avoid the n stack frames that tfb demonstrates. But as amalloy points out, this is hardly the bottleneck of this function with its memory use an exponential factor of n. In the Standard ML variant, the excessive list concatenation will cost more.

So depending on what you intend to do with this list, you may want to consider, in Standard ML, generating its elements and process them one at a time (like lazy streams allow you to); for example, rather than generate a long list and filter it, you could generate the filtered list. Here's an example: Translation of Pythagorean Triplets from Haskell to Standard ML.

sshine
  • 15,635
  • 1
  • 41
  • 66
  • lazy streams, generators, or nested loops with a callback on the inside, like in Norvig's implementation of Prolog in his PAIP. – Will Ness Jul 08 '20 at 15:21