Skip to main content

Enums

caution

The documentation that you're reading is a design document where most of the features you're reading are yet to be implemented. Check the Note on the Docs

In the previous section, we learned how to start using NeoHaskell's type system, we learned how to annotate constants and functions, and also we saw how to start modelling our domain using wrapper types, which allowed us to avoid primitive obssession and to give concrete more meaningful names to our types, more according to the domain of our application.

There are certain cases where we might want to model a thing that can only have a finite number of values, for example, the state of a lightbulb, the colors of a rainbow, the days of the week, etc. Instead of using strings to represent these values, we use enums.

Similar to wrapper types, enums allow you to avoid primitive obsession. For example, instead of using a Bool to represent the state of a lightbulb, we can use an enum with two cases: On and Off. This way, we can't accidentally pass a Bool that represents the state of a lightbulb to a function that expects a Bool that represents the state of a TV.

Defining an enum

To define an enum, we use the data keyword followed by the name of the enum and the cases that it can have, separated by vertical bars (|). For example, to define an enum that represents the state of a lightbulb, we can do the following:

data LightbulbState
= On
| Off

You can add as many cases as you want. In fact, if your application handles some specific values of a type with infinite values like Int, it is much better to use an enum instead of an Int because it will be much more clear what the function expects and what it returns.

Imagine that you're developing a videogame, and you want to represent with how many lives does a player start. Let's suppose that a player can only start with between two and six lives. Instead of using Int, which has no limits, we can use an enum with different cases. This way, we can't accidentally pass a 1 to a function that expects the number of starting lives of a player, or have a bug down there in our code that makes the player accidentally start with one million lives:

data PlayerLives
= Two
| Three
| Four
| Five
| Six

One advantage of NeoHaskell's enums over TypeScript's (and most mainstream languages) enums is that the values of a NeoHaskell enum are the values themselves and they are not usable in place of other types that are not the enum itself, while in TypeScript, for example, the values of an enum are numbers that represent the position of the case in the enum, and they can be used in any place where a number is expected. Compare the following examples:

data PlayerLives
= Two
| Three
| Four
| Five
| Six

neo> Two + 4
-- TYPE MISMATCH ----------------------------
Attempting to add two values, but the first value doesn't match the type of the second value:

Two + 4
^^^

`Two` is of type:

PlayerLives

But `(+)` needs the 1st argument to be:

Int

It definitely is much better to have this fail at compile time than to have a weird bug where there's a value called Two that is actually 0 and that can be used in place of a number.

Enums with Attached Values

In addition to simple enums, NeoHaskell allows the creation of more complex enums by attaching values to enum cases. These are powerful constructs that enable enums to carry additional, context-specific data. This feature is particularly useful when a simple label (like On or Off) isn't sufficient to express all the necessary information about an enumeration case.

Defining Enums with Attached Values

When defining an enum in NeoHaskell, you can specify one or more values to be attached to each case. These values can be of any type, including other enums or complex types. This makes enums a flexible tool for modeling a wide variety of data.

Consider a Color enum where a color can be represented in different color spaces. Some colors might be represented in the RGB color space, others in grayscale, and yet others as hexadecimal strings. Here's how you can define such an enum:

data Color
= Rgb Int Int Int -- Red, Green, Blue components
| Grayscale Int -- Intensity of gray
| Hex String -- Hexadecimal string

-- To create a value of type `Color`, you use the following syntax:
myFavoriteColor = Rgb 0 255 0

Each case of the Color enum carries different types of values:

  • RGB carries three Int values representing the red, green, and blue components of a color.
  • Grayscale carries a single Int representing the intensity of gray.
  • Hex carries a String representing the color in hexadecimal format.

This design encapsulates the concept that a color can be represented in different ways, but ultimately, it's still a color within the domain of your application.

Benefits of Enums with Attached Values

Using enums with attached values has several benefits:

  • Richer Data Modeling: You can model complex data structures in a type-safe manner, ensuring that the attached data aligns with the specific case of the enum.
  • Clarity: The code clearly communicates what data is expected with each enum case, leading to self-documenting code.
  • Safety: The compiler can enforce that all possible cases are handled in functions, reducing the likelihood of runtime errors. A function that doesn't handle all possible cases for an enum will fail to compile.

In summary, enums with attached values in NeoHaskell are a powerful feature for developers to express complex data structures cleanly and safely. They extend the utility of simple enums by allowing the carrying of additional information, which can be crucial for many applications that require detailed data representation and manipulation. And given the event-driven nature of NeoHaskell, enums with attached values are a natural fit for modeling events and their associated data.

Next Steps

We've learned how to define enums, and how to attach values to enum cases, but we haven't seen how to use them yet. In the next section, we'll start learning about handling boolean conditions through the usage of if-then-else, and later, we will transition towards pattern matching, which will help you to use your enums.