Forming a 3D cube from elements in a list

I have a list say [A, B, C,D,E,F,G,H]

I wish to form a 3D cube-like structure using these elements where neighbors of each element will be stored in a tuple.
I want the approach through coordinates.
Each element should be given x, y and z coordinates and based on x,y,z I will fetch the neighbors.

My problem is if I use a nested for loop then assignment within loop is not happening which is not allowing me to iterate through the list.

index = -1
  
for x <- 1..row_length do
      for y <- 1..row_length do
        for z <- 1..row_length do
              Index = index + 1
              {Enum.at(list,index), x, y, z}
        end
      end
    end

I tried using

g = Enum.map(a, fn node ->
for x <- 1..2 do
   for y <- 1..2 do
      for z <- 1..2 do
         index = index + 1
         {node, index, x, y, z}
      end
    end
end
end)

But the above code two code snippets give me
[
[
[[{A, 1, 1, 1}, {A, 1, 1, 2}], [{A, 1, 2, 1}, {A, 1, 2, 2}]],
[[{A, 2, 1, 1}, {A, 2, 1, 2}], [{A, 2, 2, 1}, {A, 2, 2, 2}]]
],
[
[[{B, 1, 1, 1}, {B, 1, 1, 2}], [{B, 1, 2, 1}, {B, 1, 2, 2}]],
[[{B, 2, 1, 1}, {B, 2, 1, 2}], [{B, 2, 2, 1}, {B, 2, 2, 2}]]
],
[
[[{C, 1, 1, 1}, {C, 1, 1, 2}], [{C, 1, 2, 1}, {C, 1, 2, 2}]],
[[{C, 2, 1, 1}, {C, 2, 1, 2}], [{C, 2, 2, 1}, {C, 2, 2, 2}]]
],
[
[[{D, 1, 1, 1}, {D, 1, 1, 2}], [{D, 1, 2, 1}, {D, 1, 2, 2}]],
[[{D, 2, 1, 1}, {D, 2, 1, 2}], [{D, 2, 2, 1}, {D, 2, 2, 2}]]
],
[
[[{E, 1, 1, 1}, {E, 1, 1, 2}], [{E, 1, 2, 1}, {E, 1, 2, 2}]],
[[{E, 2, 1, 1}, {E, 2, 1, 2}], [{E, 2, 2, 1}, {E, 2, 2, 2}]]
],
[
[[{F, 1, 1, 1}, {F, 1, 1, 2}], [{F, 1, 2, 1}, {F, 1, 2, 2}]],
[[{F, 2, 1, 1}, {F, 2, 1, 2}], [{F, 2, 2, 1}, {F, 2, 2, 2}]]
],
[
[[{G, 1, 1, 1}, {G, 1, 1, 2}], [{G, 1, 2, 1}, {G, 1, 2, 2}]],
[[{G, 2, 1, 1}, {G, 2, 1, 2}], [{G, 2, 2, 1}, {G, 2, 2, 2}]]
],
[
[[{H, 1, 1, 1}, {H, 1, 1, 2}], [{H, 1, 2, 1}, {H, 1, 2, 2}]],
[[{H, 2, 1, 1}, {H, 2, 1, 2}], [{H, 2, 2, 1}, {H, 2, 2, 2}]]
]
]

Where as desired results would be
{A, 1,1,1}
{B,1,1,2}
{C,1,2,1}
{D,1,2,2}
{E,2,1,1}
{F,2,1,2}
{G,2,2,1}
{H,2,2,2}

Hi @rishabhrrk it looks like you’re new here, so first of all let me say “welcome!”

I see at least 3 misunderstandings in your question, so I’m guessing you are new to Elixir, and possibly functional programming too. They are not for loops, but list comprehensions. They are much more powerful. You can get a cartesian product of values with a single use of for by using multiple generators. You cannot mutate variables, especially ones outside of the scope. Your approach to updating index is trying to do that. Also, upper case names in Elixir are intended to be used for module names, not for variable names.

Here’s how I’d approach the situation. First you want the cartesian product of a range of 1…row_index in 3 variables, so:

iex> row_length = 2
2
iex> coords = for x <- 1..row_length, y <- 1..row_length, z <- 1..row_length, do: {x,y,z}
[
  {1, 1, 1},
  {1, 1, 2},
  {1, 2, 1},
  {1, 2, 2},
  {2, 1, 1},
  {2, 1, 2},
  {2, 2, 1},
  {2, 2, 2}
]

But you want to prefix each tuple with an element from your list of node names. I’ll name the nodes with lower case atoms as is more idiomatic Elixir. And I’ll use Enum.zip to get a pairwise tuple from both the nodes and coords

iex> nodes = [:a, :b, :c, :d, :e, :f, :g, :h]
[:a, :b, :c, :d, :e, :f, :g, :h]
iex> zipped = Enum.zip(nodes, coords)
[
  a: {1, 1, 1},
  b: {1, 1, 2},
  c: {1, 2, 1},
  d: {1, 2, 2},
  e: {2, 1, 1},
  f: {2, 1, 2},
  g: {2, 2, 1},
  h: {2, 2, 2}
]

There’s a little bit of a display artifact there because Elixir thinks you have a keyword list. Each row is really a tuple within a tuple, for example {:a, {1, 1, 1}}. We can then use Enum.map to get you a flat tuple like you desire:

iex> Enum.map(zipped, fn {i, {j, k, l}} -> {i, j, k, l} end)
[
  {:a, 1, 1, 1},
  {:b, 1, 1, 2},
  {:c, 1, 2, 1},
  {:d, 1, 2, 2},
  {:e, 2, 1, 1},
  {:f, 2, 1, 2},
  {:g, 2, 2, 1},
  {:h, 2, 2, 2}
]

This could also be done with a bit of syntactic sugar and a pipeline all at once without intermediate variables like this:

 ~w(a b c d e f g h)a
|> Enum.zip(for x <- 1..row_length, y <- 1..row_length, z <- 1..row_length, do: {x, y, z})
|> Enum.map(fn {i, {j, k, l}} -> {i, j, k, l} end)
7 Likes

Thanks for the warm welcome. All your correct.
Thanks, the solution helped.

I couldn’t help but tweak this a bit more. It’s a fun little problem. I’m sure for an 8 element list the performance implications probably fall within the range of noise, but I wanted to see if I could do this with only one iteration of the values, plus I haven’t really spent time playing with the new reduce option of list comprehensions yet. Here’s basically the one-liner I came up with (if you’ll forgive the assignment to row_length).

iex> row_length = 2
2
iex> (for x <- row_length..1, y <- row_length..1, z <- row_length..1, reduce: {~w(h g f e d c b a)a, []}, do: ({[n | names], coords} -> {names, [{n, x, y, z} | coords]})) |> elem(1)
[
  {:a, 1, 1, 1},
  {:b, 1, 1, 2},
  {:c, 1, 2, 1},
  {:d, 1, 2, 2},
  {:e, 2, 1, 1},
  {:f, 2, 1, 2},
  {:g, 2, 2, 1},
  {:h, 2, 2, 2}
]

In this, I create a 2-tuple initial accumulator containing the node names I have not used yet and the coordinates to output (empty list to begin with). In each iteration of the comprehension I destructure the accumulator, popping off the next unused node name, and I set the new accumulator as the remaining node names and the new coordinate prepended to the prior list. Finally, after the comprehension finishes, I pull the desired results from tuple element 1. And I do it all starting in reverse order so after all the prepending I end up with the desired order and avoid a final reverse.

I’m not sure if I should be proud of ashamed of coming up with this :grin: It does demonstrate how versatile list comprehensions are though.

2 Likes