Implementing String.Chars

Hi, I’m new to learn Elixir. Not my first functional language, but just trying to get used to the language quirks. I’m trying to implement the String.Chars protocol for a custom data type.

I’m implementing a Solitaire solver in Elixir to learn. I’ve got some basic logic implemented here:

When I run the above program, I get “Elixir.Three of Spades” not just “Three of Spades”. What am I missing here?

Hello and welcome to the forum,

Module names are Atom under the hood, and Elixir prefix those with Elixir…

For example.

iex> to_string Jason
"Elixir.Jason"
iex> to_string Enum 
"Elixir.Enum"

Nvm, I’ve see my error now. I’m returning atoms from my show_rank function instead of strings.

Within elixir, module names are just atoms (as you have discovered). In order to play nicely on the BEAM with other languages, module names are actually all prefixed with Elixir. so your module name is actually :Elixir.Card (as you’ve also discovered). If you call Kernel.to_string/1 on a module name your’ll get the full name which is not often what you want.

If you call Kernel.inspect/1 you’ll get Card. Thats the inspect protocol at work.

I appreciate this is a bit orthogonal to your original issue but it might help make another part of Elixir clearer to you later on.

Another comment if you’ll permit, its probably more idiomatic to pattern match on function heads than to use a case expression. For example:

  def show_suit(0), do: "Spades"
  def show_suit(1), do: "Hearts"
  def show_suit(2), do: "Clubs"
  def show_suit(3), do: "Diamonds"
  def show_suit(n), do: raise(ArgumentError, "unknown suit: #{inspect suit}")
2 Likes

Why should I prefer matching on function heads?
The case statement is more to the point, I don’t have to repeatedly type in the function name.

They’ll compile to the same bytecode anyway and some consider it bad style to have a function which body is only a single case

Personally I’d do it like this:

@suite_names %{0 => "Spades", 1 => "Hearts", 2 => "Clubs", 3 => "Diamonds"}

@suite_names
|> Enum.each(fn {id, name} ->
  def show_suite(unquote(id)), do: unquote(name)
end

Omitting even the explicit raise, as the implicit will happen anyway, and I do not consider the nicer error that important…

Though to be really honest, I’d not even use numeric suite IDs, but atoms…

1 Like

Elixir (from Erlang) leans heavily on pattern matching to be expressive of intent. It is also the most optimised control path. Your example might be simple, but you’ll find a great deal of Elixir and Erlang code oriented around pattern matching over conditional expressions. And therefore people reading Elixir code are very used to parsing multiple function heads and understanding program flow around that.

1 Like

To add to this sentiment:

I love breaking up conditionals into function clauses, it’s my favorite feature of the BEAM.

The reason why is that it lets me separate requisite expectations from navigable ones at a syntax level, rather than a semantic level.


A strict function clause declares, “given these conditions, I can try to do this work”.

Scrolling through all function heads for a function lets you know what work it can attempt. Reading through each function clause implementation lets you know how it tries to do that work, and how it handles failure if it cannot proceed. But you know the work not specified in a function head will never be attempted.

By this compass, the guiding philosophy of “let it crash” is illuminated: don’t attempt anything you already know you can’t handle; instead, immediately let the caller know it asked for an invalid operation so it can decide how to recover. Promoting requirements into pattern-matched function heads and not providing fallbacks is just a clean way of declaring what you are willing to handle–versus everything else you are not willing to, and let crash (and recover to a known good state).


In this example, you know ahead of time which suits you are able to handle: 0 ("Spades"), 1 ("Hearts"), 2 ("Clubs"), or 3 ("Diamonds"). Similarly with cards of rank 0..12.

A case statement navigates what you know you are able to handle, and can provide a fallback. But in this situation you already know there is no reasonable fallback: when a caller asks for a card of suit 9001, let it crash.

Additionally:

  • When a caller asks for a card of a rank outside of 0..12, let it crash.
  • When a caller asks for a Queen of Hearts, but it’s already been dealt… then maybe handle the situation by telling it to draw again.

The difference between the former and the latter is that in the former, the caller is asking for something so impossible given business requirements, you know it must be in a bad state, and should rethink its understanding of the world; in the latter, it is trying its best but just slightly misinformed about the current state of play.


Let it crash” is about separating the former from the latter, and function heads are an elegant boundary to differentiate between them. case statements in functions work as well, but are under-utilizing the expressiveness of the BEAM.

Notably, you can detect issues with the former request without consulting a database of dealt cards–and choosing to do so as early as possible improves the speed of your program, as well as understandability of it. Moving this sort of insight into a function head just helps communicate intent to maintainers quicker, as well as possibly improving the runtime of your program.

2 Likes