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.