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

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.

1 Like

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
1 Like

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.

1 Like

Or you can use the domo library. :person_shrugging:

1 Like

I will probably use this or new, and return a Result for this quick and dirty case and keep domo in mind.

My current understanding is that Ecto is popular for validating DB persisted structs, and there is some momentum around libraries that validate data received from external services. It looks like domo has some users, but it’s not used everywhere, and the norm for structs that are not persisted are various patterns mentioned above? I am curious if you think domo will keep growing and start to become a library that people use in most projects, or if there is some resistance to introducing a library for logic that most people feel they can implement on their own quickly?

I would say that any library to achieve something like that would need to:

  1. be a part of some generator provided by Elixir core team or related (ecto, phoenix etc.)

  2. have guides about it everywhere (just like now it’s for phoenix for example)

  3. have a big community (which comes from points 1 and 2, but it may be required by some companies) - especially a big number of users who test every release

Look that ecto does not have it’s own generators, but support for it is important in phoenix generators. It’s widely adopted, well tested and especially every bigger/complete/serious Elixir/Phoenix tutorial adds lots of information about it.

On the other side if we have x number of libraries doing similar thing where:

  1. x > 1 and every of those libraries would have active support
  2. it’s not a part of any generator
  3. it’s not commonly mentioned “on every tutorial”
  4. it has at most a limited number of enthusiasts who simply likes it

there is no chance that one library would be used in most projects especially if it would not be needed in simplest cases. Developers for each project would use at most one library which:

  1. They knows/like the most
  2. The library features covers app-specific use cases
  3. They convince a leader that it needs to be used

Look that old Phoenix templates would be used almost only in unmaintained projects. Almost all projects already moved or would move to heex pretty soon not because it’s best for everything and there would never be a better solution, but because a Phoenix core team (of course they had a reason for it) would drop support for it in newest releases.

Personally I’m subscribing Phoenix.HTML goes slim · Issue #372 · phoenixframework/phoenix_html · GitHub issue. You can answer yourself … Do you know and use in most projects temple library? I have used it before introducing heex engine.

Of course I’m not saying that only Elixir/Ecto/Phoenix core teams are doing a great libraries, but the chances to adopt their libraries in most projects are much higher because of support (as above - supported in official generator), respect for their knowledge/skills and a huge communities around them.

1 Like

It is. I only used domo once and liked it but a lot of people are not happy with compilation and macro hackery and I can understand and sympathize.

In your case, a plain old struct + @enforce_keys + a new function + a non-adversarial usage will do the trick just fine.

I have been using these for a long time:

You mention about something about stopping a user from creating a struct with invalid fields. User is another developer working on the project or user who will be using the application?

There is NimbleOptions NimbleOptions — NimbleOptions v1.1.0.

Elixir community is small, it does not have the volume/size like other languages:

  • in phoenix forum, there are around 30-40 posts with activity in past week. Look at site stats section About - Elixir Programming Language Forum for forum activity levels.
  • many projects being discussed in this forum are hobby or learning projects, they won’t be making to production.
  • one can see a thousand stars for a JS library on Github which does something trivial.
  • can’t really interpolate these numbers to python or js world.

This does not translate to a problem with language, framework or libraries. Adoption numbers, popularity and momentum numbers are on a different scale in elixir world.

json_xema - i have been using from around 2019/2020(i don’t remember exact date). It just works, it has 57 stars and 4 forks. On hex.pm it has around 200 downloads a day.

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.

5 Likes

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.

3 Likes

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.

2 Likes

I think opaque doesn’t apply to my use case because I want users to be able to access .foo?

Yes so in that case opaque wouldn’t work indeed.
Or you need to add a getter function in your struct module, which might to a bit too much :slight_smile: