Is it possible to declare a generic type?

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?

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.

1 Like

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

1 Like

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.

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.

You can match on the struct type as well.

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

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.

1 Like

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

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
3 Likes

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?

1 Like

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

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

6 Likes

Will the Money library help you?

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:

1 Like

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

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.

Did you find a solution for this? I’ve the same issue where I have many structs that all implement the same protocol and would like a catch all to pattern match on them.

The solution in the case exposed here was probably to not use different structs for different currencies, but rather one struct with a field for the currency, as @benwilson512 and @dimitarvp noted. That makes it easy to pattern match.

Elixir protocols and pattern matching are both very powerful features, but they work fundamentally differently than the type system and dispatching in, say, Rust.

What is your specific case @jmnsf?

1 Like

I have several Ops (sum, subtract, multiply, add, reverse, etc.) who all implement the protocol Calculable. I also have other, fundamentally different things that implement the protocol Calculable, for example an AverageWeeklyTemperature module. One might make the case of putting all ops in the same module (lets call it Op) and pattern matching on the type, but we’d be hard-pressed to mix those with an API call to a weather service.

I wish to write a function that accepts calculables that matches the argument as one would a struct.

I’m beginning to suspect this isn’t possible though?

Not in the way you would in a language with classical inheritance.

The kind of dispatching offered by Elixir is usually more flexible than inheritance though. You can define an implementation of the protocol for each separate type (it’s up to you how to best split those types), you can even define a fallback implementation for Any, and you can even derive a protocol implementation. The protocol implementation can be defined by the module implementing the protocol, or “centralized” in a place for multiple different modules.

In all those cases though, it takes thinking about types in a different way than in an inheritance-based language.