Created
September 14, 2020 23:38
-
-
Save mbuhot/3ff150eee164cd8e73e3db799d1ea1fe to your computer and use it in GitHub Desktop.
Enforcing smart constructor in Elixir
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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