neuone
Converting an enumerated type from swift into an equivalent in elixir
Overview
I want to convert a Swift enumerated type into something similar in Elixir. I do not want to use any external libraries as I wanted to see how far I can take this.
Here is the Swift code I want to convert to Elixir:
note: about indirect keyword
the indirect keyword reference themselves, and they are called “indirect” because they modify the storage mechanism in Swift to accommodate any size. Without indirection, an enum that references itself could become infinitely sized, which is not possible. To handle self-referencing associated values, we mark the enum as indirect.
Example 1
- Using
:atomkeys to define a constant key, which are similar to Enumerated types in other languages according to my research. - Error correction is implemented.
- Added the ability to view all possible types with
all_cases\0.
defmodule Example1.Result do
defstruct [:type, :score]
@types ~w(player cpu tie previous)a
@doc """
Returns a list of the possible result types
## Example
iex> Result.all_cases()
[:player, :cpu, :tie, :previous]
"""
def all_cases(), do: @types
@doc """
Returns a result type when a `type` is passed.
## Examples
iex> Result.score(:player)
%Result{type: :player, score: 0}
iex> Result.score(:player, 100)
%Result{type: :player, score: 100}
iex> Result.score(:cpu, 100)
%Result{type: :cpu, score: 100}
iex> Result.score(:tie, 100)
%Result{type: :tie, score: 100}
iex> Result.score(:previous, %Result.score(:player, 100))
%Result{type: :previous, score: %Result{type: :player, score: 100}}
"""
def score(type, score \\ 0)
def score(type, score) when type in @types, do: put_score(type, score)
def score(type, _score), do: raise(ArgumentError, message: "You cannot use #{inspect(type)}")
defp put_score(:player, score), do: %__MODULE__{type: :player, score: score}
defp put_score(:cpu, score), do: %__MODULE__{type: :cpu, score: score}
defp put_score(:tie, score), do: %__MODULE__{type: :tie, score: score}
defp put_score(:previous, previous = %__MODULE__{type: type}) when type in @types,
do: %__MODULE__{type: :previous, score: previous}
defp put_score(:previous, _previous),
do:
raise(ArgumentError,
message:
"You must pass in a previous Result that has a known type of :player, :cpu, :tie, :previous"
)
end
Example 2
- Using a Sum Type (or tagged unions or discriminated unions) so that I can enumerate all the possiblities that the type can take. Using tuples.
- The functions
player/1,cpu/1,tie/1, andprevious/1in the Result module act as constructors for the Sum Type of@type result - I was reading up on typespecs and changed my approach.
defmodule Example2.Result do
@type score :: Integer.t()
@type result ::
{:player, score}
| {:cpu, score}
| {:tie, score}
| {:previous, result}
defstruct [:value]
@type t() :: %__MODULE__{
value: result
}
@spec player(score) :: Result.t()
def player(score), do: put_value(:player, score)
@spec cpu(score) :: Result.t()
def cpu(score), do: put_value(:cpu, score)
@spec tie(score) :: Result.t()
def tie(score), do: put_value(:tie, score)
@spec previous(Result.t()) :: Result.t()
def previous(my_result = %__MODULE__{}), do: put_value(:previous, my_result)
defp put_value(type, score), do: %__MODULE__{value: {type, score}}
end
Example 3
- Elixir allows nesting modules and module names are also atoms, as mentioned in this source.
- This approach is similar to the first one, but utilizes nested modules to explore what gets returned at the call site.
defmodule Example3.Result do
defmodule Player do
defstruct [:score]
end
defmodule CPU do
defstruct [:score]
end
defmodule Tie do
defstruct [:score]
end
defmodule Previous do
defstruct [:result]
end
@types ~w(player cpu tie previous)a
alias __MODULE__.{
Player,
CPU,
Tie,
Previous
}
@doc """
Returns a result type when a `type` is passed.
## Examples
iex> Result.cpu(:player)
%Result.Player{score: 0}
iex> Result.score(:player, 100)
%Result.Player{score: 100}
iex> Result.score(:cpu, 100)
%Result.CPU{score: 0}
iex> Result.score(:tie, 100)
%Result.Tie{score: 100}
iex> Result.score :previous, %Result.Player{score: 100}
%Result.Previous{result: %Result.Player{score: 100}}
"""
def score(type, score \\ 0)
def score(type, score) when type in @types, do: put_score(type, score)
def score(type, _score), do: raise(ArgumentError, message: "You cannot use #{inspect(type)}")
defp put_score(:player, score), do: %Player{score: score}
defp put_score(:cpu, score), do: %CPU{score: score}
defp put_score(:tie, score), do: %Tie{score: score}
defp put_score(:previous, previous), do: put_previous(previous)
defp put_previous(previous = %Player{}), do: %Previous{result: previous}
defp put_previous(previous = %CPU{}), do: %Previous{result: previous}
defp put_previous(previous = %Tie{}), do: %Previous{result: previous}
defp put_previous(_previous),
do: raise(ArgumentError, message: "You must pass in a previous Result.Previous")
end
Usage and Ergonomics
Example 1 - is idiomatic (I think)
iex> Result.score(:player, 100)
%Result{type: :player, score: 100}
Example 2 - improves ergonomics with a constructor.
iex> Result.player(100)
%Result{value: {:player, 100}}
Example 3 - uses nested modules to future-proof the struct, allowing additional information to be added.
iex> Result.score(:player, 100)
%Result.Player{score: 100}
Conclusion
My goal is to understand what is the ideal approach to this in Elixir.
- Example 2 is growing on me as the solution. I have to get used to returning a tuple as my final value, but that’s okay.
- I do like name spacing my modules like in Example 3.
Thanks for reading any feedback?
Most Liked
dimitarvp
I always liked #3 but I found it overkill in practice so nowadays I use #2. It’s simple, readable and you can add Dialyzer typespecs like you did.
To me it’s the most practical option. You can always go ham with dedicated modules for each enum value and standardized constructors etc. but it’s important to not have the line count of your project explode.
aenglisc
Defining a struct for every type gets too big too quickly. Unless your goal is to use protocols (which I see little reason for here) I would say it’s more effort than it’s worth.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









