From b7b692f4bd03f92de25ca1a2ebdee2b8b7965761 Mon Sep 17 00:00:00 2001 From: Anton Bangratz Date: Mon, 9 Oct 2023 15:39:28 +0200 Subject: [PATCH 1/3] Fix specs in AWORmap (dialyzer) --- lib/crdt.ex | 4 ++++ lib/crdt/awor_map.ex | 18 +++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/crdt.ex b/lib/crdt.ex index 1d9420b..804203f 100644 --- a/lib/crdt.ex +++ b/lib/crdt.ex @@ -3,6 +3,10 @@ defprotocol CRDT do Protocol defining the interface for CRDTs. """ + @type actor :: term + @type key :: term + @type crdt :: term + @doc """ Returns the actual value of the CRDT """ diff --git a/lib/crdt/awor_map.ex b/lib/crdt/awor_map.ex index 6a00d08..97f0411 100644 --- a/lib/crdt/awor_map.ex +++ b/lib/crdt/awor_map.ex @@ -1,13 +1,9 @@ defmodule CRDT.AWORMap do @moduledoc false - @type actor :: term - @type key :: term - @type crdt :: term - @type t :: %__MODULE__{ keys: CRDT.AWORSet.t(), - entries: %{key => crdt} + entries: %{CRDT.key() => CRDT.crdt()} } defstruct keys: CRDT.AWORSet.new(), entries: %{} @@ -47,7 +43,7 @@ defmodule CRDT.AWORMap do entries: %{key: %CRDT.GCounter{}} } """ - @spec put(t, actor, key, crdt) :: t + @spec put(t, CRDT.actor(), CRDT.key(), CRDT.crdt()) :: t def put(%__MODULE__{keys: keys, entries: entries}, actor, key, crdt) do CRDT.impl_for!(crdt) @@ -67,7 +63,7 @@ defmodule CRDT.AWORMap do ...> |> CRDT.AWORMap.fetch(:key) {:ok, %CRDT.GCounter{}} """ - @spec fetch(t, key) :: {:ok, crdt} | :error + @spec fetch(t, CRDT.key()) :: {:ok, CRDT.crdt()} | :error def fetch(%__MODULE__{entries: entries}, key), do: Map.fetch(entries, key) @doc """ @@ -86,7 +82,7 @@ defmodule CRDT.AWORMap do iex> CRDT.AWORMap.new() |> CRDT.AWORMap.fetch!(:key) ** (KeyError) key :key not found in: %{} """ - @spec fetch!(t, key) :: crdt + @spec fetch!(t, CRDT.key()) :: CRDT.crdt() def fetch!(%__MODULE__{entries: entries}, key), do: Map.fetch!(entries, key) @doc """ @@ -102,7 +98,7 @@ defmodule CRDT.AWORMap do ...> |> CRDT.AWORMap.get(:key, CRDT.GCounter.new()) %CRDT.GCounter{} """ - @spec get(t, key, crdt) :: crdt + @spec get(t, CRDT.key(), CRDT.crdt()) :: CRDT.crdt() def get(%__MODULE__{entries: entries}, key, default \\ nil), do: Map.get(entries, key, default) @doc """ @@ -125,7 +121,7 @@ defmodule CRDT.AWORMap do ...> |> CRDT.value() %{key: 5} """ - @spec update(t, actor, key, crdt, (crdt -> crdt)) :: t + @spec update(t, CRDT.actor(), CRDT.key(), CRDT.crdt(), (CRDT.crdt() -> CRDT.crdt())) :: t def update(%__MODULE__{entries: entries} = awor_map, actor, key, default, fun) when is_function(fun) do CRDT.impl_for!(default) @@ -161,7 +157,7 @@ defmodule CRDT.AWORMap do ...> |> CRDT.value() ** (KeyError) key :key not found in: %{} """ - @spec update!(t, actor, key, (crdt -> crdt)) :: t + @spec update!(t, CRDT.actor(), CRDT.key(), (CRDT.crdt() -> CRDT.crdt())) :: t def update!(%__MODULE__{entries: entries} = awor_map, actor, key, fun) when is_function(fun) do crdt = Map.fetch!(entries, key) From c9785597a33f7f9f091514f8d3ee9a9fae749895 Mon Sep 17 00:00:00 2001 From: Anton Bangratz Date: Mon, 9 Oct 2023 15:40:56 +0200 Subject: [PATCH 2/3] Implement LWWRegister --- lib/crdt/lww_register.ex | 74 +++++++++++++++++++++++++++++++++ test/crdt/lww_register_test.exs | 5 +++ 2 files changed, 79 insertions(+) create mode 100644 lib/crdt/lww_register.ex create mode 100644 test/crdt/lww_register_test.exs diff --git a/lib/crdt/lww_register.ex b/lib/crdt/lww_register.ex new file mode 100644 index 0000000..2c597f7 --- /dev/null +++ b/lib/crdt/lww_register.ex @@ -0,0 +1,74 @@ +defmodule CRDT.LWWRegister do + @moduledoc """ + A last-write-wins register + + Is used to store simple values with a timestamp. Using `merge` on two registers + will result in creating a register with the value corresponding to the most + recent timestamp + """ + + @type t :: %CRDT.LWWRegister{ + value: term | nil, + timestamp: {pos_integer(), pos_integer(), pos_integer()} + } + + defstruct value: nil, timestamp: System.system_time() + + @doc """ + Creates a new register with the value set to the supplied parameter or nil + and the current timestamp as returned by `System.system_time/0` + + iex> CRDT.LWWRegister.new() + %CRDT.LWWRegister{value: nil} + + iex> CRDT.LWWRegister.new("data") + %CRDT.LWWRegister{value: "data"} + + """ + @spec new :: t + def(new(), do: %CRDT.LWWRegister{}) + + @spec new(term) :: t + def new(data), do: %CRDT.LWWRegister{value: data} + + @doc """ + Sets the value of the register to the supplied parameter and updates the + timestamp to the current timestamp as returned by `System.system_time/0` + + iex> CRDT.LWWRegister.new() |> CRDT.LWWRegister.set("data") + ...> |> CRDT.value() + "data" + + """ + @spec set(t, term) :: t + def set(register, data) do + %CRDT.LWWRegister{register | value: data, timestamp: System.system_time()} + end +end + +defimpl CRDT, for: CRDT.LWWRegister do + @doc """ + Returns the value of the register + + iex> CRDT.LWWRegister.new("data") |> CRDT.value() + "data" + """ + def value(register), do: register.value + + @doc """ + Merges two registers and returns a new register with the value corresponding + to the most recent timestamp + + iex> CRDT.LWWRegister.new("data") |> CRDT.merge(CRDT.LWWRegister.new("data2")) + ...> |> CRDT.value() + "data2" + """ + + def merge(register1, register2) do + if register1.timestamp > register2.timestamp do + register1 + else + register2 + end + end +end diff --git a/test/crdt/lww_register_test.exs b/test/crdt/lww_register_test.exs new file mode 100644 index 0000000..f32cd08 --- /dev/null +++ b/test/crdt/lww_register_test.exs @@ -0,0 +1,5 @@ +defmodule CRDT.LWWRegisterTest do + use ExUnit.Case + doctest CRDT.LWWRegister + doctest CRDT.CRDT.LWWRegister +end From 9cf18e087a79c4835cd9ef14a54bd501f6e7e37e Mon Sep 17 00:00:00 2001 From: Anton Bangratz Date: Tue, 10 Oct 2023 16:19:12 +0200 Subject: [PATCH 3/3] Update specs with parentheses Keeping it consistent with Elixir documentation --- lib/crdt.ex | 10 +++++----- lib/crdt/access.ex | 6 +++--- lib/crdt/awor_map.ex | 14 +++++++------- lib/crdt/awor_set.ex | 6 +++--- lib/crdt/delta_awor_set.ex | 12 ++++++------ lib/crdt/dot_context.ex | 12 ++++++------ lib/crdt/dot_kernel.ex | 14 +++++++------- lib/crdt/g_counter.ex | 6 +++--- lib/crdt/lww_register.ex | 16 ++++++++-------- 9 files changed, 48 insertions(+), 48 deletions(-) diff --git a/lib/crdt.ex b/lib/crdt.ex index 804203f..94c9d9a 100644 --- a/lib/crdt.ex +++ b/lib/crdt.ex @@ -3,19 +3,19 @@ defprotocol CRDT do Protocol defining the interface for CRDTs. """ - @type actor :: term - @type key :: term - @type crdt :: term + @type actor :: term() + @type key :: term() + @type crdt :: term() @doc """ Returns the actual value of the CRDT """ - @spec value(t) :: term + @spec value(t()) :: term() def value(crdt) @doc """ Merges two CRDTs. """ - @spec merge(t, t) :: t + @spec merge(t(), t()) :: t() def merge(crdt1, crdt2) end diff --git a/lib/crdt/access.ex b/lib/crdt/access.ex index 18b104a..1151584 100644 --- a/lib/crdt/access.ex +++ b/lib/crdt/access.ex @@ -1,8 +1,8 @@ defprotocol CRDT.Access do - @spec get_in(t, nonempty_list(term)) :: term + @spec get_in(t(), nonempty_list(term())) :: term() def get_in(t, list) - @spec put_in(t, term, nonempty_list(term), CRDT.crdt()) :: CRDT.crdt() + @spec put_in(t(), term(), nonempty_list(term()), CRDT.crdt()) :: CRDT.crdt() def put_in(t, actor, list, value) - @spec update_in(t, term, nonempty_list(term), (term -> term)) :: CRDT.crdt() + @spec update_in(t(), term(), nonempty_list(term()), (term() -> term())) :: CRDT.crdt() def update_in(t, actor, list, fun) end diff --git a/lib/crdt/awor_map.ex b/lib/crdt/awor_map.ex index 97f0411..fe75d4c 100644 --- a/lib/crdt/awor_map.ex +++ b/lib/crdt/awor_map.ex @@ -24,7 +24,7 @@ defmodule CRDT.AWORMap do entries: %{} } """ - @spec new() :: t + @spec new() :: t() def new, do: %__MODULE__{} @doc """ @@ -43,7 +43,7 @@ defmodule CRDT.AWORMap do entries: %{key: %CRDT.GCounter{}} } """ - @spec put(t, CRDT.actor(), CRDT.key(), CRDT.crdt()) :: t + @spec put(t(), CRDT.actor(), CRDT.key(), CRDT.crdt()) :: t() def put(%__MODULE__{keys: keys, entries: entries}, actor, key, crdt) do CRDT.impl_for!(crdt) @@ -63,7 +63,7 @@ defmodule CRDT.AWORMap do ...> |> CRDT.AWORMap.fetch(:key) {:ok, %CRDT.GCounter{}} """ - @spec fetch(t, CRDT.key()) :: {:ok, CRDT.crdt()} | :error + @spec fetch(t(), CRDT.key()) :: {:ok, CRDT.crdt()} | :error def fetch(%__MODULE__{entries: entries}, key), do: Map.fetch(entries, key) @doc """ @@ -82,7 +82,7 @@ defmodule CRDT.AWORMap do iex> CRDT.AWORMap.new() |> CRDT.AWORMap.fetch!(:key) ** (KeyError) key :key not found in: %{} """ - @spec fetch!(t, CRDT.key()) :: CRDT.crdt() + @spec fetch!(t(), CRDT.key()) :: CRDT.crdt() def fetch!(%__MODULE__{entries: entries}, key), do: Map.fetch!(entries, key) @doc """ @@ -98,7 +98,7 @@ defmodule CRDT.AWORMap do ...> |> CRDT.AWORMap.get(:key, CRDT.GCounter.new()) %CRDT.GCounter{} """ - @spec get(t, CRDT.key(), CRDT.crdt()) :: CRDT.crdt() + @spec get(t(), CRDT.key(), CRDT.crdt()) :: CRDT.crdt() def get(%__MODULE__{entries: entries}, key, default \\ nil), do: Map.get(entries, key, default) @doc """ @@ -121,7 +121,7 @@ defmodule CRDT.AWORMap do ...> |> CRDT.value() %{key: 5} """ - @spec update(t, CRDT.actor(), CRDT.key(), CRDT.crdt(), (CRDT.crdt() -> CRDT.crdt())) :: t + @spec update(t(), CRDT.actor(), CRDT.key(), CRDT.crdt(), (CRDT.crdt() -> CRDT.crdt())) :: t() def update(%__MODULE__{entries: entries} = awor_map, actor, key, default, fun) when is_function(fun) do CRDT.impl_for!(default) @@ -157,7 +157,7 @@ defmodule CRDT.AWORMap do ...> |> CRDT.value() ** (KeyError) key :key not found in: %{} """ - @spec update!(t, CRDT.actor(), CRDT.key(), (CRDT.crdt() -> CRDT.crdt())) :: t + @spec update!(t(), CRDT.actor(), CRDT.key(), (CRDT.crdt() -> CRDT.crdt())) :: t() def update!(%__MODULE__{entries: entries} = awor_map, actor, key, fun) when is_function(fun) do crdt = Map.fetch!(entries, key) diff --git a/lib/crdt/awor_set.ex b/lib/crdt/awor_set.ex index d02d251..7c81d5c 100644 --- a/lib/crdt/awor_set.ex +++ b/lib/crdt/awor_set.ex @@ -23,7 +23,7 @@ defmodule CRDT.AWORSet do } } """ - @spec new() :: t + @spec new() :: t() def new, do: %__MODULE__{} @doc """ @@ -39,7 +39,7 @@ defmodule CRDT.AWORSet do } } """ - @spec add(t, actor, value) :: t + @spec add(t(), actor(), value()) :: t() def add(%__MODULE__{dot_kernel: dot_kernel}, actor, value) do %__MODULE__{ dot_kernel: @@ -62,7 +62,7 @@ defmodule CRDT.AWORSet do } } """ - @spec remove(t, value) :: t + @spec remove(t(), value()) :: t() def remove(%__MODULE__{dot_kernel: dot_kernel}, value) do %__MODULE__{dot_kernel: CRDT.DotKernel.remove(dot_kernel, value)} end diff --git a/lib/crdt/delta_awor_set.ex b/lib/crdt/delta_awor_set.ex index 9069a3d..8e8da0f 100644 --- a/lib/crdt/delta_awor_set.ex +++ b/lib/crdt/delta_awor_set.ex @@ -28,7 +28,7 @@ defmodule CRDT.DeltaAWORSet do } } """ - @spec new() :: t + @spec new() :: t() def new, do: %__MODULE__{} @doc """ @@ -48,7 +48,7 @@ defmodule CRDT.DeltaAWORSet do } } """ - @spec add(t, actor, value) :: t + @spec add(t(), actor(), value()) :: t() def add(%__MODULE__{dot_kernel: dot_kernel, delta: delta}, actor, value) do {dot_kernel, delta} = {dot_kernel, delta} @@ -75,7 +75,7 @@ defmodule CRDT.DeltaAWORSet do } } """ - @spec remove(t, value) :: t + @spec remove(t(), value()) :: t() def remove(%__MODULE__{dot_kernel: dot_kernel, delta: delta}, value) do {dot_kernel, delta} = {dot_kernel, delta} @@ -92,7 +92,7 @@ defmodule CRDT.DeltaAWORSet do iex> CRDT.DeltaAWORSet.new() |> CRDT.DeltaAWORSet.add(:a, "value") |> CRDT.DeltaAWORSet.value() ["value"] """ - @spec value(t) :: list + @spec value(t()) :: list() def value(%__MODULE__{dot_kernel: dot_kernel}) do CRDT.DotKernel.values(dot_kernel) end @@ -119,7 +119,7 @@ defmodule CRDT.DeltaAWORSet do } } """ - @spec merge(t, t) :: t + @spec merge(t(), t()) :: t() def merge( %__MODULE__{dot_kernel: dot_kernel_a, delta: delta_a}, %__MODULE__{dot_kernel: dot_kernel_b, delta: delta_b} @@ -151,7 +151,7 @@ defmodule CRDT.DeltaAWORSet do } } """ - @spec merge_delta(t, CRDT.DotKernel.t()) :: t + @spec merge_delta(t(), CRDT.DotKernel.t()) :: t() def merge_delta(%__MODULE__{dot_kernel: dot_kernel_a, delta: delta_a}, delta) do %__MODULE__{ dot_kernel: CRDT.DotKernel.merge(dot_kernel_a, delta), diff --git a/lib/crdt/dot_context.ex b/lib/crdt/dot_context.ex index f65a712..86b96c4 100644 --- a/lib/crdt/dot_context.ex +++ b/lib/crdt/dot_context.ex @@ -20,7 +20,7 @@ defmodule CRDT.DotContext do iex> CRDT.DotContext.new() %CRDT.DotContext{version_vector: %{}, dot_cloud: []} """ - @spec new() :: t + @spec new() :: t() def new, do: %__MODULE__{} @doc """ @@ -31,7 +31,7 @@ defmodule CRDT.DotContext do iex> CRDT.DotContext.new() |> CRDT.DotContext.contains?({:a, 1}) false """ - @spec contains?(t, dot) :: boolean + @spec contains?(t(), dot()) :: boolean def contains?( %__MODULE__{version_vector: version_vector, dot_cloud: dot_cloud}, {actor, version} = dot @@ -53,7 +53,7 @@ defmodule CRDT.DotContext do iex> CRDT.DotContext.new() |> CRDT.DotContext.next_dot(:a) {{:a, 1}, %CRDT.DotContext{version_vector: %{a: 1}, dot_cloud: []}} """ - @spec next_dot(t, actor) :: {dot, t} + @spec next_dot(t(), actor()) :: {dot(), t()} def next_dot(%__MODULE__{version_vector: version_vector} = dot_context, actor) do version = Map.get(version_vector, actor, 0) + 1 @@ -74,7 +74,7 @@ defmodule CRDT.DotContext do iex> CRDT.DotContext.new() |> CRDT.DotContext.add({:a, 1}) %CRDT.DotContext{version_vector: %{}, dot_cloud: [{:a, 1}]} """ - @spec add(t, dot) :: t + @spec add(t(), dot()) :: t() def add(%__MODULE__{dot_cloud: dot_cloud} = dot_context, dot) do %{dot_context | dot_cloud: [dot | dot_cloud]} end @@ -94,7 +94,7 @@ defmodule CRDT.DotContext do ...> |> CRDT.DotContext.compress() %CRDT.DotContext{version_vector: %{a: 3}, dot_cloud: [{:b, 2}, {:a, 5}]} """ - @spec compress(t) :: t + @spec compress(t()) :: t() def compress(%__MODULE__{version_vector: version_vector, dot_cloud: dot_cloud}) do {version_vector, dot_cloud} = for {actor, version} = dot <- Enum.sort(dot_cloud), reduce: {version_vector, []} do @@ -135,7 +135,7 @@ defmodule CRDT.DotContext do ...> ) %CRDT.DotContext{version_vector: %{a: 5}, dot_cloud: [{:b, 2}, {:a, 7}]} """ - @spec merge(t, t) :: t + @spec merge(t(), t()) :: t() def merge( %__MODULE__{version_vector: version_vector_a, dot_cloud: dot_cloud_a}, %__MODULE__{version_vector: version_vector_b, dot_cloud: dot_cloud_b} diff --git a/lib/crdt/dot_kernel.ex b/lib/crdt/dot_kernel.ex index df64dd0..1c04f08 100644 --- a/lib/crdt/dot_kernel.ex +++ b/lib/crdt/dot_kernel.ex @@ -24,7 +24,7 @@ defmodule CRDT.DotKernel do entries: %{} } """ - @spec new() :: t + @spec new() :: t() def new, do: %__MODULE__{} @doc """ @@ -53,7 +53,7 @@ defmodule CRDT.DotKernel do } } """ - @spec add(t, actor, value) :: t + @spec add(t(), actor(), value()) :: t() def add(%__MODULE__{dot_context: dot_context, entries: entries}, actor, value) do {dot, dot_context} = CRDT.DotContext.next_dot(dot_context, actor) @@ -63,7 +63,7 @@ defmodule CRDT.DotKernel do } end - @spec add({t, t}, actor, value) :: {t, t} + @spec add({t(), t()}, actor(), value()) :: {t(), t()} def add( { %__MODULE__{dot_context: dot_context, entries: entries}, @@ -115,7 +115,7 @@ defmodule CRDT.DotKernel do } } """ - @spec remove(t, value) :: t + @spec remove(t(), value()) :: t() def remove(%__MODULE__{entries: entries} = dot_kernel, value) do entries = for {dot, entry_value} <- entries, entry_value == value, reduce: entries do @@ -128,7 +128,7 @@ defmodule CRDT.DotKernel do %__MODULE__{dot_kernel | entries: entries} end - @spec remove({t, t}, value) :: {t, t} + @spec remove({t(), t()}, value()) :: {t(), t()} def remove( { %__MODULE__{entries: entries} = dot_kernel, @@ -164,7 +164,7 @@ defmodule CRDT.DotKernel do ["value"] """ - @spec values(t) :: list + @spec values(t()) :: list() def values(%__MODULE__{entries: entries}) do Map.values(entries) end @@ -188,7 +188,7 @@ defmodule CRDT.DotKernel do entries: %{{:a, 1} => "value1"} } """ - @spec merge(t, t) :: t + @spec merge(t(), t()) :: t() def merge( %__MODULE__{dot_context: dot_context_a, entries: entries_a}, %__MODULE__{dot_context: dot_context_b, entries: entries_b} diff --git a/lib/crdt/g_counter.ex b/lib/crdt/g_counter.ex index aaee78c..7d2c24a 100644 --- a/lib/crdt/g_counter.ex +++ b/lib/crdt/g_counter.ex @@ -17,7 +17,7 @@ defmodule CRDT.GCounter do iex> CRDT.GCounter.new() %CRDT.GCounter{value: %{}} """ - @spec new() :: t + @spec new() :: t() def new, do: %__MODULE__{} @doc """ @@ -28,7 +28,7 @@ defmodule CRDT.GCounter do iex> CRDT.GCounter.new(a: 1, b: 2) %CRDT.GCounter{value: %{a: 1, b: 2}} """ - @spec new([{actor, non_neg_integer}]) :: t + @spec new([{actor(), non_neg_integer()}]) :: t() def new(values) do %__MODULE__{value: Map.new(values)} end @@ -43,7 +43,7 @@ defmodule CRDT.GCounter do ...> |> CRDT.GCounter.inc(:b, 2) %CRDT.GCounter{value: %{a: 1, b: 2}} """ - @spec inc(t, actor, non_neg_integer) :: t + @spec inc(t(), actor(), non_neg_integer()) :: t() def inc(%__MODULE__{value: value}, actor, amount \\ 1) do %__MODULE__{value: Map.update(value, actor, amount, &(&1 + amount))} end diff --git a/lib/crdt/lww_register.ex b/lib/crdt/lww_register.ex index 2c597f7..94644b1 100644 --- a/lib/crdt/lww_register.ex +++ b/lib/crdt/lww_register.ex @@ -8,8 +8,8 @@ defmodule CRDT.LWWRegister do """ @type t :: %CRDT.LWWRegister{ - value: term | nil, - timestamp: {pos_integer(), pos_integer(), pos_integer()} + value: term() | nil, + timestamp: pos_integer() } defstruct value: nil, timestamp: System.system_time() @@ -25,10 +25,10 @@ defmodule CRDT.LWWRegister do %CRDT.LWWRegister{value: "data"} """ - @spec new :: t - def(new(), do: %CRDT.LWWRegister{}) + @spec new :: t() + def new, do: %CRDT.LWWRegister{} - @spec new(term) :: t + @spec new(term()) :: t() def new(data), do: %CRDT.LWWRegister{value: data} @doc """ @@ -40,9 +40,9 @@ defmodule CRDT.LWWRegister do "data" """ - @spec set(t, term) :: t - def set(register, data) do - %CRDT.LWWRegister{register | value: data, timestamp: System.system_time()} + @spec set(t(), term()) :: t() + def set(_register, data) do + %CRDT.LWWRegister{value: data, timestamp: System.system_time()} end end