Defining your own Unison data types

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 thestructuralanduniquekeywords, 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. Thetypekeyword tells the UCM that we're creating a new type as opposed to aterm definition.A type definition needs a modifier ofuniqueorstructuralbefore 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 theGenretype can bePoetryorFictionorCookBooksetc.

The data constructors forGenreabove don't have any arguments, so they don't contain data beyond tagging something as being one thing or another at the moment.

🎨
By convention, the UCM will put the data constructors for a type in a namespace which shares the type name; so the data constructors for the typeGenrecan be referenced likeGenre.PoetryorGenre.CookBooks

The meaning of "unique"

We indicated earlier thatGenreisuniqueby its name. By applying the modifieruniquewe've indicated to the type system that the name of this type issemanticallyimportant. An example of a structurally identical type might be aWeekday(below):

NeitherWeekdaynorGenreaccept 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 typeGenreis unique, it cannot be substituted by aWeekdayor anything else with its same structure.

👋

If you accidentally re-add a unique term to the codebase without any changes, Unison treats the "re-addition" as anew data type.It's unique after all! 😅

It's a good practice to place your unique data types under the---fold once you've added them, or remove them from your scratch file altogether when you're not directly changing the data types.

What happens if we create identical structural types?

Say we want to represent an author type with a name, a birth year, and aSetofGenrethat they might might be filed under. Let's try to make this one astructuraltype.

structural type Author = Author Text Nat (Set Genre)

In this case, thestruct.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:Text,Nat,and aSetofGenre.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.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 aGenre,anAuthor,and aBookthat can be added, but taking a closer look at our signature forbookFinderyou'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. BothBookandAuthorhave data constructors which take in aText,aNat,and aSetof 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. WhileBookandAuthorare 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 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 parameterstto define it.

structural type Maybe t =  Just t | Nothing

When you save yourscratch.ufile to addMaybeto the codebase, you discover that it's structurally identical to theOptionaldata 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 aMaybecan 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.

🌻

Summary:

  • Unison data types can be definedstructural|unique type MyType = DataA | DataB
  • Disambiguate your types semantically by theirnamewithuniquekeyword.
  • Structural types are considered equivalent when theirdata constructorsand parameters are structurally identical.