kanishka
Is there an equivalent to a "private constructor" for elixir structs?
Are there patterns similar to having a private constructor along with a public build/validate function to guide users of a struct module to build valid instances? I understand that you may not be able to completely stop a user from creating a struct with invalid field values, but just wonder if there are patterns for something along those lines. I understand that you can use Ecto validations with embedded schemas to express some constraints, but wondering if there some pattern for plain structs
Marked As Solved
sodapopcan
There is not. A common pattern is to define a new/1 for this and you just have to document that that is what should be used.
Edit: sorry “there is not” referring to not being able to mail a struct as private.
Also Liked
sabiwara
In addition to other answers, if you are using typespecs and dialyzer it might be interesting to add a definition of an opaque type for your struct to achieve some kind of encapsulation.
defmodule MyStruct do
@opaque t :: %__MODULE__{foo: integer()}
@enforce_keys [:foo]
defstruct [:foo]
# functions in this module can create or manipulate internals of t()
@spec new(integer()) :: t()
def new(foo) when is_integer(foo), do: %__MODULE__{foo: foo}
end
defmodule OtherModule do
# dialyzer will complain since you can't assume the internals of MyStruct.t() outside of MyStruct
@spec create_struct() :: MyStruct.t()
def create_struct() do
%MyStruct{foo: 0}
end
end
Dialyzer will report:
other_module.ex:3:contract_with_opaque
The @spec for OtherModule.create_struct/0 has an opaque
subtype MyStruct.t() which is violated by the success typing.
While this doesn’t offer strong guarantees, it can help detect cases that are by-passing your constructor in your codebase.
gregvaughn
A few years back I used Ecto with embedded schemas to act as an anti-corruption layer at the edge of our system to validate incoming in-memory data from third parties into known structs for our core business logic. I ended up writing about a 20-25 line module with a __using__ macro that streamlined it for the codebase. I prefer that over adding a dependency.
tme_317
After trying several community options I’ve been using @bitwalker’s simple Strukt library for a few months and really like it. It is very similar to using embedded schemas and validations but without much of the boilerplate. It defines new and change functions in the module with several methods of validation compatible with Ecto changesets.
kartheek
Elixir does have concept of constructors - they are from OOPS world. There is map and there is struct which is map with additional field __struct__. There are functions which operate on data. There are Modules which group functions together. I might be oversimplifying things…
I use @enforce_keys and init(opts) to initialise structs (not everywhere though).
@enforce_keys [keys_list]
defstruct [keys]
def init(opts \\ %{}) do
# do some validations, and throw if needed
%__MODULE__{
#set data
}
end
Eiji
There is no solution which covers all use cases, but a pure Elixir validation is pretty simple to write, for example with code below:
defmodule Example do
@required_keys ~w[sample]a
@enforce_keys @required_keys
defstruct [:sample]
def sample(data) when is_list(data) do
@required_keys
|> Enum.reject(&Keyword.has_key?(data, &1))
|> sample(data)
end
defp sample([], data) do
case validate(data) do
{:error, reason} -> {:error, reason}
data -> struct(__MODULE__, data)
end
end
defp sample(missing_fields, _data) do
{:error, "missing fields: " <> Enum.join(missing_fields, ", ")}
end
def sample!(data) when is_list(data) do
case validate(data) do
{:error, reason} -> raise reason
data -> struct!(__MODULE__, data)
end
end
defp validate(data) do
Enum.reduce_while(data, %{}, fn {key, value}, acc ->
case validate(key, value) do
# when value is validated put it using key
{:ok, value} -> {:cont, Map.put(acc, key, value)}
# otherwise when validation fails return error
{:error, reason} -> {:halt, {:error, reason}}
end
end)
end
defp validate(:sample, value) when is_integer(value), do: {:ok, value}
defp validate(:sample, _value), do: {:error, "sample is not an integer"}
defp validate(_key, value), do: {:ok, value}
end
the struct would not be validated (except @enforce_keys when using it “by hand” like %Example{}. Officially there is no support for preventing others to use write struct by hand.







