kanishka

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

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

sabiwara

Elixir Core Team

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

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

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

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

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.

Where Next?

Popular in Questions Top

JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
Fl4m3Ph03n1x
Background Let’s assume I have a typical GenServer that receives messages as requests, does some operation in a DB and returns responses....
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

We're in Beta

About us Mission Statement