thorsten-michael

thorsten-michael

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.

First Post!

href

href

You could use the erlang’s digraph module, which uses an ETS table as the underlying storage. An acyclic digraph is the same as a “tree”.

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 The Erlangelist - To spawn, or not to spawn? 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).

Last Post!

user20230119

user20230119

Is there anything better than digraph or libgraph to traverse tree structs?

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews