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.