thorsten-michael

thorsten-michael

Trees of data in Elixir

What is the idomatic way to process tree data structures in Elixir?

I have a database table that holds a ton of rows which define a hierarchy by an attribute called parent_id.

Solution 1: Huge Map
A Tree module that transforms the rows into a map. For each row, it contains the mapping froim the id attribute of each row to something like %{item: row, children: [children_ids...], parent: parent_id}. Also, a list of root_ids is stored, for rows without a parent_id. So, the structure returned looks like:

%{ nodes: %{
     1 => %{item: %{...}, children: [2, 3], parent: nil},
     2 => %{item: %{...}, children: [], parent: 1},  
     3 => %{item: %{...}, children: [4], parent: 1},
     ...,
   roots: [1]}

The module has functions to get the root nodes and to fetch nodes by their id from the map. The same way, it fetches the children for a given node.

Solution 2: Using Agents
Instead of a huge map, I tried to use an Agent for every node in the tree. Its a process holding quite the same data as above, but it is easier to set up the structure from the initial list of rows.

 %{ roots: [#PID<0.122.0>] }

and an Agents with their state like

%{item: %{}, children: [PID#<0.123.0>, PID#<0.124.0>], parent: nil} # PID#<0.122.0>
%{item: %{}, children: [], parent: PID#<0.122.0>} # PID#<0.123.0>

I can even map a function over a tree to transform all the nodes while keeping the structure. But here I can shoot in my foot: If I just map a function f that transforms the agents item state, I get the behaviour of a mutable data structure.
So I did not transform the state of the agent, but created a new one with the transformed item and the parent and children properly set up.

Most Liked

sasajuric

sasajuric

Author of Elixir In Action

To be completely honest I’ve tried this approach myself way back when, in my Erlang days. That’s why I’m confident it’s not a good approach :smiley:

It’s hard to say, because the problem is vaguely described, but I think concurrent processing should be possible with a flat huge map (though can’t say if it’s worth it for your case). However, if you need to process from bottom to top, it might be better to organize the map as child to parent relationships and the list of leaves.

You also suggest that the amount of data is huge. That might lead to a large active heap, and cause some larger GC pauses in the owner process, as I briefly discussed here. If that’s the case, :digraph might be a good fit, because it’s based on ETS. Or otherwise, your hand-rolled ETS table. This might also simplify concurrent processing.

I definitely don’t advise agents (or processes). It’s going to be 2kb overhead per node (and you suggest there are a lot of them). It’s also going to involve a lot of extra message passing and scheduler overhead. With some careful work, you should be able to achieve whatever you want without agents. A large map or a nested data structure would be functional solutions, while ETS is an optimization for the cases when you have a frequently changing large active working set.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Is it a directed acyclic graph or a tree? That is, does any node ever have more than one parent? If it’s a true tree then my recommendation is to just have a big nested structure of maps, it ends up working pretty well.

If it’s actually a DAG then something like :digraph is likely to work best. Going the agent route would be a great example of something that http://theerlangelist.com/article/spawn_or_not argues against.

Qqwy

Qqwy

TypeCheck Core Team

The way your database is set up, there is nothing preventing cycles from happening. Also, when you want to query all nodes back up the tree, or the subtree starting at a certain node, you will require N database queries (I think? Maybe there is some archaic black magic to follow all the parent IDs in SQL directly, but it isn’t going to be pretty).

What I have seen other libraries, most notably the Ruby gem ‘Ancestry’, do, is to store a list of IDs of the path back up the tree. This way, cycles are prevented, and you can also select a subtree at once.

As for how to represent them in memory: Hex.PM contains Arbor, zipper_tree, Garph, and quite possibly some more packages that can help with this (disclaimer: I have not inspected them in detail). The basic idea of a ‘rose tree’ like the one you are attempting to build, is to have a node, which has a list of children (each of these being a node again). In Haskell parlance:

data Tree a = Node a [Tree a]
In Elixir, this would be something like

defmodule Tree do
 defstruct val: nil, children: []
end

Traversing these kind of trees is simple: Given a node, do something with the value, and then call it recursively for all the children (this is pre-order traversal, post-order traversal would be to first go to the children, and then the current node). An implementation of this can be seen in Macro.prewalk and Macro.postwalk: Yes, quoted elixir code is also a tree: an Abstract Syntax Tree (Although it is of course not wrapped in a stuct).

OvermindDL1

OvermindDL1

I’m not sure if everyone else is over-thinking it or if I’m just lazy, but I would in no way try to build up a graph there for a few reasons:

First, no cycles are possible in a baked structure in an immutable language, this is a ‘good’ thing.

Second, you are pulling them from a database and everything has an id key as well as a parent_id key, what happens if you have a cycle (database will not catch that)?

I’d just grab the things, stuff them into a map with the key being the id then anytime I need the parent I just do all_of_them[this_thing.id] as I need. It is fast, no duplicating of memory, no giant trees in memory, cycles are represented (your job to break out of cycles if so), etc… etc…

Where Next?

Popular in Questions Top

joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New

Other popular topics Top

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New

We're in Beta

About us Mission Statement