Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Enforcing smart constructor in Elixir
defmodule Opaque.User do
@moduledoc """
Demonstrates a ridiculous method for enforcing that a type remains in a valid state using a closure and internal secret
"""
alias __MODULE__
# Ensure that the internal closure can only be called from within this module
@update_key :crypto.strong_rand_bytes(8)
@enforce_keys [:internal]
defstruct [:internal]
@typedoc """
User struct with hidden internals
"""
@type t :: %__MODULE__{
internal: function()
}
@doc """
Constructor - validates name, dob and email
"""
def new(name: name, dob: dob, email: email) do
with :ok <- validate_name(name),
:ok <- validate_dob(dob),
:ok <- validate_email(email) do
wrap(%{
name: name,
email: email,
dob: dob
})
end
end
@doc "Name accessor"
def name(%User{internal: f}), do: f.(:get, :name)
@doc "Validate and update name"
def with_name(user = %User{}, new_name) do
with :ok <- validate_name(new_name) do
update(user, :name, new_name)
end
end
@doc "DOB accessor"
def dob(%User{internal: f}), do: f.(:get, :dob)
@doc "Email accessor"
def email(%User{internal: f}), do: f.(:get, :email)
@doc "Validate and update email"
def with_email(user = %User{}, new_email) do
with :ok <- validate_name(new_email) do
update(user, :email, new_email)
end
end
# Validation rules
defp validate_name(name) when is_binary(name), do: :ok
defp validate_dob(%Date{}), do: :ok
defp validate_email(email) when is_binary(email), do: :ok
# internal field update
defp update(user, key, value) do
user.internal.(:update, {@update_key, key, value})
end
# Wrap the internal state in a closure
defp wrap(state) do
%User{
internal: fn
:get, key -> Map.fetch!(state, key)
:update, {@update_key, key, value} -> wrap(%{state | key => value})
end
}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.