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 the
uniquekeywords, 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:
Let's break down the anatomy of this code. The
typekeyword tells the UCM that we're creating a new type as opposed to aterm definition.A type definition needs a modifier of
structuralbefore 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 the
Genretype can be
The data constructors for
Genreabove 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 that
uniqueby its name. By applying the modifier
uniquewe've indicated to the type system that the name of this type issemanticallyimportant. An example of a structurally identical type might be a
Genreaccept 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 type
Genreis unique, it cannot be substituted by a
Weekdayor anything else with its same structure.
What happens if we create identical structural types?
structural type Author = Author Text Nat (Set Genre)
In this case, the
struct.Authortype 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:
Genre.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 this
struct.Authortype 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 a
Bookthat can be added, but taking a closer look at our signature for
bookFinderyou'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. Both
Authorhave data constructors which take in a
Setof 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. While
Authorare structurally identical, it matters that the types mean different things.
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 called
Maybe.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 parameters
tto define it.
structural type Maybe t = Just t | Nothing
When you save your
scratch.ufile to add
Maybeto the codebase, you discover that it's structurally identical to the
Optionaldata type in
⍟ 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 a
Maybecan be manipulated by the functions for
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.
Found on the right hand side of the equals sign in a type declaration, a data constructor describes how to construct a value of some type.