polar

Records

A record groups named values, like an object in JavaScript. Records are how you model most data in Polar.

Declaring a record type

Record types go in the types zone:

types
  Task = { id: Int, title: String, done: Bool }

  Post = { title: String, author: { name: String, email: String } }

A field can hold any type, including another record, as author does here.

Creating and reading records

Build a record with braces, and read a field with a dot:

let task: Task = { id: 1, title: "Write docs", done: false }

Log.info(task.title)

Every field has to be there. Leaving one out, or reading a field that doesn't exist, is a compile error.

Why the : Task?

On its own, { id: 1, title: "Write docs", done: false } is just a record with three fields. Writing : Task tells Polar it's a Task, which matters once Task has abilities of its own, like turning into JSON. A parameter or return type of Task does the same, so you only need the annotation on let.

Updating a record

Records can't be changed in place. To "update" one, make a copy with some fields replaced. ..t copies every field of t, and the fields after it replace the old ones:

complete(t: Task) -> Task {
  { ..t, done: true }
}

The original task is untouched. This makes it easy to reason about data: once you have a value, nothing else can change it behind your back.

Asking only for what you need

Here's where Polar's records get interesting. Say we want a function that returns a title. Both Task and Post have one. Instead of writing the function twice, we describe just the part we need:

title_of(r: { title: String | rest }) -> String {
  r.title
}

Read { title: String | rest } as "a record with a title of type String, and any other fields". rest stands for the other fields, whatever they are. This is called row polymorphism.

title_of(task)
title_of(post)
title_of({ title: "Inline", pages: 3 })

All three calls work. A record without a title doesn't:

error[POLAR0502]: missing field `title`
 --> records.px:9:23
  |
4 |   title_of(r: { title: String | rest }) -> String {
  |            --------------------------- `title` is required here
...
9 |     Log.info(title_of({ name: "no title here" }))
  |                       ^^^^^^^^^^^^^^^^^^^^^^^^^ this record has no field `title`

Keeping the rest

rest also works in return types. This function changes a title and hands back a record of the same shape, other fields included:

rename(r: { title: String | rest }, title: String) -> { title: String | rest } {
  { ..r, title: title }
}

Rename a Task and you get a Task back, so rename(task, "x").id still type-checks.

Tip: Row types are a good default for helper functions. A function that takes { title: String | rest } instead of Task keeps working as your types grow.

All together

module Records

types
  Task = { id: Int, title: String, done: Bool }

  Post = { title: String, author: { name: String, email: String } }

functions
  complete(t: Task) -> Task {
    { ..t, done: true }
  }

  title_of(r: { title: String | rest }) -> String {
    r.title
  }

  rename(r: { title: String | rest }, title: String) -> { title: String | rest } {
    { ..r, title: title }
  }

  main() {
    let task: Task = { id: 1, title: "Write docs", done: false }
    let post: Post = { title: "Hello", author: { name: "Ann", email: "ann@example.com" } }
    let finished = complete(task)

    Log.info("#{task.title}: done=#{task.done}, then done=#{finished.done}")
    Log.info("#{post.title} by #{post.author.name}")
    Log.info(title_of(task))
    Log.info(title_of(post))
    Log.info(title_of({ title: "Inline", pages: 3 }))
    Log.info(rename(task, "Write better docs").title)
    Log.info("#{rename(task, "x").id}")
  }

exports
  main
$ polar run records.px
Write docs: done=false, then done=true
Hello by Ann
Write docs
Hello
Inline
Write better docs
1

Records describe data that has all of its fields. Next, variants describe data that is one of several cases.