Erlang DS - A library working with Erlang Key-Value data structures agnostically

Hello fellow Erlangers!

I’m making a quick announcement here of a little library I’ve built (and have been running a version in production for almost a decade), and have recently spent a good bit of time overhauling it for some upcoming refactoring.

It’s called Erlang DS

(The DS stands for “Data Structures”)

Perhaps the name is a little on-the-nose, perhaps it’s a little bold to make a library with a core module with a 2-character name in an environment with a single shared namespace. But that’s what I did - and it can’t be undid.

Erlang DS is a simple library for agnostically getting, setting, updating, transforming, converting, and overall working with common Erlang data structures (maps, dicts, proplists). Plus, it also provides a few mechanisms for extending its functionality.

The core module is called ds and its functions are all succinct (e.g. ds:get(Obj, key)).

Does this do anything hugely new or novel?

Not really. It’ll save you a few (or more) keystrokes here and there, depending on the kind of data you’re handling and the operations you’re performing on that data.

Then why even spend the time to make it?

Before I embarked on this, I was working with records. But their rigidity when tweaking definitions was off-putting and tedious for my needs. I yearned for something akin to maps (like ROK’s Frames), but maps weren’t part of the language yet. dict lacked the features I was after. Most data I worked with (like database records) were uncomplicated and smallish, making key-value tuples a sensible, accessible, and flexible choice.

However, the lists:key* functions were overkill and more verbose than my preferences.

So I made a simple library for working with proplists that appeals to my tendencies for terseness. Over time, this library evolved into Erlang DS.

For instance, tasks I frequently perform include:

  • Succinctly getting or setting single/multiple values (maps handle this well, but proplists and dicts do not).
  • Applying bulk updates to specific fields (e.g., converting values to atoms, booleans, date strings, unix timestamps).
  • Rekeying structures.

But you just said you wanted maps, and maps exist now. Why even do this?

If you’re already mostly using maps, awesome! This tool might not be for you.

For those not using maps and maybe wanting to make the migration to maps, or for those who actually find value in the some of the little bells and whistles it offers, then this can help you.

However, if you aren’t fully on maps, wish to transition, or find some of its nifty features beneficial, Erlang DS could assist. It offers core features absent in maps, like mass-updating (ds:update and ds:transform) and rekeying (ds:rekey).

For those who currently lean more on structures other than maps, this tool simplifies the migration to maps. Its agnostic approach allows gradual conversions without hiccups. Say I opt to switch a database function to return a map instead of a proplist; ds remains consistent.

Okay, fine. So, what can this do? Blow my socks off!

Whoa, whoa, settle down! It’s not like I’ve solved P=NP or anything. I’ve built a slightly fancier getter-setter library, so rein in your expectations a tad.

But to give you an idea, the following calls work regardless if Obj is a key-value tuple list, map, or dict. If you develop a handler module, it could cater to other structures too. The library recognizes data types, ensuring modified structures are returned with their original type.

%% basic getter
Name = ds:get(Obj, name).

%% basic setter
Obj2 = ds:set(Obj, name, "Frank").

%% mass getting
[Name, Email, Phone] = ds:get_list(Obj, [name, email, phone]).

%% mass setting
Obj3 = ds:set(Obj, [{name, "Jesse"}, {email, ""}, {phone, "5555555555"}]).

%% rekeying - maybe I want "nice" field names for a CSV export
Obj4 = ds:rekey(Obj, [
	{name, <<"Name">>},
	{email, <<"Email Address">>},
	{phone, <<"Phone Number">>}

%% deleting one key
Obj5 = ds:delete(Obj, name),

%% deleting the name and email keys
Obj6 = ds:delete_list(Obj, [name, email]).

%% deleting all keys *except* the name and email keys
Obj7 = ds:keep(Obj, [name, email]),

%% Converting the 'status', 'class', and 'type' field to atoms
Obj8 = ds:update(Obj, [status, class, type], atomize),

%% Converting the expiration and creation fields from unixtime to an ISO date format:
Obj9 = ds:update(Obj, [expiration, creation], {date, "Y-m-d"}).

%% Applying a function to the values of the `extended_data` and `special_conditions` fields
Obj10 = ds:update(Obj, [extended_data, special_conditions], fun base64:decode/1).

%% Registering `base64:decode/1` as an "updater" called `decode64`:
ds:register_updater(decode64, {base64, decode}).

%% Doing the same ds:update/3 but with the new 'decode64' shortcut:
Obj11 = ds:update(Obj, [extended_data, special_conditions], decode64).

%% Combining the above 3 updaters into a single transform and throwing a `to_bool` in there
Obj11 = ds:transform(Obj, [
	{atomize, [status, class, type]},
	{boolize, [is_active]},
	{{date, "Y-m-d"}, [expiration, creation]},
	{decode64, [extended_data, special_conditions]}


Additionally, you can devise support for your own data structures or even call external adapters or APIs (though I’d advise against frequent use of the latter). It’s just structured as a behavior and registered with ds:register_type_handler(ModuleName). Support for dict is incorporated through a type handler, and you can view its implementation in erlang_ds_dict.erl

Wrapping Up!

Enough about that!

If this seems like something you’d find useful, sweet, incorporate it into your apps. If not, that’s cool, too, and I apologize that you read this whole thing only for it to be something you don’t find useful.

Anyway, here are links:

Github: choptastic/erlang_ds (The README has all the documentation)
Hex: erlang_ds (hex also has the documentation, so you should be good).

Anyway, happy hacking!