neuone

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 :atom keys 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, and previous/1 in 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

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

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.

Where Next?

Popular in Questions Top

rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
hpopp
To simplify some tasks at work, I wrote and published this package yesterday. It’s a simple macro that enables Access behaviour on struct...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New

Other popular topics Top

peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
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
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
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
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
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