lasp-lang / types

Prototype implementation of Conflict-free Replicated Data Types (CRDTs) in Erlang.
https://lasp-lang.org
Apache License 2.0
139 stars 13 forks source link

Make CRDTs as simple to use as possible #85

Closed goncalotomas closed 1 year ago

goncalotomas commented 7 years ago

I spoke to @vitorenesduarte about what CRDTs I should use for the FMKe driver, because looking at this repository there is simply too much choice for someone without more knowledge in CRDT internals.

I think there is a way to use CRDTs in a transparent way while maintaining all the different types of configurations for those users that actually care/know the difference. I feel that this is more aligned with Lasp's goals than a big list of CRDTs where (in my case at least) people get lost in a sea of choices. This is my proposal for an alternative Lasp CRDT design, and I'm going to use a basic example using the Counter CRDT since it is the one I know best.

Since all CRDT Counters should, in principle, have identical APIs, one can look at the differences between Counter implementations as different behaviours.

Imagine that the Counter API is as follows:

For simplicity, also assume that there are only two implementations of Counter: gcounter and pncounter.

My idea is to give the Counter CRDT a default behaviour, and since by default users will expect to be able to call dec/1, it makes sense to have pncounter as the default Counter behaviour.

This makes a slight change in the state of Counter. If a Counter has behaviour x and state representation Y, its representation would now become {x, Y}.

An example with two implementations of Counter:

-module(counter).
-export([
    new/0,
    new/1,
    inc/2,
    dec/2,
    query/1
]).

new() ->
    new(pncounter).

%% @pre: Behaviour == pncounter || Behaviour == gcounter 
new(Behaviour) ->
    {Behaviour, Behaviour:new()}.

inc(Amount, _Counter = {Behaviour, State}) when Amount > 0 ->
    erlang:apply(Behaviour,mutate,[increment,Amount,State]).

dec(Amount, _Counter = {Behaviour, State}) when Amount > 0 ->
    %% should throw {error, op_not_supported} or something similar for gcounter
    erlang:apply(Behaviour,mutate,[decrement,Amount,State]).

query(Counter = {Behaviour, _State}) ->
    erlang:apply(Behaviour,query,[Counter]).

An alternative would be to bundle up all the logic from all counters in a single module, but there are so many choices that it would probably get out of hand.

Applied to every type of CRDT, this would make it dead simple to use them - just pick the one that can store the type of data you want to put inside it, and read the docs for details about all different behaviours if performance is not good enough.

Comments?