We've seen a whirlwind introduction to Unison data types before, but here we'll equip you with the information you need to create your own types. We'll talk about thestructural
andunique
keywords, and learn about Unison's syntax for defining types with named fields.
Let's say we are modeling a local library--we might want to start by representing a couple of book genre options. In a scratch.u file, we'll write something like:
type Genre
type Genre
= fundamentals.dataTypes.uniqueAndStructuralTypes.Genre.Fiction
| fundamentals.dataTypes.uniqueAndStructuralTypes.Genre.Poetry
| fundamentals.dataTypes.uniqueAndStructuralTypes.Genre.CookBooks
| fundamentals.dataTypes.uniqueAndStructuralTypes.Genre.Science
| fundamentals.dataTypes.uniqueAndStructuralTypes.Genre.Biography
Let's break down the anatomy of this code. Thetype
keyword tells the UCM that we're creating a new type as opposed to aterm definition.A type definition needs a modifier ofunique
orstructural
before it, which tells Unison whether the type is unique by its name or by its structure (more on that in a bit). The values after the equals sign are known asdata constructors.Each data constructor is separated by a pipe|
which means that theGenre
type can bePoetry
orFiction
orCookBooks
etc.
The data constructors forGenre
above don't have any arguments, so they don't contain data beyond tagging something as being one thing or another at the moment.
The meaning of "unique"
We indicated earlier thatGenre
isunique
by its name. By applying the modifierunique
we've indicated to the type system that the name of this type issemanticallyimportant. An example of a structurally identical type might be aWeekday
(below):
structural type Weekday
structural type Weekday
= fundamentals.dataTypes.uniqueAndStructuralTypes.Weekday.Mon
| fundamentals.dataTypes.uniqueAndStructuralTypes.Weekday.Tues
| fundamentals.dataTypes.uniqueAndStructuralTypes.Weekday.Wed
| fundamentals.dataTypes.uniqueAndStructuralTypes.Weekday.Thurs
| fundamentals.dataTypes.uniqueAndStructuralTypes.Weekday.Fri
NeitherWeekday
norGenre
accept type parameters to construct a value of the type, and both have five data constructors which each have zero arguments, but obviously these model very different domains. Because we've indicated that the typeGenre
is unique, it cannot be substituted by aWeekday
or anything else with its same structure.
What happens if we create identical structural types?
Say we want to represent an author type with a name, a birth year, and aSet
ofGenre
that they might might be filed under. Let's try to make this one astructural
type.
structural type Author = Author Text Nat (Set Genre)
In this case, thestruct.Author
type has a single data constructor which happens to be called "Author." Following the data constructor name, we specify thetypesthat are needed to make an author:Text
,Nat
,and aSet
ofGenre
.Note that calling the data constructor here the same name as the type is just a convention, we could have given the type a differently named data constructor altogether.
Creating an instance of thisstruct.Author
type looks like:
authorExample : Author
authorExample =
genres = Set.fromList [CookBooks]
Author "Julia Child" 1912 genres
We'd also like to model the idea of a book with a title, a publication year, and a set of genres that might describe it.
structural type Book = Book Text Nat (Set Genre)
Great, let's write a function which tries to find a book for a given author. In our scratch file we might write something like:
bookFinder : Author -> [(Author, Book)]-> Optional Book
bookFinder author list =
map = Map.fromList list
get author map
When we save our scratch file we see the following output:
⍟ These new definitions are ok to `add`:
type Genre
type Author
type Book
bookFinder : Author -> [(Author, Author)] -> Optional Author
Our types show up as expected, there's aGenre
,anAuthor
,and aBook
that can be added, but taking a closer look at our signature forbookFinder
you'll notice that the types that we wrote in our function definition earlier seem to have been changed! 🤔 This is because the types as we've defined them are equivalent to the Unison type system. BothBook
andAuthor
have data constructors which take in aText
,aNat
,and aSet
of the same things. These two types are freely interchangeable in Unison programs. This is a situation in which we might want to make both types unique by theirnameto avoid any mix-ups. WhileBook
andAuthor
are structurally identical, it matters that the types mean different things.
type uniqueVersion.Author
type uniqueVersion.Author
= fundamentals.dataTypes._uniqueAndStructuralTypes.uniqueVersion.Author.uniqueVersion.Author.Author
Text Nat
type uniqueVersion.Book
type uniqueVersion.Book
= fundamentals.dataTypes._uniqueAndStructuralTypes.uniqueVersion.Book.Book
Text Nat
Now our UCM output reflects our named types.
⍟ These new definitions are ok to `add`:
bookFinder : Author -> [(Author, Book)]-> Optional Book
Why use structural types?
It might seem like structural types introduce confusion when modeling a domain, after all, the role of a type system is to help enforce what types are compatible with each other. Consider the following: you're a library developer and you introduce a structural type calledMaybe
.Maybe is fairly abstract in nature, but you intend to use it to capture when a thing "might be" present or not. (I know, suspend your disbelief here.) The things that a "Maybe" contains are not specific, so you've used the type parameterst
to define it.
structural type Maybe t = Just t | Nothing
When you save yourscratch.u
file to addMaybe
to the codebase, you discover that it's structurally identical to theOptional
data type inbase
.
⍟ These new definitions are ok to `add`:
structural type Maybe t
(also named Optional)
You've got a suite of helpful functions to work with already! The functions in your program which were written to return aMaybe
can be manipulated by the functions forOptional
.
Structural types can be useful when writing code with fewer domain-specific requirements. There are some cases in which using a structural type might reveal that you don't need to reinvent the wheel. One rule of thumb to help decide whether to make a type structural or not is to think if the type you're defining has additional semantics or expected behavior beyond the information that's given in the type signature.