1. The reason is not to implement Enumerable for Tuple
From the Elixir mailing list:
If there is an implementation of the protocol for the tuple, it contradicts all the entries. Given that user protocol instances are almost always specific to entries that add a tuple, the entire Enumerable protocol will make it pretty useless.
- Peter Minten
I wanted to list the tuples first, and even the "Enumerable" on them was eventually implemented, which did not work.
- Chris Keele
How does this violate the protocol? I will try to put everything together and explain the problem from a technical point of view.
Tuples. . What is interesting about tuples is that they are mainly used for duck printing , using pattern matching . You are not required to create a new module for a new struct every time you need some new simple type. Instead, you create a tuple - a kind of object of a virtual type. Atoms are often used as the first elements of type type names, for example {:ok, result}
and {:error, description}
. This is how tuples are used almost everywhere in Elixir because it is their design goal. They are also used as the basis for " records ", which comes from Erlang. Elixir has structures for this purpose, but also provides a Record module for Erlang compatibility. Therefore, in most cases, tuples are separate heterogeneous data structures that should not be listed. Tuples should be considered as instances of various virtual types. There is even an @type
directive that allows you to define custom types based on tuples. But remember that they are virtual, and is_tuple/1
still returns true for all these tuples.
Protocols . Elixir protocols, on the other hand, are class types that provide ad hoc polymorphism . For those who come from OOP, this is something like superclasses and multiple inheritance . One important thing the protocol does for you is automatic type checking. When you pass some data to a protocol function, it checks if this class belongs to this class, that is, this protocol is implemented for this data type. If not, you will get this error:
** (Protocol.UndefinedError) protocol Enumerable not implemented for {}
Thus, Elixir saves your code from stupid errors and complex errors if you are not mistaken in architectural decisions.
Generally. Now imagine that we are implementing Enumerable for Tuple. What he does is list all the tuples, while 99.9% of the tuples in Elixir are not designed for this. All checks are broken. The tragedy is the same as if all the animals in the world began to quack. If a tuple is passed to the Enum or Stream module accidentally, you will not see a useful error message. Instead, your code will produce unexpected results, unpredictable behavior, and possibly data corruption.
2. Reason not to use tuples as collections
Good, reliable Elixir code should contain typespecs , which will help developers understand the code and give Dialyzer the ability to test the code for you. Imagine you need a collection of similar elements. The typepec for lists and maps might look like this:
@type list_of_type :: [type] @type map_of_type :: %{optional(key_type) => value_type}
But you cannot write the same type for a tuple, because {type}
means "a tuple of one element of type type
". You can write typepec for a tuple of a predetermined length, for example {type, type, type}
, or for a tuple of any elements such as tuple()
, but there is no way to write a type specification for a tuple of similar elements just by design. Thus, choosing tuples to store your collection of elements means that you lose such good ability to make your code reliable.
Conclusion
The rule not to use tuples as lists of similar elements is a rule that explains how in most cases to choose the correct type in Elixir. Violation of this rule may be considered as a possible signal of poor design choice. When people say that “tuples are not for design collections,” it means not just “you are doing something unusual,” but “you can break Elixir’s functions by making the design wrong in your application.”
If you really want to use the tuple as a collection for some reason, and you are sure that you know what you are doing, then it would be nice to wrap it in some kind of struct . You can implement the Enumerable protocol for your structure without the risk of breaking the entire contents of tuples. It is worth noting that Erlang uses tuples as collections for the internal representation of array
, gb_trees
, gb_sets
, etc.
iex(1)> :array.from_list ['a', 'b', 'c'] {:array, 3, 10, :undefined, {'a', 'b', 'c', :undefined, :undefined, :undefined, :undefined, :undefined, :undefined, :undefined}}
Not sure if there is another technical reason not to use tuples as collections. If someone can provide another good explanation for the conflict between the Record and Enumerable protocol, they can improve that answer.