Hubble is a prototype library allowing to create, read, and update deep Erlang data structures in a manner that would be somewhat similar to using dot notation in languages with their roots closer to OO.
It's a terrible pun on wanting a library that lets you zoom is in your data structure's deep space.
Hubble
is the name given to whatever the library produces as a data structure.
At this time, it's equivalent to a recursive tuple-list of the form:
[{Key1, [{Key2, [{Key3, Val1}, {Key4, Val2}]},
{KeyA, Val3}]},
{KeyB, Val4}]
Where elements are accessible by specifying how to reach them (see Using the library).
$ erl -make
The library should also be inherently compatible with rebar.
$ erl -make && ct_run -pa ebin -suite test/* -logdir logs/
The suites obviously use Common Test.
Sane mode is a more verbose mode that uses no magic. It is the recommended mode for the library.
The 4 basic operations are:
hubble:new() -> Hubble
: initiates an empty data structure to be filled later. The data structure is called aHubble
.hubble:put(Hubble, [Path,To,Item], Value)
: this uses a givenHubble
data structure, and creates every intermediary path (if they do not exist) up until the last one, whereValue
is attributed to it.hubble:get(Hubble, [Path,To,Item])
": this will fetch a value currently stored within a given path.hubble:up(Hubble, [Path,To,Item], NewVal)
: Will update the value in a given Path to a new one, if and only if the path exists. Otherwise, the operation errors out.
Two shortcut operations for mass updating are provided. They're not more efficient, just nicer to look at:
hubble:puts(Hubble, [{[path,1], my_value}, {[somewhat, "deeper",{path}], other_val}]).
hubble:ups(Hubble, [{[path,1], my_value}, {[somewhat, "deeper",{path}], other_val}]).
The first one does multiple put
operations, while the latter does multiple
up
operations.
Let's create a fictional RPG character with given statistics:
1> Stats = hubble:puts(hubble:new(),
1> [{[strength], 12},
1> {[wisdom], 9},
1> {[luck], 10},
1> {[dexterity], 5}]).
...
And let's make a baseline character bio that integrates the stats:
2> Char = hubble:puts(hubble:new(),
2> [{[name], <<"karl">>},
2> {[bio, age], 219},
2> {[bio, hometown], <<"The Internet">>},
2> {[bio, parent, father], undefined},
2> {[bio, parent, mother], <<"A Unicorn">>},
2> {[stats], Stats}]).
...
Oh yeah and let's not forget its level!
3> Char2 = hubble:up(Char, [level], 1).
** exception error: badpath
in function hubble:up/3 (src/hubble.erl, line 41)
4> Char2 = hubble:put(Char, [level], 1).
...
We can only update fields that exist! Let's say for instance our friend karl finds out his father was a man named Randalf. Let's update his profile:
5> Char3 = hubble:up(Char2, [bio,parent,father], <<"Randalf">>).
...
That worked. We can fetch back any information by using a path, or partial one too:
6> hubble:get(Char3, [stats,wisdom]).
9
7> hubble:get(Char3, [bio,parent,mother]).
<<"A Unicorn">>
8> hubble:get(hubble:get(Char3, [bio,parent]), [father]).
<<"Randalf">>
And that's about it.
This is a mode that uses a parse transform to introduce syntactic sugar for people who really can't tolerate using all these damn tokens to do their thing.
The insane mode is enabled by using a parse transform:
-compile({parse_transform, hubble_trans}).
This will automatically allow you to use hubble functions locally with the following syntax:
Hubble = new()
put(Hubble, part,of,a,path, Val)
up(Hubble, part,of,a,path, NewVal)
get(Hubble, part, of, a, path)
puts(Hubble, [part,of,a,path, Val],
[another,path, OtherVal]).
ups(Hubble, [part,of,a,path, Val],
[another,path, OtherVal]).
The functions get a variable number of arguments, which gets translated back into the long 'sane' form. This means the example above could now be written as:
demo() ->
Stats = puts(new(),
[strength, 12],
[wisdom, 9],
[luck, 10],
[dexterity, 5]),
Char = puts(new(),
[name, <<"karl">>],
[bio, age, 219],
[bio, hometown, <<"The Internet">>],
[bio, parent, father, undefined],
[bio, parent, mother, <<"A Unicorn">>],
[stats, Stats]),
Char2 = put(Char, level, 1),
Char3 = up(Char2, bio,parent,father, <<"Randalf">>),
9 = get(Char3, stats, wisdom),
<<"A Unicorn">> = get(Char3, bio, parent, mother),
<<"Randalf">> = get(get(Char3, bio, parent), father).
Note that functions prefixed with hubble:
(a fully qualified call) will not be
parse-transformed. This means you could make a mixed usage of both forms of
functions if you felt like it, although I have no idea why you would do that (but
hey, it's the insane mode after all!).
Oh yeah, one more precaution. The get/1
function and put/2
function can never happen naturally with the hubble
app. These are not parse-transformed back to
anything -- they are the auto-imported functions erlang:get/1
and erlang:put/2
,
used to access the process dictionary. To avoid confusion and highlight errors,
consider adding:
-no_auto_import([get/1, put/2]).
To the modules that use the parse transforms, although I firmly believe you should suffer for your bad decisions of using them.
I would like it if it could be possible to support specifying the types of the underlying Hubble data structure for parse transforms and regular functions. Basically, I think it would be neat if the user could specify something like:
-hubble(dict).
In conjunction with the parse transform to allow specifying how data is stored and retrieved (not sure if I'd like it to have impact on the fully qualified calls too).
Alternatively, using this form, it could be interesting to allow custom implementation of functions as a very weird callback module:
-hubble(puts, fun ?MODULE:my_puts/3).
-hubble(ups, fun ?MODULE:my_ups/3).
So someone could overload one or all of the functions to either support custom data structures, or temporarily allow to patch functions the user thinks are broken.
This would need explicit support from the library.
I had a free day while sitting on a bus and on a plane. People were arguing on the Erlang mailing list about a library a bit like that so I decided to write it just to see, and flex my parse transforming muscles.
Well given the underlying representation is a list, and that lists:key*
functions
are used to navigate around a Hubble, you have to decide if an O(n) behaviour is
suitable for you.
Under the current implementation, the first elements added to the hubble are the first ones to be found when looking data up. This means if you have some paths you are likely to take more often than others, it can be possible to set them in stone when first creating the structure to get generally very fast reads and updates.
Otherwise, you may judge that simple navigation is worth the (potential) performance hit, although a benchmark of your own should show how things go.
I would not recommend using the parse transform version of the module in production because I bet people who will look at your code will not like that you're using auto-imported functions with variable arities, something that could easy make you be seen as a heretic. I will personally offer no support to code using that form unless I find the free time to do it. I'll also offer no commitment to backwards compatibility of the parse transformed code on that one. Maybe I'll offer sed command to help port it. Eh.