Data Structure Agnostic Key-Value Library for Erlang

Introducing a simple library tailored for Erlang's common key-value data structures. Currently it ships supporting proplists, maps, and dicts (with a built-in handler). It can be further expanded to support new types with the handler system. With a universal interface, seamlessly convert between structures and harness additional functionalities not natively found in some, or even any, of these data types. Just "do the damn thing" without sweating about the underlying structure.

Contemplating an upgrade for a legacy app from proplists to maps? This tool's got your back. With ds, maintain consistent calls for a smooth, hassle-free transition.

Important Links

Key Points to Remember

Modified Syntax Plugin (experimental/optional)

Erlang DS comes with an optional modification to the Erlang syntax to satisfy those who thrive on brevity, while also making the get syntax more familiar to non-Erlang developers.

With this syntax plugin enabled, the following calls are equivalent:

X = Obj->key.
X = ds:get(Obj, key).

And these two calls are equivalent:

[A,B,C] = Obj->[key1, key2, key3].
[A,B,C] = ds:get_list(Obj, [key1, key2, key3]).

For more information, see the Adding the Syntax Plugin section at the bottom of this page. Please note this functionality is experimental.

Function Reference

Reference Preface 1: When we mention "object" or Obj, we're referring to the provided data structure, even if it doesn't align with the traditional OO paradigm.

Reference Preface 2: Optional Arguments are denoted by [Brackets]

Getting, Setting, and Deleting (one value)

Getting and Setting (multiple values)

Deleting and Filtering Values

Working with Keys

Mass Updating

Built-In Special Updaters

