Language Tour
Custom Types

Custom types

Aiken's custom types are named collections of keys and/or values. They are similar to objects in object oriented languages, though they don't have methods.

Custom types are defined with the type keyword.

type Datum {
  Datum { signer: ByteArray, count: Int }
}

Here we have defined a custom type called Datum. Its constructor is called Datum and it has two fields: A signer field which is a ByteArray, and a count field which is an Int.

Once defined the custom type can be used in functions:

fn datums() {
  // Fields can be given in any order
  let datum1 = Datum { signer: #[0xAA, 0xBB], count: 2001 }
  let datum2 = Datum { count: 1805, signer: #[0xAA, 0xCC] }
 
  [datum1, datum1]
}

Shorthand notation

Because single constructors are quite common, there exists a special shorthand notation when the type and the constructor have the same name. So instead of the above, one can write:

type Datum {
  signer: ByteArray,
  count: Int
}

These two notations are synonym. With the shorthand, we implicitly indicate that there's a single constructor named Datum which can be used for constructing values of type Datum, or can also be used when destructuring (see below).

Multiple constructors

Custom types in Aiken can be defined with multiple constructors, making them a way of modeling data that can be one of a few different variants.

We've already seen a custom type with multiple constructors in the Language Tour - Bool.

Aiken's built-in Bool type is defined like this:

/// A Bool is a value that is either `True` or `False`
type Bool {
  True
  False
}

It's a simple custom type with constructors that takes no arguments at all! Use it to answer yes/no questions and to indicate whether something is True or False.

The records created by different constructors for a custom type can contain different values. For example a User custom type could have a LoggedIn constructor that creates records with a name, and a Guest constructor which creates records without any contained values.

type User {
  LoggedIn { count: Int }  // A logged in user
  Guest                    // A guest user with no details
}
let user1 = LoggedIn { count: 4 }
let user2 = LoggedIn { count: 2 }
let visitor = Guest

Option

We define Option as a generic type like so:

type Option<a> {
  None
  Some(a)
}

Then, functions which fail may safely return an optional value.

fn get_head(a: List<a>) -> Option<a> {
  when a is {
    [a, ..] -> Some(a)
    [] -> None
  }
}

The Option type is readily available in Aiken; it is part of the default types and values available by default. Don't hesitate to use it!

Destructuring

When given a custom type record we can pattern match on it to determine which record constructor matches, and to assign names to any contained values.

fn get_name(user) {
  when user is {
    LoggedIn { count } -> count
    Guest -> "Guest user"
  }
}

Custom types can also be destructured with a let binding.

type Score {
  Points(Int)
}
 
let score = Points(50)
let Points(p) = score // This brings a let-binding `p` in scope.
 
p // 50

During destructuring you may also use discards (_) or spreads (..).

type Dog {
  Dog { name: ByteArray, cuteness: Int, age: Int }
}
 
let dog = Dog { name: #"436173686577", cuteness: 9001, age: 3 }

You will need to specify all args for a pattern match, or alternatively use the spread operator.

// All fields present
let Dog { name: name, cuteness: _, age: _ } = dog
builtin.decode_utf8(name) // "Cashew"
 
// Other fields ignored by spreading.
// Field punning is supported. Hence `age` is a shorthand for `age: age`.
let Dog { age, .. } = dog
age // 3

Named accessors

If a custom type has only one variant and named fields they can be accessed using .field_name.

For example using the Dog type defined earlier.

let dog = Dog { name: #[82, 105, 110], cuteness: 2001 }
dog.cuteness // This returns 2001

Generics

Custom types can be be parameterised with other types, making their contents variable.

For example, this Box type is a simple record that holds a single value.

type Box<inner_type> {
  Box(inner: inner_type)
}

The type of the field inner is inner_type, which is a parameter of the Box type. If it holds an int the box's type is Box<Int>, if it holds a string the box's type is Box<String>.

fn foo() {
  let a = Box(420) // type is Box<Int>
  let b = Box("That's my ninja way!") // type is Box<String>
}

Record updates

Aiken provides a dedicated syntax for updating some of the fields of a custom type record.

type Person {
  name: ByteArray,
  shoe_size: Int,
  age: Int,
  is_happy: Bool,
}
 
fn have_birthday(person) {
  // It's this person's birthday, so increment their age and
  // make them happy
  Person { ..person, age: person.age + 1, is_happy: True }
}

The update syntax created a new record with the values of the initial record. It replaces the given binding with their new values.

Type aliases

A type alias lets you create a name which is identical to another type, without any additional information.

type MyNumber = Integer

They are most useful for simplifying type signatures.

type Person = (String, Integer)
 
fn create_person(name: String, age: Integer) -> Person {
  (name, age)
}

Data

At runtime custom types become an opaque Plutus' Data. In Aiken's type system Data matches with any user-defined type (but with none of the primitive types).

Thus, it's also possible to cast any custom type into Data. Extracting from Data however requires the use of expect

fn to_datum(datum: Data) -> Datum {
    expect d: Datum = datum
    d
}

Note that this conversion will fail if the given Data isn't actually a valid representation of the target type. The primary use-case here is for instantiating script contexts, datums and redeemers provided to scripts in an opaque fashion.

It's also useful for interacting with builtins that operate on raw Data. In this case, the conversation happens implicitly. Simply expect any function that accepts Data to automatically work on any custom type.

type Datum {
  count: Int,
}
 
let datum = Datum { count: 1 }
 
// fn(Data) -> ByteArray
builtin.serialise_data(datum) // some bytearray

expect

expect is a special keyword which works like let, but allows to perform some potentially unsafe conversions. There are two main use cases for it:

Non-exhaustive pattern matching

In cases where you have a value of a type that has multiple constructor variants, but are only truly interested in one of the possible variants as outcomes (i.e. any other outcomes invalidate the program), then expect is the perfect tool. Consider the Option type from above in the following example:

let x = Some(42)
 
// As a pattern-match
let y = when x is {
  None -> ???
  Some(y) -> y
}
 
// Using expect
expect Some(y) = x

Casting from Data into a custom type

Another crucial use of expect is to turn some opaque Data into a concrete representation. In the context of a Cardano smart-contract, we can encounter Data in various places although in general it's most likely when dealing with the datums attached to outputs.

Often, we do expect a specific structure for some given datum, and so being able to safely relay those assumptions in the validators comes in handy.

The syntax is identical to the other use case, but it requires an explicit type annotation.

type MyDatum {
  foo: Int,
  bar: ByteArray,
}
 
fn to_my_datum(data: Data) -> MyDatum {
  expect my_datum: MyDatum = data
  my_datum
}