Accessing dynamic keys from map

I am trying to write a function that acts upon a list of maps, where the keys are selected by the user. A minimal example:

# Data
list = 
  [
    %{lower: "a", upper: "A", acute: "á", macron: "ā", grave: "à"},
    %{lower: "e", upper: "E", acute: "é", macron: "ē", grave: "è"},
    %{lower: "i", upper: "I", acute: "í", macron: "ī", grave: "ì"}
  ]

Accessing with a static key would look like:

defmodule Matching do
  def show_macron(list) do
    for %{lower: lower, macron: macron} <- list do
      "#{lower}: #{macron}"
    end
  end
end

Not wishing to duplicate show_upper show_grave… I thought I could do this:

defmodule Matching do
  def show_general(list, key1 \\ :lower, key2 \\ :macron) do
    for %{unquote(key1) => key1, unquote(key2) => key2} <- list do
      "#{key1}: #{key2}"
    end
  end
end

But it returns a ** (CompileError) undefined function key1/0 (there is no such import). I suspect this is something to do with scoping, but list, key1, key2 is in the same outer scope, so if list could be accessed, surely the others can too?

try this:

defmodule Matching do
  def show_general(list, key1 \\ :lower, key2 \\ :macron) do
    for %{^key1 => key1, ^key2 => key2} <- list do
      "#{key1}: #{key2}"
    end
  end
end

explanation: given the user keys wrapped it value inside a variable, you use pin operator (^) to use it values as the value that will used to access the map

NOTE: you are not using the the parameters, see that in your code is unquote(key) instead unquote(key1)

5 Likes

Hey there! I believe you’ll want the pin operator ^ instead of unquote:

defmodule Matching do
  def show_general(list, key1 \\ :lower, key2 \\ :macron) do
    for %{^key1 => key1, ^key2 => key2} <- list do
      "#{key1}: #{key2}"
    end
  end
end

Matching.show_general(list, :upper, :grave)

In this case, pinning will achieve what you want - substituting the variable for the underlying value as if you had written it yourself.

You probably don’t need to worry about unquote unless you’re diving into macros/metaprogramming. It’s a bit confusing, but unquoting is really only done inside defmacro and quote expressions, so unless you see those keywords, you most likely want to use the pin operator instead.

1 Like

Thank you both. TIL!

1 Like

If you want to use unquote, you could generate all the clauses at compile-time:

defmodule Matching do
  for key <- [:lower, :upper], value <- [:acute, :macron, :grave] do
    def unquote(:"show_#{key}_#{value}")(list) do
      for %{unquote(key) => key1, unquote(value) => value1} <- list do
        "#{key1}: #{value1}"
      end
    end
  end
end

This will give you six functions:

iex(1)> list = ...
[
  %{acute: "á", grave: "à", lower: "a", macron: "ā", upper: "A"},
  %{acute: "é", grave: "è", lower: "e", macron: "ē", upper: "E"},
  %{acute: "í", grave: "ì", lower: "i", macron: "ī", upper: "I"}
]
iex(2)> Matching.show_
show_lower_acute/1     show_lower_grave/1     show_lower_macron/1
show_upper_acute/1     show_upper_grave/1     show_upper_macron/1

iex(2)> Matching.show_lower_acute(list)
["a: á", "e: é", "i: í"]

Of course if you’re going that far, you could also define the list at compile time, removing the need to pass it as an argument:

defmodule Matching do
  list =
    Macro.escape([
      %{lower: "a", upper: "A", acute: "á", macron: "ā", grave: "à"},
      %{lower: "e", upper: "E", acute: "é", macron: "ē", grave: "è"},
      %{lower: "i", upper: "I", acute: "í", macron: "ī", grave: "ì"}
    ])

  for key <- [:lower, :upper], value <- [:acute, :macron, :grave] do
    def unquote(:"show_#{key}_#{value}")() do
      for %{unquote(key) => key1, unquote(value) => value1} <- unquote(list) do
        "#{key1}: #{value1}"
      end
    end
  end
end

Produces:

iex(1)> Matching.show_
show_lower_acute/0     show_lower_grave/0     show_lower_macron/0
show_upper_acute/0     show_upper_grave/0     show_upper_macron/0

iex(1)> Matching.show_upper_grave()
["A: à", "E: è", "I: ì"]
4 Likes