Converting lists of tuples to maps / HashDict / Structs

2,427 views
Skip to first unread message

Mattias Gyllsdorff

unread,
May 20, 2015, 2:13:49 PM5/20/15
to elixir-l...@googlegroups.com
I have been using Elixir for a few days now and I frequently have tuples that I want to convert to maps or structs. Is it possible to use the pattern match on a tuple to create a map without having to create any temporary variables?

Right now I usually end up doing something like this;
# query(Repo, @sql, []).rows
rows
= [{1, 1, 500, {{2015, 1, 1}, {14, 13, 20}}}, {2, 2, 300, {{2015, 1, 1}, {14, 13, 10}}}]

# Example 1
rows
|> Enum.map(fn {sequence, id, value, date} ->
 
%{sequence: sequence, id: id, value: value, date: Date.from(date)} end)
|> do_stuff

# Example 2
rows
|> Enum.map(fn row ->
 
%{sequence: elem(row, 0), id: elem(row, 1) , value: elem(row, 2) , date: Date.from(elem(row, 3)))}
end)

# Example 3
columns
= [:sequence, :id, :value, :date]
rows
|> Enum.map(fn row ->
 
Enum.zip(columns, Tuple.to_list(row))
 
|> Enum.into(HashDict.new, fn
     
{key, value} when key == :date -> {key, Date.from(value)}
     
{key, value} -> {key, value}
 
end)
end)

Is there any other or more idiomatic way to do this? 

I don't really like having to repeat the field name three times like I do in example 1. I have the same "repeat the field name three times problem" when I parse files using the binary pattern match feature

def match(<<"01", layout :: 20 - binary, version::2 - binary, date::20-binary, _ :: binary>>) do
 
%{code: 01, layout: String.rstrip(layout), version: version, date: date}
end
def match(<<"05", date :: 20 - binary, sender::2 - binary, sequence::20-binary, _ :: binary>>) do
 
%{code: 05, date: date, sender: sender, sequence: sequence}
end
def match(<<code :: 2 - binary, rest :: binary>>) do
 
%{code: String.to_integer(code), error: :unhandled}
end



Peter Hamilton

unread,
May 20, 2015, 2:22:16 PM5/20/15
to elixir-l...@googlegroups.com
I've been contemplating building Macro.prewalk/3 for all native elixir types. I find doing deep traversal of arrays/maps/etc to be fairly common (especially when glueing two libraries together). Each case is usually handled in a one-off manner and I haven't yet seen a generic path.

I'd be curious to see what your "ideal idiom" would be.

--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/4a35f4b1-2c57-49e5-9a64-d72d6ca7c6a4%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Mattias Gyllsdorff

unread,
May 20, 2015, 2:44:59 PM5/20/15
to elixir-l...@googlegroups.com
Keep in mind that I have never used Elixir or Erlang before so I am still discovering various tricks.

I saw the map pattern match in the book "Programming Elixir", page 77.
iex> %{ name: a_name } = person
%{height: 1.88, name: "Dave"}
iex
> a_name
"Dave"
So I figured perhaps tuple parts can be matched into the value part of a map like that. 
[{1, 1, 500, {{2015, 1, 1}, {14, 13, 20}}}, {2, 2, 300, {{2015, 1, 1}, {14, 13, 10}}}]
|> Enum.map(fn row ->
 
# Works
 
# {first_part_of_tuple, second_part_of_tuple, value, date} = row

 
# Does not work
 
%{sequence: first_part_of_tuple, id: second_part_of_tuple, value: value, date: date} = row
end)

Something like this would reduce the repetition. 

I am actually asking because I am interested in how a experienced Elixir / Erlang coder would do this.

Robert Virding

unread,
May 20, 2015, 3:01:53 PM5/20/15
to elixir-l...@googlegroups.com
Just to be precise you are actually no repeating the field name 3 times in example 1. You are giving the field name once and the other 2 uses are as a variable which can be called anything. So

    rows
    |> Enum.map(fn {s, i, v, d} ->
            %{sequence: s, id: i, value: v, date: Date.from(d)} end)
    |>
