I’m still fairly new to the language. I wanted to know more about the “Elixir” way of doing things so I wrote a tiny program 3 ways, but I’m not sure which is the “right way”. I know you’d normally want only one module per page, this is just for demonstration purposes.
This one has the most isoloation of responsibilities:
defmodule Shapes do
defmodule Rectangle do
defstruct [:width, :height]
def perimeter(%Rectangle{width: width, height: height}) do
2 * (width + height)
end
def area(%Rectangle{width: width, height: height}) do
width * height
end
end
defmodule Circle do
defstruct [:radius]
def area(%Circle{radius: radius}) do
radius * radius * :math.pi()
end
end
end
This one leverages pattern matching a little more.
defmodule Shape do
defmodule Rectangle do
defstruct [:width, :height]
end
defmodule Circle do
defstruct [:radius]
end
def perimeter(%Rectangle{width: width, height: height}) do
2 * (width + height)
end
def area(%Rectangle{width: width, height: height}) do
width * height
end
def area(%Circle{radius: radius}) do
radius * radius * :math.pi()
end
end
This is my attempt at implementing it as a protocol. I’m a little unsure
because to my understanding protocols are for taking in different types.
defprotocol Shape do
def area(data)
def perimeter(data)
end
defmodule Rectangle do
defstruct [:width, :height]
defimpl Shape, for: Rectangle do
def area(%Rectangle{width: width, height: height}) do
width * height
end
def perimeter(%Rectangle{width: width, height: height}) do
2 * (width + height)
end
end
end
defmodule Circle do
defstruct [:radius]
defimpl Shape, for: Circle do
def area(%Circle{radius: radius}) do
:math.pi() * radius * radius
end
end
end
You don’t need the for: when the defimpl is nested in the module it’s implementing for.
If this were actually intended to be reused I would also maybe make your modules be Shape.Rectangle and Shape.Circle to avoid module name piracy (keep as many top level namespaces open, we have aliases to help readability)
This sample project is almost a textbook example of polymorphism. Protocols are Elixir’s core way of supporting polymorphism. That being said, your second approach is not bad when you have limited expectations of the datatypes to support.
The main thing a protocol offers over the second approach is that you could publish the Protocol in one package. Then some other library author could choose to make their new data structure (which was unknown to you when you wrote the Protocol) fit into it.
I think your protocols version is great, but you should implement all the functions:
def perimeter(%Circle{radius: radius}) do
2 * :math.pi() * radius
end
It seems that even structs that don’t implement all the functions in a protocol have dummy placeholders. Here’s an example from the core library for Function, which doesn’t implement all the functions of Enumerable:
This is special though. All the “optional” callbacks of Enumerable can be implemented using the non optional reduce/3. The optional ones are only useful when the underlying datastructures allow for more efficient ways for those operations than using reduce/3. So you can still use Enum.count on any Enumerable, even if the implementation returns {:error, __MODULE__}.
That’s different to not implementing part of a protocol.
Also I think @optional_callbacks is the more appropriate way to mark optional callbacks, but Enumerable likely can’t do so for historical reasons.
Thanks for pointing that out - I didn’t check the implementation of Enumerable properly when I wrote that. I was looking for an example of a protocol that defines multiple functions, and an implementation that doesn’t implement them all. All the other protocols I can think of only define one function