Here is my proposition:
defmodule Stats do
required_keys = [:false_negative, :false_positive, :true_positive]
@enforce_keys required_keys
defstruct required_keys ++ [annotations: 0, precision: 0, recall: 0]
@type t :: %__MODULE__{
annotations: integer,
false_negative: integer,
false_positive: integer,
precision: float,
recall: float,
true_positive: integer
}
@spec init(t) :: t
def init(%__MODULE__{true_positive: true_positive} = struct) do
computed = %{
precision: init_computed(true_positive, struct.false_positive),
recall: init_computed(true_positive, struct.false_negative)
}
Map.merge(struct, computed)
end
defp init_computed(true_positive, value), do: true_positive / (true_positive + value)
end
Stats.init(%Stats{annotations: 3, false_negative: 1, false_positive: 2, true_positive: 1})
It would fail if you did not pass 3 keys (specified in @enforce_keys
) which are required to compute.
Alternatively you could write something like:
defmodule Stats do
required_keys = [:false_negative, :false_positive, :true_positive]
@enforce_keys required_keys
defstruct required_keys ++ [annotations: 0, precision: 0, recall: 0]
@type t :: %__MODULE__{
annotations: integer,
false_negative: integer,
false_positive: integer,
precision: float,
recall: float,
true_positive: integer
}
@spec init(map, integer, integer, integer) :: t
def init(map, false_negative, false_positive, true_positive) do
full_map =
map
|> Map.put(:false_negative, false_negative)
|> Map.put(:false_positive, false_positive)
|> Map.put(:precision, init_computed(true_positive, false_positive))
|> Map.put(:recall, init_computed(true_positive, false_negative))
|> Map.put(:true_positive, true_positive)
struct(__MODULE__, full_map)
end
defp init_computed(true_positive, value), do: true_positive / (true_positive + value)
end
This way allows to pass map (don’t require struct), but forces to pass required keys separately.
Finally you can also do it in more pattern-matching like way:
defmodule Stats do
required_keys = [:false_negative, :false_positive, :true_positive]
@enforce_keys required_keys
defstruct required_keys ++ [annotations: 0, precision: 0, recall: 0]
@type init_map :: %{
optional(:annotations) => integer,
optional(:precision) => float,
optional(:recall) => float,
required(:false_negative) => integer,
required(:false_positive) => integer,
required(:true_positive) => integer
}
@type t :: %__MODULE__{
annotations: integer,
false_negative: integer,
false_positive: integer,
precision: float,
recall: float,
true_positive: integer
}
@spec init(init_map) :: t
def init(
%{
false_negative: false_negative,
false_positive: false_positive,
true_positive: true_positive
} = map
) do
computed = %{
precision: init_computed(true_positive, false_positive),
recall: init_computed(true_positive, false_negative)
}
__MODULE__ |> struct(map) |> Map.merge(computed)
end
defp init_computed(true_positive, value), do: true_positive / (true_positive + value)
end
Which allows to pass required keys in map.