Immutability with Flow

gcanti
2 min readDec 9, 2016

--

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

--

--