heves
How to generate permutations from different sets of elements?
I’d like to generate a list from a variable length list of lists, choosing one element from each one. Like this:
list = [[:a, :b], [1, 2, 3], [{4, 5}, {5, 6}]]
magic_function(list)
Output:
[:a, 1, {4, 5}]
[:a, 1, {5, 6}]
[:a, 2, {4, 5}]
[:a, 2, {5, 6}]
[:a, 3, {4, 5}]
[:a, 3, {5, 6}]
[:b, 1, {4, 5}]
[:b, 1, {5, 6}]
[:b, 2, {4, 5}]
...
Is something like this possible in Elixir? I feel lost because of the variable length of the 2d list
Marked As Solved
Eiji
I believe this code is the best:
defmodule Example do
# a function head declaring defaults
def sample(list, data \\ [])
# in case where empty list is passed as an input
def sample([], []), do: []
# data here is finished element of output list
# due to prepending the data is reversed
# which is fixed by `:lists.reverse/1`
# we need to wrap the output element into one element list
# otherwise our data is improperly concatenated
# i.e. instead of list of lists we have just a list
# for example: [:a, 1, {4, 5}, :a, 1, {5, 4}, :a, …]
def sample([], data), do: [:lists.reverse(data)]
# when heads i.e. elements of first list ends
# all we need to do is to return an empty list
# to make concatenation work
def sample([[] | _tail], _data), do: []
# collecting data and recursion part
def sample([[sub_head | head] | tail], data) do
# using ++ operator we concatenates two sides each returning a list of lists
# left side returns an output result for first element (nested recursion)
# right side returns an output result for rest elements (tail recursion)
# collecting data is really simple
# we are prepending a first element of head into data
# until we reach end of nested levels
sample(tail, [sub_head | data]) ++ sample([head | tail], data)
end
end
Example.sample([[:a, :b], [1, 2, 3], [{4, 5}, {5, 6}]])
This code should be fastest and work as long as the input is list contains 1 or more lists.
The other solutions are limited to 3-element list and one of them have also other problem which was already mentioned by its author:
In my case all of below calls work:
# empty list
Example.sample([])
# one element list with no sub elements
Example.sample([[]])
# one element list
Example.sample([[:a, :b]])
# two element list
Example.sample([[:a, :b], [1, 2, 3]])
# three element list (author's example input)
Example.sample([[:a, :b], [1, 2, 3], [{4, 5}, {5, 6}]])
# three element list with lists as sub elements instead tuple
Example.sample([[:a, :b], [1, 2, 3], [[4, 5], [5, 6]]])
# four element list
Example.sample([[:a, :b], [1, 2, 3], [{4, 5}, {5, 6}], ["abc", "def"]])
Note: If one of root list is empty, for example:
[[:a, :b], [1, 2, 3], [], [{4, 5}, {5, 6}]]
then then my example would properly return empty list. If you want to simply skit empty elements use this code:
def sample([head | [[] | tail]], data), do: sample([head | tail], data)
right before “collecting data and recursion part” comment.
However the above does not work if empty list is a first element. To fix that you need an extra helper function to avoid conflicts in pattern-matching, for example:
defmodule Example do
# if empty list is a first element
def before_sample([[] | tail]), do: before_sample(tail)
# in any other case
def before_sample(list), do: list
# definition of sample function goes here…
end
input = [[], [:a, :b], [1, 2, 3], [{4, 5}, {5, 6}]]
input
|> Example.before_sample()
|> Example.sample()
Also Liked
trisolaran
This seems to be doing the trick:
Enum.reduce(list, fn acc, sublist ->
for a <- sublist, b <- acc do
List.flatten([a, b])
end
end)
[
[:a, 1, {4, 5}],
[:a, 1, {5, 6}],
[:a, 2, {4, 5}],
[:a, 2, {5, 6}],
[:a, 3, {4, 5}],
[:a, 3, {5, 6}],
[:b, 1, {4, 5}],
[:b, 1, {5, 6}],
[:b, 2, {4, 5}],
[:b, 2, {5, 6}],
[:b, 3, {4, 5}],
[:b, 3, {5, 6}]
]
Unless the elements of some of the sublists are lists themselves, cause in that case they would be flattened.
UPDATE: This one below should cover all cases, just like @Eiji’s solution
def magic_function([]), do: []
def magic_function(list) do
list
|> Enum.reduce(fn sublist, acc ->
for a <- acc, b <- sublist do
[b | List.wrap(a)]
end
end)
|> Enum.map(&Enum.reverse/1)
end
trisolaran
Ah, good catch! Thanks.
I beg to differ. What about this one:
def magic_function([]), do: []
def magic_function(list) do
list
|> Enum.reduce([nil], fn sublist, acc ->
for a <- acc, b <- sublist do
[b | List.wrap(a)]
end
end)
|> Enum.map(&Enum.reverse/1)
end
I think it covers all cases now. The initial accumulator of [nil] for the reduce handles the case when there’s only one sublist: the sublist’s elements are prepended to List.wrap(nil), which is an empty list.
So it appears recursion is not necessary after all
Eiji
Did you challenged a senior developer? ![]()
defmodule Example do
def sample(list, opts \\ [skip: true])
# uncomment if empty list is could be passed as an input
def sample([], _opts), do: []
# uncomment if empty list could be a first element
def sample([[] | tail], opts), do: sample(tail, opts)
# uncomment if list could contain only one element list
def sample([list], _opts), do: Enum.map(list, &List.wrap/1)
# in any other case
def sample(list, opts) do
if opts[:skip], do: sample_with_skip(list), else: sample_without_skip(list)
end
# skip empty elements in root list
def sample_with_skip(list) do
for sub_list <- list, reduce: [] do
# skip empty elements in root list
data when sub_list == [] ->
data
# passing a first element of root list as data
[] ->
sub_list
data ->
for sub_data <- data, element <- sub_list do
[element | List.wrap(sub_data)]
end
end
|> Enum.map(&Enum.reverse/1)
end
# alternatively do not skip empty elements in root list
def sample_without_skip(list) do
for sub_list <- list, reduce: [] do
# when element is empty list set data to nil
_data when sub_list == [] ->
nil
# no matter what happens later if data is nil keep it as is
nil ->
nil
# passing a first element of root list as data
[] ->
sub_list
data ->
for sub_data <- data, element <- sub_list do
[element | List.wrap(sub_data)]
end
end
|> case do
nil -> []
list -> Enum.map(list, &Enum.reverse/1)
end
end
end
Pretty much the same as Enum.reduce/3 version. Simply pattern-matching here is not only in function clause, but also in for …, reduce: … clauseend notation.
However this looks like an art for art's sake. Just look how simple is raw pattern-matching comparing to 2 other versions of my solution. ![]()
fmn
hello!
iex> [a, b, c] = [[:a, :b], [1, 2, 3], [{4, 5}, {5, 6}]]
iex> List.flatten(for e1 <- a, do: for e2 <- b, do: for e3 <- c, do: {e1, e2, e3})
[
{:a, 1, {4, 5}},
{:a, 1, {5, 6}},
{:a, 2, {4, 5}},
{:a, 2, {5, 6}},
{:a, 3, {4, 5}},
{:a, 3, {5, 6}},
{:b, 1, {4, 5}},
{:b, 1, {5, 6}},
{:b, 2, {4, 5}},
{:b, 2, {5, 6}},
{:b, 3, {4, 5}},
{:b, 3, {5, 6}}
]
is that close enough? ![]()
dtew
Thank you very much for this very educative discourse! I think this is an excellent way (for me at least) to learn. I will surely look through your previous posts for more Elixir fixes.







