Data Types
Edit on GitHubLists, numbers, and strings are all cool, but sometimes we need to represent more complex data structures. That’s where data types come in—they allow us to define our own types that are better suited to solve specific problems.
Enum Types
Enums allow you to represent data that has multiple variations. Combined with pattern matching, they will make you feel like you’ve got superpowers. They’re the backbone of programming in Grain, and you’ll come to find that they make it ridiculously easy to understand what a program does.
Let’s imagine for a moment that we’re vegetable farmers and we want to represent the vegetables we offer. We could represent them like this:
If we have many vegetables to offer, we could instead write it this way for readability:
This declaration does two things—it creates a new type called Veggie
and it creates data constructors for each vegetable, each of which is called a variant. Type names and variant names always begin with a capital letter.
Data Constructors
Data constructors allow us to create data. For the Veggie
type, we now have constructors for each vegetable variant, and each one shares its name.
If someone were to purchase our vegetables, their shopping cart might look like this:
Since this constructed data is just like any other value in Grain, we can do things like check equality.
This is great, but we’ll soon explore a much more powerful way to work with our data constructors in the section on pattern matching.
Compound Variants
What if we sold both red and green cabbage? While we might be tempted to create variants RedCabbage
and GreenCabbage
, a better way might be to give the kind of cabbage its own type.
1 | enum CabbageColor { Red, Green } |
If we wanted to associate a quantity with each vegetable, we could do that too:
1 | enum CabbageColor { Red, Green } |
Type Variables
Imagine a world where we sell both fruits and vegetables. If we wanted to represent our inventory, we might do it like this:
Instead of creating an inventory type for each kind of produce, we could instead create one type and use a type variable to allow us to use it with both fruits and vegetables.
Type variables always begin with a lowercase letter. Something to note from this example: although we’ve created one type that works with both fruits and vegetables, we would still not be allowed to mix inventory types, i.e. [Crate(Broccoli), Crate(Apples)]
because veggieInventory
has type Inventory<Veggie>
and fruitInventory
has type Inventory<Fruit>
.
Printing Variants
If your variant type is exported from your module, the variants are printable.
We haven’t discussed exports yet, but we’ll go much deeper into them in another section.
Record Types
Records are sort of like tuples, though each field has a name.
1 | record Person { name: String, age: Number } |
Record fields are accessed using the dot operator, i.e. record.field
.
Field Shorthand
If we create a binding with the same name as our record’s fields, we can use a shorthand to create our record.
Printing Records
If your record type is exported from your module, records of that type are printable.
We haven’t discussed exports yet, but we’ll go much deeper into them in another section.
Mutable record properties
We’ve previously created mutable let
bindings with the mut
keyword. In a similar fashion, we can also create mutable record fields.