hakunin
hakunin

Reputation: 4231

How to create a map in a loop in Elixir

I am creating a 2d map and want to start by pre-filling it with empty values.

I know the following will not work in Elixir, but this is what I am trying to do.

def empty_map(size_x, size_y) do
  map = %{}

  for x <- 1..size_x do
    for y <- 1..size_y do
      map = Map.put(map, {x, y}, " ")
    end
  end
end

Then I will be drawing shapes onto that map like

def create_room(map, {from_x, from_y}, {width, height}) do
  for x in from_x..(from_x + width) do
    for y in from_x..(from_x + width) do
      if # first line, or last line, or first col, or last col
        map = Map.replace(map, x, y, '#')
      else
        map = Map.replace(map, x, y, '.')
      end
    end
  end
end

I have tried doing it as a 2D array, but I think flat map with coordinate touples as keys will be easier to work with.

I know I am supposed to use recursions, but I don't really have a good idea of how to do it elegantly and this scenario keeps coming up and I haven't seen a simple/universal way to do this.

Upvotes: 1

Views: 2522

Answers (4)

Cuddlecake
Cuddlecake

Reputation: 13

I've tried to create my own solution, that would do the above in a manner that is understandable but still as concise as possible (I'm just a beginner myself).

  def create_room(left_bound, right_bound, lower_bound, upper_bound) do
    for x <- left_bound..right_bound,
        y <- lower_bound..upper_bound,
        into: %{}
    do
      draw_tile(x, y, border?(x, y, left_bound, right_bound, lower_bound, upper_bound));
    end
  end

  def border?(x, y, left_bound, right_bound, lower_bound, upper_bound) do
    x in [left_bound, right_bound] ||
    y in [lower_bound, upper_bound]
  end

  def draw_tile(x, y, _is_border = true) do
    {{x,y}, "#"}
  end

  def draw_tile(x, y, _is_border = false) do
    {{x,y}, "."}
  end

First of all, since a Range is inclusive (i.e. 1..4 is [1,2,3,4] and not [1,2,3]), I use actual bounds instead of width and height parameters. This makes it more clear to me what I have to do with the code.

Next, I make a simple comprehension, as shown in the answer of @AA., basically looping over all possible combinations of x and y.

If you return a two-element tuple within the body of that loop, then the first element of the tuple will be the key and the second will be the value inserted into the map.

Within the body, I use draw_tile, a function which takes coordinates and a boolean indicating whether the tile is a border-tile or not.

Finally, the function border? just checks if either x or y are equal to left_bound or right_bound and lower_bound or upper_bound respectively.

In Elixir, although if statements exist, it is more idiomatic to use pattern matching in a function, as I did with draw_tile.

Also, I think (personal opinion, not sure if this is reflected in the Elixir Community as a whole), nesting should be avoided as much as possible.

Edit: You will also find a lot of information on syntax in the Elixir docs.

For example, see Kernel.SpecialForms.for/1 or Kernel.in/2

Upvotes: 0

AA.
AA.

Reputation: 4606

One line using Comprehensions:

    for x <- 1..10, y <- 1..10, into: %{}, do: {{x, y}, " "}

Upvotes: 5

Steve Pallen
Steve Pallen

Reputation: 4507

Another approach is to create a list tuples with comprehensions and convert it into a map.

iex(18)> defmodule Room do
...(18)>   def empty_map(size_x, size_y) do
...(18)>     for x <- 1..size_x, y <- 1..size_y do
...(18)>       {{x,y}, " "}
...(18)>     end
...(18)>     |> Enum.into(%{})
...(18)>   end
...(18)>
...(18)>   def create_room(map, {from_x, from_y}, {width, height}) do
...(18)>     last_x = from_x + width
...(18)>     last_y = from_y + height
...(18)>     for x <- from_x..last_x, y <- from_y..last_y do
...(18)>       if x == from_x or x == last_x or y == from_y or y == last_y,
...(18)>         do: {{x, y}, "#"}, else: {{x, y}, "."}
...(18)>     end
...(18)>     |> Enum.into(map)
...(18)>   end
...(18)> end
warning: redefining module Room (current version defined in memory)
  iex:18

{:module, Room,
 <<70, 79, 82, 49, 0, 0, 10, 244, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 10,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:create_room, 3}}


iex(19)> Room.empty_map(3,4) |> Room.create_room({1,2}, {3,3})
%{{1, 1} => " ", {1, 2} => "#", {1, 3} => "#", {1, 4} => "#", {1, 5} => "#",
  {2, 1} => " ", {2, 2} => "#", {2, 3} => ".", {2, 4} => ".", {2, 5} => "#",
  {3, 1} => " ", {3, 2} => "#", {3, 3} => ".", {3, 4} => ".", {3, 5} => "#",
  {4, 2} => "#", {4, 3} => "#", {4, 4} => "#", {4, 5} => "#"}
iex(20)>

Upvotes: 1

Dogbert
Dogbert

Reputation: 222118

You can use two nested Enum.reduce/3 here, passing the map as the accumulator, instead of writing recursive functions yourself:

defmodule A do
  def empty_map(size_x, size_y) do
    Enum.reduce(1..size_x, %{}, fn x, acc ->
      Enum.reduce(1..size_y, acc, fn y, acc ->
        Map.put(acc, {x, y}, " ")
      end)
    end)
  end
end

IO.inspect A.empty_map(3, 4)

Output:

%{{1, 1} => " ", {1, 2} => " ", {1, 3} => " ", {1, 4} => " ", {2, 1} => " ",
  {2, 2} => " ", {2, 3} => " ", {2, 4} => " ", {3, 1} => " ", {3, 2} => " ",
  {3, 3} => " ", {3, 4} => " "}

Upvotes: 2

Related Questions