Enum types in Kotlin, Elixir, and Elm
“‘Begin at the beginning’, the King said, very gravely,
‘and go on till you come to the end: then stop.’”
– Lewis Carroll, Alice in Wonderland
1. Introduction
The goal of this blog post is to define the concept of enum types and compare the implementation of enum types in three different functional programming languages: Kotlin, Elixir, and Elm.
The post is structured as follows. In Section 2, we define the concept of enum types. Then, in Sections 3, 4, and 5 we look at concrete implementations of enum types in Kotlin, Elixir, and Elm, respectively. The post is concluded in Section 6.
2. Enum types
In this section, we define the concept of enum types.
An enum - short for enumerated - type is a data type consisting of a set of named values which we call the members of the type. One of the most common enum types in modern programming languages is the boolean type, which has exactly two members: true and false. We can express this boolean type in an ML-like syntax as:
where we declare a datatype
with the name boolean
that has
two type constructors, True
and False
(separated by a |
), corresponding to the two members of the
enum type. Consequently, any instance of the type boolean
can only have
the value of either True
or False
, which allows us to do
exhaustive pattern matching
like so:
where we define a function, foo
, that takes an argument, bool
, of type boolean
and cases on its members/type constructors, True
and False
, using a case
<var> of ...
expression.
For practical reasons, a boolean type is usually included by default in most
modern programming languages, so in the following three sections we instead look
at how to express a shape
enum type, with members Rectangle
, Circle
,
and Triangle
:
in each of our three programming languages of choice. Furthermore, in order to
see how each language handles pattern matching, we also implement an example
function, edges
, which takes an argument of type shape
and returns the
number of edges of the given shape:
In the next section, we implement shape
and edges
in Kotlin.
3. Enum types in Kotlin
In this section, we implement the shape
enum type and edges
example function
in Kotlin.
Given Java’s strong
influence on Kotlin, it is no surprise that Kotlin has inherited
Java’s
class-oriented paradigm,
where all non-primitive data types are defined in terms of classes. Furthermore,
Kotlin has also inherited the enum
keyword from Java, which - as the name
suggests - is used for defining enum types (or classes). Thus, in order to
define our custom enum type we declare our new type as enum class Shape
followed by listing each of the members of the enum type, Rectangle
, Circle
,
and Triangle
:
each separated by a ,
and terminated with a ;
.
Kotlin also allows us to do pattern matching on enums, as demonstrated below
where we define the edges
function, which takes an argument of type Shape
and returns its number of edges:
Instead of a case <var> of ...
expression, Kotlin uses a when (var) {...}
expression for pattern matching and as in our reference example in the previous
section, the body of the expression includes a clause for each of the members of
the enum type (class).
Before we move on, there are a few things worth noting about the above code snippets:
- Despite Kotlin’s Java heritage, it allows us easily to separate data,
Shape
, and logic,edges
, such that we do not have to introduce the concept of edges into our enum class definition as a method, but instead we can define a separate function,edges
, somewhere else in the source code, resulting in lower coupling, - we do not need to include an
else
clause in the body of thewhen
expression, as the pattern matching ofshape
in thewhen
expression is exhaustive, and lastly, - since
edges
takes an argument of typeShape
rather thanShape?
, the type system enforces the constraint thatedges
cannot be called with anull
reference, which helps make Kotlin code easier to reason about than traditional Java code, as it reduces the number of needed null checks.
Finally, in order to test the above code, we write a main
function which
instantiates a variable of type Shape
and prints the result of calling edges
on it:
Having implemented our shape
enum type and edges
example function in Kotlin,
while demonstrating how to pattern match on its members, we move on to repeat
the exercise in Elixir.
4. Enum types in Elixir
In this section, we implement the shape
enum type and edges
example function
in Elixir.
Unlike Kotlin, Elixir does not have a dedicated keyword or construct for
defining an enum type as part of the language, so instead we have to use the
@type
directive to declare our own enum types. The @type
directive allows us
to combine existing types, and instances of types, into new custom types. These
custom types can then be enforced by a static analysis tool
like dialyxir, which is used for type
checking Elixir source code.
In order to define our shape
enum in Elixir, we create a module named Shape
and declare a custom @type
named t
inside of it, where the members of t
are the atoms :rectangle
,
:circle
, and :triangle
:
Here, ::
separates the name of the type, on the left, from its definition, on
the right, while |
separates each of the members of the type, and finally :
is used for constructing each of the atoms.
Having defined the above module, we can then refer to the shape
enum type as
Shape.t
, in the same way as we would refer to the String.t
type.
Similar to the Kotlin case, we define an example function, edges
, which given
an argument of type Shape.t
, returns the numbers of edges of the matched
Shape.t
member, via pattern matching:
Here, the case expression used in Elixir, case <var> do ...
, is very
similar to the case expression used in Section 2, case
<var> of ...
, and likewise for the actual clauses for each of the members.
Once again, we notice a few things about the code snippets above:
- While Kotlin allowed us to separate the logic for calculating the number of
edges from the actual definition of the
Shape
enum type, this is always the case in Elixir, as it does not include constructs for combining data and logic as classes, and - just as we used the
@type
directive to define our custom type, we use the@spec
directive to state that theedges
function takes as input a value of typeShape.t
and returns a value of typeinteger
.
Again, we can test the above code by instantiating a value of type Shape.t
and
pass it to edges
:
While the function call and definition above looks very similar to the Kotlin
version, there is one distinctive difference: because of Elixir’s dynamic type
checking, we cannot fully guarantee that edges
is never given an argument
that is not of type Shape
on runtime, which may result in a runtime error if
we don’t include an else
-clause in the case
expression. However, by
specifying proper type signatures of our functions combined with Elixir’s
excellent type inference engine and tools like dialyxir, all of which we have
discussed above, we can do much to reduce this risk without scattering
else
-clauses in our code.
Finally, we note that we could also have written edges
in a slightly more
Elixir idiomatic way:
where we inline the pattern matching in the function declaration. In this
particular case, we chose the former style with the case
expression as it
closer resembles the other example snippets.1
Having implemented our shape
enum type and edges
example function in both
Kotlin and Elixir, we move on to our final language example, Elm.
5. Enum types in Elm
In this section, we implement the shape
enum type and edges
example function
in Elm.
In the case of Elm, we return to an ML-like syntax similar to what we saw at the
beginning of this post, where we define our type, Shape
, using the type
keyword followed by listing each of the members of the type, Rectangle
, Circle
, and
Triangle
, separated by |
:
As in the Kotlin case, we can do exhaustive pattern matching without any
else
-clause in our case
expression, as the Shape
type can only be
constructed using the three listed members:
While the above snippets are very similar to the original examples in
Section 2, there is the added function declaration,
edges : Shape -> Int
, which states that edges
takes a value of type
Shape
and returns a value of type Int
.
Note also, that unlike Kotlin and Elixir, we do not even have to think about the
possibility of passing a null
or nil
reference to edges
, as these concepts
are not even part of the Elm language.
Finally, in order to run the above code, we implement the main
function, where
we instantiate a value of type Shape
, pass it to the edges
function, and
print it as a text DOM element:
Having implemented our shape
enum type and edges
example function in our
third and final language, Elm, we conclude this post in the next section.
6. Conclusion
In this blog post, we have defined the concept of enum types, and compared the implementation of enum types in the three different programming languages: Kotlin, Elixir, and Elm.
Across the three implementations, we notice that Elixir is the only language which does not have a dedicated keyword or construct for defining enum types while Elm has the highest level of type safety by default, as it does not include the concept of a null reference.
Functional programming
- « Previous |
- Archive |
- Next »