Records
Flix supports row polymorphic extensible records.
Flix records are immutable (but may contain mutable reference cells).
Record Literals
A record literal is written with curly braces:
{ x = 1, y = 2 }
which has the record type
{ x = Int32, y = Int32 }
.
The order of fields in a record does not matter. Hence the above record is equivalent to:
{ y = 2, x = 1 }
which has type { y = Int32, x = Int32 }
. This type is equivalent to { x = Int32, y = Int32 }
. In other words, the order of fields within a record type
does not matter.
Field Access
We can access the field of a record using a dot:
let p = { x = 1, y = 2 };
p.x + p.y
The type system ensures that we cannot access a field that does not exist.
Records are immutable. Once constructed, the values of the record fields cannot be changed.
Field Update
While records are immutable, we can construct a new record with an updated field value:
let p1 = { x = 1, y = 2 };
let p2 = { x = 3 | p1 };
p1.x + p2.x
The expression { x = 3 | p1 }
updates the record p1
with a new value of its
x
field. Note that updating a field requires that the field exists on the
record. A record cannot be updated with a new field, but it can be extended
with a new field, as we shall see later.
Record Extension
We can add a new field to an existing record as follows:
let p1 = { x = 1, y = 2 };
let p2 = { +z = 3 | p1 };
p1.x + p1.y + p2.z
Here the expression { +z = 3 | p1 }
extends the record p1
with a new field
z
such that the result has three fields: x
, y
, and z
all of which are of
Int32
type.
Record Restriction
Similarly to record extension, we can also remove a field from a record:
let p1 = { x = 1, y = 2 };
let p2 = { -y | p1 };
Here the record p2
has the same fields as p1
except that the y
field has
been removed.
Row Polymorphism: Open and Closed Records
A function may specify that it requires a record with two fields:
def f(r: {x = Int32, y = Int32}): Int32 = r.x + r.y
We can call this function with the records { x = 1, y = 2 }
and { y = 2, x = 1 }
, but we cannot call it with the record { x = 1, y = 2, z = 3 }
since
the signature of f
demands a record with exactly two fields: x
and y
. We
say that the record r
is closed.
We can lift this restriction by using row polymorphism:
def g(r: {x = Int32, y = Int32 | s}): Int32 = r.x + r.y
We can call this function with any record as long as it has x
and y
fields
which are of type Int32
. We say that the record type of r
is open.
Named Parameters with Records
When a function has multiple parameters that share the same type, it is easy to
get confused about the right argument order. For example, what does
String.contains("Hello","Hello World")
return? What does
String.contains("Hello World", "Hello")
return?
A common solution to this problem is to use named parameters. Flix supports a form of named parameters building on records. For example, we can write a function translate to translate from one language to another as follows:
def translate(from: {from = Language}, to: {to = Language}, text: String): String = ???
We can call this function as follows:
translate({from = English}, {to = French}, "Where is the library?")
Since such verbosity gets tedious, we can also use the syntactic sugar:
translate(from = English, to = French, "Where is the library?")
which is equivalent to the above.