Wanting to know more about the "Elixir" way of doing things

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

Thank you for any help :slight_smile:

3 Likes

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)

2 Likes

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.

6 Likes

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.

1 Like

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 :thinking:

Protocols can’t have missing functions.

Behaviours can and usually the way to check is using function_exported?/3