There are a few built-in custom updaters that can be enabled or disabled (see "Expanding erlang_ds with custom updaters for how to make your own custom updaters).

These updaters are enabled by default:

These qdate-based updaters are NOT enabled by default

Transforming: Updating on Steroids

Conversion and Type-Checking

Comparison Helpers for Sorting lists of Objects

Merging Objects

Expanding erlang_ds with Custom Updaters

You can create your own custom updaters to be used with ds:update/3 and ds:transform/2.

To register a custom updater, you call can take 2 possible forms.

The Full Custom Updater

The more powerful updater format is this:

ds:register({UpdaterName, NumUpdaterArgs}, {Module, Function, NumArgs})

Example

If you regularly update values to be formatted as strings with something like this:

FormatFun = fun(D) -> qdate:to_string("Y-m-d", D) end,
ds:update(Obj, [signup_date, valid_date], FormatFun).

You could register a default formatter like this:

ds:register_updater({format_date, 1}, {qdate, to_string, 2}).

Once that's done, you can do this call anywhere in your code:

ds:update(Obj, [signup_date, valid_date], {format_date, "Y-m-d"}).

This is the same as calling qdate:to_string("Y-m-d", X) on the provided fields.

The Simple Custom Updater

If you have a basic updater you might use regularly (one that doesn't take additional arguments), you can use the short syntax for this.

In the above case, let's say you also very regularly convert your values specifically to ISO-formatted strings, you could make a short version. Let's start by defining a basic module here:

-module(my_formatter).
-export([format_iso/1]).

format_iso(D) ->
    qdate:to_string("Y-m-d", D).

Now you can register this function with the shorter ds:register_updater(UpdaterName, {Module, Function}):

ds:register_updater(iso_date, {my_formatter, format_iso}).

And you can then simply call this those fields to ISO dates.

ds:update(Obj, [signup_date, valid_date], iso_date).

The observant reader may have noticed that the following are identical:

ds:register_updater(UpdaterName, {Module, Function}).

ds:register_updater({UpdaterName, 0}, {Module, Function, 1}).`

Worth noting is that UpdaterArgs must always be one less than the FunctionArgs.

Expanding erlang_ds with new types

Extending erlang_ds to support new data types is quite easy.

Create a new module, add the attribute -behavior(erlang_ds_type_handler). (see erlang_ds_type_handler.erl for behavior callback details).

And define the following functions:

(see erlang_ds_dict.erl for an example).

Add to your rebar.config's deps section

{deps, [
    erlang_ds
]}.

Adding the Syntax Plugin

If you want to add Erlang DS's syntax customizations to your app, doing so is very easy:

  1. Add the following line to your code somewhere above your function definitions:

    -compile({parse_transform, ds_syntax}).
  2. Add erlang_ds to the plugins section in to your rebar.config:

    {plugins, [
       erlang_ds
    ]}.
  3. Add the provider hook to your rebar.config:

    {provider_hooks, [
       {pre, [
          {compile, {ds_syntax, arrow}}
       ]}
    ]}.

    Please note that the arrow mentioned above is in reference to the use of ->. Some other variants may be added in the future, depending on user interest.

  4. Recompile your code:

    rebar3 compile
  5. Profit?

Experimental

This syntax plugin is still experimental, and I probably don't need to say it, but language purists will not like this.

But if you do decide to use it, here are some important points to note:

Examples

Here are a handful of examples (with the equivalent ds calls)

Obj->a,         % ds:get(Obj, a)
Obj->A,         % ds:get(Obj, A)
Obj->{a,b},     % ds:get(Obj, {a,b}),
Obj->"key",     % ds:get(Obj, "key"),
(Obj)->a,       % ERROR: left-hand-side (LHS) of -> MUST be a variable. Even a
                %   parenthetical expression on the LHS will break it.
#{}->a,         % ERROR: left-hand-side of -> MUST be a variable
(#{})->a,       % ERROR: left-hand-side of -> MUST be a variable
Obj->f(),       % ds:get(Obj, f)()
                %   more readable version: Fun = ds:get(Obj, f),
                                           Fun().
Obj->(f()),     % ds:get(Obj, f())
Obj->(m:f()),   % ds:get(Obj, m:f())
Obj->m:f(),     % ds:get(Obj, m):f()  probably not what you intend.
Obj->a->b,      % ERROR: Will translate to ds:get(Obj, a)->b, then will
                %   throw an error because the left-hand-side of -> is
                %   not a variable.

Obj->[a],       % ds:get_list(Obj, [a]),
Obj->[a,b],     % ds:get_list(Obj, [a,b]),
Obj->[m:f()],   % ds:get_list(Obj, [m:f()]),
Obj->["key"],   % ds:get_list(Obj, ["key"]),
Obj->[[a,b]],   % ds:get_list(Obj, [[a,b]]).
Obj->([a,b]).   % ds:get(Obj, [a,b]), %% notice the () around the list tells
                %   the parser that you're getting a single value

How does the syntax plugin work?

The parse transform powering it isn't actually a parse transform. Instead, the plugin hijacks the parser and does an initial pass over the tokens looking for the relevant expressions involving the tokens above (currently ->). The presence of -compile({parse_transform, ds_syntax}) in the module, simply indicates to the (now-hijacked) parser that it needs to be preparsed with the ds_syntax plugin and then actually perform the proper parsing.

Known Limitations of the syntax plugin

Aside from the restrictions above, a current known limitation is that there is not currently any support for setting values with the syntax plugin. This will likely be changed in the near future, but currently, there is no setting mechanism for it.

Origins & Philosophy

Erlang DS emerged from the need for a unified module with concise function calls tailored for proplists, especially before Erlang's introduction of the map structure. Although maps have become a preferred choice, legacy code still utilizes proplists or dicts, or even a mix of the trio.

For a few examples of the motivation to create this:

None of these comments is to criticize the Erlang team - they are incredible, and what they've built is incredibly powerful. But no one can be everything to everyone, and tailor their development to every one of their users' requirements or idiocyncracies. Hence, this library was born to bridge such gaps and offer utility.

Constructive feedback and pull requests are heartily welcomed.

PULL REQUESTS ARE WELCOMED

Additional Notes

Formerly known as sigma_proplist, Erlang DS originated from Sigma Star Systems. After nearly a decade in production, it was refashioned to support more data structures, resulting in the Erlang DS we have today.

Currently, the syntax-highlighting is dependent on meck 0.9.2. The newest versions crash. This is being looked into.

Versions

See Changelog

About

Author: Jesse Gumm

Copyright 2013-2024, Jesse Gumm

MIT License