Is it possible to declare a generic type?

typespecs
defstruct

#1

For example I have multiple currencies

dollar.ex

defmodule Multicurrency.Currency.Dollar do
  @enforce_keys [:amount]
  defstruct [:amount]

  @type t :: %__MODULE__{
    amount: integer()
  }

  @spec new(number()) :: Multicurrency.Currency.Dollar.t()
  def new(amount) do
    %__MODULE__{amount: amount}
  end
end

franc.ex

defmodule Multicurrency.Currency.Franc do
  @enforce_keys [:amount]
  defstruct [:amount]

  @type t :: %__MODULE__{
    amount: integer()
  }

  @spec new(number()) :: Multicurrency.Currency.Franc.t()
  def new(amount) do
    %__MODULE__{amount: amount}
  end
end

They are basically the same and I wonder if it’s possible to have some kind of a generic struct or a protocol for structs that will define common keys and allow to pattern match on the instances of that struct:

defmodule Multicurrency.Currency do
  @enforce_keys [:amount]
  defstruct [:amount]

  @type t :: %__MODULE__{
    amount: integer()
  }

end
defmodule Multicurrency.Currency.Franc do
  @impl Multicurrency.Currency

  @spec new(number()) :: Multicurrency.Currency.t()
  def new(amount) do
    %__MODULE__{amount: amount}
  end
end
def equals?(Multicurrency.Currency{amount: a1},  Multicurrency.Currency{amount: a2}) do
    a1 == a2
  end

Does it even make sense?


#2

You can with the built-in Protocol if you handle all currency types within your singular protocol implementation or double-dispatch out to another protocol for the specific currency type (which should be fine).

If you truly want to protocol dispatch over an instance binding (I.E. full match) then the built-in Protocol cannot do that but ProtocolEx can (and it can be faster than Elixir protocols as well). But I don’t recommend it until you actually Need proper match semantics.


#3

What about pattern-matching in functions? Or that is a totally different strategy, i.e. not an Elixir-way?


#4

That’s what I meant in my first option, pattern match in the functions of the implementation (or the direct calls, however you are doing it). This is perfectly fine if you don’t need it to be extensible via plugins or so.


#5

Yeah but you can only pattern match on the map keys, so you can’t be certain you’re getting a currency, you may be just getting a struct that happens to have an amount type.


#6

You can match on the struct type as well.


#7

But you can’t match on multiple struct types at once, correct?

How would you combine these into one clause?

def add(%Multicurrency.Currency.Franc{} = currency) do ... end

def add(%Multicurrency.Currency.Dollar{} = currency) do ... end

#8

Not the prettiest, but:

def add(%currency_type{} = currency) 
when currency_type in [Multicurrency.Currency.Franc, Multicurrency.Currency.Dollar] do 
end

But you could turn part of that into a guard.


#9

That’s what I’m talking about. It feels that this can be solved in a more straightforward manner.


#10

If you’re on the newest erlang you could do.

defguard is_currency(currency) 
when :erlang.map_get(:__struct__, currency) in [Multicurrency.Currency.Franc, Multicurrency.Currency.Dollar]

def add(currency) when is_currency(currency) do
end

#11

I realize it’s side-stepping the question, but this really sounds more like a use case for a record: {:currency, “HKD”, amount}. Then add, equals?, etc. are trivial to write.

What are you trying to achieve with the structs?


#12

Is that easier than %Currency{denomination: "HKD", amount: amount}?

I think the flaw with @lessless’s plan honesty is making a struct per denomination.


#13

Will the Money library help you?


#14

Guys, just for the clarity - this is not the code I’m writing for the production system.
I’m just following the Money example in the TDD by Example book.

The reason why I asked about generic structure is that I saw how useful similar concepts are in Rust and wondered to which extend we can do such thing in Elixir.

Thanks for the replies everyone! :heart:


#15

Another option is something like this in the generic module (would be Currency):

@base_map [
    %{
      key: "for-db",
      module: EuroOrSomething,
      name: "For UI"
    },
    ...
  ]

and then among others

@modules Enum.reduce(@base_map, [], &[&1.module | &2])

with that you’d have @modules compiled which you can use for such “generics”. The function head that uses it would look like

def func(%{__struct__: module} = struct, params) when module in @modules

#16

If it’s for educational purposes only, I’d do this:

defmodule Money do
  defstruct [:currency, :amount]

  def work_with_francs(%{currency: "FRF", amount: amount}) do
  end

  def work_with_dollars(%{currency: "USD", amount: amount}) do
  end
end

No need for protocols, normal pattern-matching will get you a long way. Additionally, those functions will work not only with the struct but with any map that has the currency and amount keys.