Record
Records are like JavaScript objects but are
lighter
immutable by default
fixed in field names and types
very fast
a bit more rigidly typed
Usage
Type (mandatory):
REtype person = {
age: int,
name: string
};
Value (this will be inferred to be of type person
):
RElet me = {
age: 5,
name: "Big Reason"
};
Access (the familiar dot notation):
RElet name = me.name;
Record Needs an Explicit Definition
If you only write {age: 5, name: "Baby Reason"}
without an explicit declaration somewhere above, the type system will give you an error. If the type definition resides in another file, you need to explicitly indicate which file it is:
RE/* School.re */
type person = {age: int, name: string};
RE/* example.re */
let me: School.person = {age: 20, name: "Big Reason"};
/* or */
let me = School.{age: 20, name: "Big Reason"};
/* or */
let me = {School.age: 20, name: "Big Reason"};
Either of the above 3 says "this record's definition is found in the School file". The first one, the regular type annotation, is preferred.
Immutable Update
New records can be created from old records with the ...
spread operator. The original record isn't mutated.
RElet meNextYear = {...me, age: me.age + 1};
This update is very efficient! Try a few in our playground to see how records are compiled.
Note: spread cannot add new fields, as a record's shape is fixed by its type.
Mutable Update
Record fields can optionally be mutable. This allows you to update those fields in-place with the =
operator.
REtype person = {
name: string,
mutable age: int
};
let baby = {name: "Baby Reason", age: 5};
baby.age = baby.age + 1; /* alter `baby`. Happy birthday! */
Syntax shorthand
To reduce redundancy, we provide punning for a record's types and values. Punning refers to the syntax shorthand you can use when the name of a field matches the name of its value/type:
REtype horsePower = {power: int, metric: bool};
let metric = true;
let someHorsePower = {power: 10, metric};
/* same as the value {power: 10, metric: metric}; */
type car = {name: string, horsePower};
/* same as the type {name: string, horsePower: horsePower}; */
Note that there's no punning for a single record field! {foo}
doesn't do what you expect (it's a block that returns the value foo
).
Tips & Tricks
Record Types Are Found By Field Name
With records, you cannot say "I'd like this function to take any record type, as long as they have the field age
". The following works, but not as expected:
REtype person = {age: int, name: string};
type monster = {age: int, hasTentacles: bool};
let getAge = (entity) => entity.age;
The last line's function will infer that the parameter entity
must be of type monster
. The following code's last line fails:
RElet kraken = {age: 9999, hasTentacles: true};
let me = {age: 5, name: "Baby Reason"};
getAge(kraken);
getAge(me);
The type system will complain that me
is a person
, and that getAge
only works on monster
. If you need such capability, use Reason objects, described here.
Design Decisions
After reading the constraints in the previous sections, and if you're coming from a dynamic language background, you might be wondering why one would bother with record in the first place instead of straight using object, since the former needs explicit typing and doesn't allow different records with the same field name to be passed to the same function, etc.
The truth is that most of the times in your app, your data's shape is actually fixed, and if it's not, it can potentially be better represented as a combination of variant (introduced next) + record instead*.
Record, since its fields are fixed, is compiled to an array with array index accesses instead of JS object (try it in the playground!). On native, it compiles to basically a region of memory where a field access is just one field lookup + one actual access, aka 2 assembly instructions. The good old days where folks measured in nanoseconds...
Finally, since a record type is resolved through finding that single explicit type declaration (we call this "nominal typing"), the type error messages end up better than the counterpart ("structural typing", like for tuples). This makes refactoring easier; changing a record type's fields naturally allows the compiler to know that it's still the same record, just misused in some places. Otherwise, under structural typing, it might get hard to tell whether the definition site or the usage site is wrong.
* And we're not just finding excuses for ourselves! Reason objects do support these features.