do_stuff

is a perfectly valid equivalent form.

Robert

Robert Virding

unread,
May 20, 2015, 3:14:10 PM5/20/15
to elixir-l...@googlegroups.com
The problem here is understanding what a pattern match does. So when you write

    pattern = value

you are testing the structure of the value and maybe extracting values from it. So the first match:

    {first_part_of_tuple, second_part_of_tuple, value, date} = row

first tests if row is a tuple of 4 elements, which it is, and then extracts the 4 elements of row into the variables first/second_part_of_tuple, value and data. The second match:

   
%{sequence: first_part_of_tuple, id: second_part_of_tuple, value: value, date: date} = row

does *not* build a map but first tests if row is a map, which it isn't as it is a tuple, and if it was a map then tests if it has the fields first/second_part_of_tuple, id, value and date and finally extracts the values of those files and binds the variables
first/second_part_of_tuple, id, value and date to those values.

Basically a pattern tests the datatype, structure and extracts values. To build a structure you need to do it on the RHS. You could have written the Enum as

Enum.map(fn row ->
 
{first_part_of_tuple, second_part_of_tuple, value, date} = row

 
%{sequence: first_part_of_tuple, id: second_part_of_tuple, value: value, date: date}
end)

which matches row and then builds and returns a map. This is basically exactly the as your previous example 1 except you have moved the output of the function head into the body.

Robert

Peter Hamilton

unread,
May 20, 2015, 4:39:06 PM5/20/15
to elixir-l...@googlegroups.com
Completely in right field, Macros!

let's say we had:

map_from_tuple(row, {:sequence, :id, :value, :date})

which did exactly what you wanted.

here's how we might implement it:

  defmacro map_from_tuple(obj, keys_tuple) do
    {_, _, keys_list} = keys_tuple
    vars_list = keys_list |> Enum.map(&(Macro.var(&1, __MODULE__)))
    vars_tuple = {:{}, [], vars_list}
    map = {:%{}, [], Enum.zip(keys_list, vars_list)}
    quote do
      unquote(vars_tuple) = unquote(obj)
      unquote(map)
    end
  end

Remember the first rule of macros: "Don't use macros"

Also, you could probably do all of this at runtime. It might even be a better idea to do it then. I'm posting this largely for fun.

Alexei Sholik

unread,
May 20, 2015, 5:18:22 PM5/20/15
to elixir-l...@googlegroups.com
With Ecto, the idiomatic way to get data from a DB would be to use Ecto.Query.

If that's not possible, you could still automate the conversion from tuples to maps if you define a schema for your table. Say you call it MyTable, then you'll be able to do this:

rows = ...
fields = MyTable.__schema__(:fields)
Enum.map(rows, fn row ->
  Enum.zip(fields, Tuple.to_list(row))
  |> Enum.into(%{})
end)


Mattias Gyllsdorff

unread,
May 21, 2015, 2:16:04 AM5/21/15
to elixir-l...@googlegroups.com
I generally use the Ecto DSL when I write new queries but we have several older, very complex, raw sql queries that we need to work with. 

Since Ecto does not officially(?) support mapping rows to Ecto models I currently do this;
Repo.transaction(fn ->
 
Ecto.Adapters.SQL.query(repo, @sql_query, [])
 
|> Util.DB.result_to_map(fn
     
{key, value} when key == "date" -> {key, Date.from(value)}

     
{key, value} -> {key, value}
 
end)

 
|> do_stuff
end)

Thank you for all your help. 

I think I will post a question in the Ecto group about transforming rows to models. I could probably use __schema__(:load, source, idx, values) but that looks like a internal API so I am a bit worried it might change.
Reply all
Reply to author
Forward
0 new messages