Product types in Kotlin, Elixir, and Elm
prerequisites: Enum types in Elm, Elixir, and Kotlin
“All for one and one for all,
united we stand divided we fall.”
– Alexandre Dumas, The Three Musketeers
1. Introduction
The goal of this blog post is to define the concept of product types and compare the implementation of product 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 product types. Then, in Sections 3, 4, and 5 we look at concrete implementations of product types in Kotlin, Elixir, and Elm, respectively. The post is concluded in Section 6.
2. Product types
In this section, we define the concept of product types.
A product type is a composite data type that compounds two or more types in a fixed order; we call these compounded types the fields of the product type. A common example of a product type is the point type, which compounds two float types, corresponding to an x- and a y-coordinate, into a new type. We can express this point type in our ML-like syntax as:
where we declare point
as a datatype
consisting of two float values, the x-
and y-coordinate, separated by a *
(not be confused with the multiplication
operator). Any instance of the point
type is then
a tuple of two floats, e.g. (3, 2)
. We
can access the fields of such a tuple using pattern matching - sometimes also
called destructuring:
Here, we construct a point
named p
as the tuple (3, 2)
, then assign its
fields to x
and y
, by pattern matching on the structure of the tuple, and
finally add them together. Product types defined in terms of tuples may also be
called tuple types.
Unfortunately, defining products as tuples has the downside that it is not clear
from the actual definition of a type what is the semantic meaning behind each of
its fields, e.g. without the comment in the definition of the point
type
above, it is not clear which float
corresponds to x
and which one
corresponds to y
. However, we can improve the situation by requiring that each
field of a product type has to be assigned a name, which gives us the following
new definition of the point
type:
Any instance of the point
type is now a tuple with named fields, e.g. (x = 3,
y = 2)
. Product types defined in terms of named fields are also called record
types or structs.
Accessing the named fields of a product type, without having to pattern match on
its whole structure, is straightforward, as seen in the following example, where
we compute
the Euclidean distance
between two points, p
and q
:
Here, we access an individual field using the common <var>.<field>
expression.
In each of the following sections, we return to the shape
example from
our
previous post,
and implement product type versions of each of the different shapes:
rectangle
, circle
, and triangle
. Specifically, we define each of the
shapes in terms of their corresponding mathematical definition, i.e. a rectangle
has a height and width, a circle has a radius, and a triangle has a base
and a height. In our ML-like syntax, we express this as follows:
For our example function, we want to compute the area of each of these three different shapes, so we have to implement corresponding area functions:
Note that in our reference implementation above, we use destructuring on each of the different types directly in the header of their corresponding area function, in order to make the definitions more concise. Alternatively, we could have chosen to access the fields of the product types without destructuring, e.g.
In the next section, we implement the rectangle
, circle
, and triangle
product types along with their corresponding area example functions in Kotlin.
3. Product types in Kotlin
In this section, we implement the rectangle
, circle
, and triangle
product
types along with their area functions in Kotlin.
As discussed in the previous post, Kotlin is heavily influenced by Java which means that all non-primitive data types are defined in terms of classes, and product types are no exception. Likewise, we also discussed that we prefer to separate data and logic, and thus would like to avoid defining our product types as plain old classes, e.g.
Instead, we would like to signal to the Kotlin compiler - and other developers -
that we are defining product types, which should not do much beyond store some
data. Fortunately, Kotlin introduces the concept
of data class, which
does exactly this while also automatically deriving reasonable implementations
of equals
, toString
, and copy
. Defining our product types, Rectangle
,
Circle
, and Triangle
, as data classes is now straightforward, as we just
need to add the data
keyword before the class
keyword:
Note also the conciseness Kotlin brings when specifying a class, Rectangle
,
and its fields, height
and width
, compared to a traditional Java class.
Implementing our three area functions is also rather straightforward, as each function takes an argument of their expected shape type and returns the calculated area of that type:
If we wanted to pattern match on the fields of each of the types, as demonstrated in the previous section, we could instead use Kotlin’s destructuring declarations to do just that:
However, in the case of our area functions, it would not do much in terms of making the code more elegant.
Finally, in order to test our code, we implement the main
function which
instantiates a variable of type Rectangle
and prints the result of calling
rectangle_area
on it:
Having implemented our product types, rectangle
, circle
, and triangle
,
along with their area functions in Kotlin, we move on to repeat the exercise in
Elixir.
4. Product types in Elixir
In this section, we implement the rectangle
, circle
, and triangle
product
types along with their area functions in Elixir.
In order to define our different shape types in Elixir, we take a slightly different approach than in the case of the enum type, by encapsulating each of our types in a module named after the corresponding type:
Breaking down the above definition, we first look at the @type
declaration of
t
, where __MODULE__
refers to the name of the enclosing module, Rectangle
,
and the %<name>{<property_name>: <property_type>, ...}
construct declares a
struct
type called <name>
and with a set of <property_name>: <property_type>
pairs. While the @type
directive declares the Rectangle.t
type, the
defstruct
keyword defines the actual data structure of a Rectangle
, by
taking a list of [<property_name>: <default_value>]
as its arguments,
corresponding to the properties declared in our type declaration. In this case,
we define the type Rectangle
to have two properties, height
and width
,
both of type float
and both with default value 0.0
.
We define Circle
and Triangle
in a similar manner:
We can now refer to the three product types as Rectangle.t
, Circle.t
, and
Triangle.t
respectively, allowing us to define our three area functions, which
given an argument of the corresponding shape type, returns the computed area of
that shape:
Note, that Elixir allows us to pattern match not just on the type but also directly on its fields at the same time, making them readily available in the body of the function declaration.
We test the code by instantiating a value of type Rectangle.t
and pass it to
its area function:
While the Kotlin and Elixir implementations are quite similar in many ways, it is noteworthy that the concept of pattern matching on the structure of types is a more natural feature of the Elixir language compared to Kotlin.
Having implemented our rectangle
, circle
, and triangle
product types in
Kotlin, we move on to our final language example, Elm.
5. Product types in Elm
In this section, we implement the rectangle
, circle
, and triangle
product
types along with their area functions in Elm.
In order to implement our product types, rectangle
, circle
, and triangle
,
in Elm, we can use a syntax similar to what we saw in Section 2. We specify a
product type using the type alias
keywords followed by listing each of the
fields of the type, e.g. height
and width
, separated by ,
and encapsulated
by {...}
:
As in the Elixir case, we can pattern match (or destructure) our product type arguments directly in the header of our function declarations:
thus making our code more concise. Besides a few syntactic differences, there is not much difference between the ML-like reference example and our actual Elm implementation.
Once again, we implement the main
function, in which we instantiate a value of
type Rectangle
, pass it to the rectangleArea
function, and print it as a
text DOM element:
Having implemented our rectangle
, circle
, and triangle
product types in
Elm, we are ready to conclude this post in the next section.
6. Conclusion
In this blog post, we have defined the concept of product types, and compared the implementation of product types in the three different programming languages: Kotlin, Elixir, and Elm.
While all three languages support product types on a language level, we note that pattern matching on the structure of types in general is a fundamental part of programming in Elixir, and thus it shines a bit brighter here than the other languages.
Functional programming
- « Previous |
- Archive |
- Next »