Immutability is one of the building blocks of functional programming. An immutable object is an object whose state cannot be modified after it is created. Immutability allows for referential transparency: there is no difference between a value, and a reference to that value. Immutability is key to functional programming because it matches the goals of minimizing the parts that change, making it easier to reason about those parts.
There’s no specific support for immutability in the current (0.36) version of Flow (think of the readonly keyword in TypeScript), however let’s see how we can leverage some other features in order to get immutability for our data structures.
Objects
Objects are mutable by default
type Person = { name: string };const person: Person = { name: 'Giulio' }person.name = 'Guido' // ok
Starting from version 0.34 we can annotate properties with variance modifiers (see here for a detailed post about property variance). In short, if we annotate a property as covariant (+ symbol), we can read from but we can’t write to
type Person = { +name: string }; // <= covarianceconst person: Person = { name: 'Giulio' }person.name = 'Guido' // error (*)(*) Covariant property `name` incompatible with contravariant use
Classes
Can we use the same trick to make class instances immutable?
class Person {
name: string;
constructor(name: string) {
this.name = name
}
}new Person('Giulio').name = 'Guido' // <= no error
Of course but there’s a problem, Flow complains for the assignment of the property name in the constructor
class Person {
+name: string; // <= covariance
constructor(name: string) {
this.name = name // error (*)
}
}(*) Covariant property `name` incompatible with contravariant use
Here’s a workaround
class Person {
+name: string;
constructor(name: string) {
(this: any).name = name // ok
}
}new Person('Giulio').name = 'Guido' // error (*)(*) Covariant property `name` incompatible with contravariant use
Arrays
Immutable arrays are tricky since we can’t leverage variance annotations. One option is to define a custom ReadonlyArray interface and two converting functions with (almost) zero cost abstraction
type ReadonlyArray<+A> = {
'@@iterator()': Iterator<A>, // <= handy for..of
+length: number,
+[key: number]: void | A, // safe access by index
map<B>(f: (a: A, ...rest: Array<empty>) => B): ReadonlyArray<B>
// other non mutating methods...
};function toReadonlyArray<T>(xs: Array<T>): ReadonlyArray<T> {
return ((xs: any): ReadonlyArray<T>)
}function toArray<T>(xs: ReadonlyArray<T>): Array<T> {
return ((xs: any): Array<T>)
}
Usage
const xs = toReadonlyArray([1, 2, 3])xs[0] = 4 // error (*)(*) Covariant computed property incompatible with contravariant use
Alas Flow will complain about each individual usage of this interface when an array is expected
declare function f(xs: Array<any>): void;f(xs) // error (*)(*) This type is incompatible with the expected param type of array type
Ideally we should just have covariant interfaces, there’s an open feature request here and here.
EDIT: There’s $ReadOnlyArray<T> in version 0.38