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
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.
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.
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
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.
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.
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.
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.