When I write JavaScript code, I try to treat objects and arrays as immutable, and I prefer writing pure functions whenever possible. I find this makes code much easier to reason about and avoids lots of subtle bugs. (I learned about the practical benefits of immutable collections from Clojure.)
So it would be nice if Flow could have some sort of immutability checking. With a "const" annotation, the programmer could:
Additionally, a function could have a "pure" annotation to indicate that Flow should check that a function has no side effects. Of course, all functions called by a pure function would need to be checked for purity as well.
Preferably, immutability should apply to nested objects, such as { a : { b : 'c' }}
. (In JavaScript, unfortunately, the Object.freeze() function does not prevent nested objects from being modified.)
Flow looks extremely nice, BTW. Thank you for making it open source.
This is a much requested feature. The main issue is syntax: we need to be careful to design this while keeping future versions of JS in mind. No doubt it would be very useful.
The other issue that I see here is that there is no existing standard way of "altering" an immutable object.
var immutable foo = {bar: 'baz'};
foo.bar = 'biz'; // should return a new foo object with a changed property.
The naive way of doing this would be to create a new object with the needed property and add the existing immutable object to its prototype:
function immutableSet(obj, prop, val) {
var props = {};
props[prop] = {value: val};
return Object.create(obj, props);
}
immutableSet(foo, 'bar', 'biz');
It would be nice if flow was able to de-sugar to something like this.
Facebook's immutable.js offers immutable objects:
var foo1 = Immutable.Map({bar: 'baz'});
var foo2 = foo1.set('bar', 'biz');
foo1.get('bar'); // 'baz'
foo2.get('bar'); // 'biz'
Presumably most of immutable.js's functions could be annotated as pure.
I think the important thing here is the const
annotation. Persistent data structures are a whole different topic.
Idea: a contract of immutability for objects and arrays.
E.g.
function foo <T> (x : Immutable<T>) : number {
x.value = 1; // ERROR: `x` is Immutable<T>
return x.value;
}
the contract is also considered broken if you call any function that doesn't implement the contract:
function bar <T> (x : T) : number {
x.value = 1;
return x.value;
}
function foo <T> (x : Immutable<T>) : number {
return bar(x); // ERROR: `x` is Immutable<T>, bar expects mutable T
}
function qoo <T> (x : Immutable<T>) : number {
return x.value;
}
function doo <T> (x : Immutable<T>) : number {
return qoo(x); // this is OK
}
function too <T> (x : T) : number {
return x.value;
}
function hoo <T> (x : Immutable<T>) : number {
return too(x); // this is OK too, b/c can be statically analysed
}
Thank you for reporting this issue and appreciate your patience. We've notified the core team for an update on this issue. We're looking for a response within the next 30 days or the issue may be closed.
I'd say that if starting of scratch the bias here would be to assume all arguments are read-only, since this is the common and safe case, and require annotations when an argument can be mutated .
function append(a: string[], b: string) {
return a.push(b)
}
==> ERROR: Call to push mutates a read-only argument
function append(a: var string[], b: string) {
return a.push(b)
}
==> Flow checks sucessfully.
In any case having a way to express this would be a clear win for type-safety.
I just ran into a related mistake that ideally flow would be able to catch:
function thingy(x) {
var foo = Immutable.Map({bar: 'baz'});
if (x) foo.merge({foo: 1});
return foo;
}
The mistake here is that the line should read return foo.merge
, because the .merge
function is entirely pure it must be an error to not use the return value in any way.
+1, but -1 to overloading the keyword const
.
I like the proposal from @jussi-kalliokoski to solve the syntax issue. Just add a magic type called Immutable
or NoMutate
or something else. (Immutable is probably not a good idea since Immutable.js exists)
But further, it's very important to add a way to annotate object and class methods as non-mutating.
So that this is no longer a type error:
var map: Immutable.Map<string, number>
var x = map.get('x') && map.get('x') > 10
As of right now, Flow thinks that .get
could mutate the object map
itself, and so the &&
doesn't work.
Instead, the workaround for now is:
var map: Immutable.Map<string, number>
var xVal = map.get('x')
var x = xVal && xVal > 10
However, it's not the end of the world to have to define a few extra variables for type-safety. So I support the team focussing on the general use case before tackling these features.
An other use case for Immutabilty would also be to solve type casting errors
like in this example:
/* @flow */
type Foo = { foo: boolean }
type Bar = Foo & { bar: boolean }
const bars: { [key: string]: Bar } = Object.freeze({})
const foos: { [key: string]: Foo } = bars // generate an error
/* @flow */
type Foo = { foo: boolean }
type Bar = Foo & { bar: boolean }
const bars: { [key: string]: Bar } = {}
const foos: { [key: string]: Foo } = { ... bars } // work around
/* @flow */
type Foo = { foo: boolean }
type Bar = Foo & { bar: boolean }
const bars: Array<Bar> = []
const foos: Array<Foo> = bars.slice() // generate an error
/* @flow */
type Foo = { foo: boolean }
type Bar = Foo & { bar: boolean }
const bars: Array<Bar> = []
const foos: Array<Foo> = [...bars] // workaround
The caveat with a special Immutable
type (or any way to signify immutability) would be that it only works shallowly.
type ImmutableRecordSomething = $Immutable<{ value: ?string, child: { value: ?string } }>;
const record: ImmutableRecordSomething = {
value: 'a',
child: { value: 'b' },
};
record.value = null // Flow error, record is immutable
record.child.value = null // Just fine, only record is protected normally
although that could be solved by doing child: Immutable<{ value: ?string }>
I guess.
Another issue would be passing it to other functions. If they are not set up to use flow, they would invalidate the immutability.
type ImmutableRecord = $Immutable<{ value: ?string }>;
const record: ImmutableRecord = { value: 'a' };
record.value = null // Flow error, record is immutable
// Assuming that lodash' mapValues has no flow types and it can't infer immutability
const newRecord: ImmutableRecord = _.mapValues(record, (x) => x);
record.value = null // No error, record is downgraded to mutable because of mapValues
What about creating something similar to inout parameter in swift
In terms of naming, would like to throw another option into the hat $Frozen<type>
eg
$Frozen<{ value: ?string }>
$Frozen<Array<number>>
@cloudkite that could be confusing, or maybe colliding with a possible type for objects that have actually been Object.freeze()
-ed
@pgherveou As much as I would love to have explicit mutability like this, I think flow should typecheck on working javascript code, and in javascript you are allowed to mutate your parameters :-/
I know that saying '+1' is extremely annoying, but... +1 here :) I would love this feature.
I wanted to somehow use Immutable Record for this, but it seems it's not very flexible no matter how I try to hack it to work with Flow (no way to capture disjoint unions, for example).
I think $Frozen<T>
makes sense as it would just be a way to describe the result of Object.freeze(T)
, which is already supported by Flow.
Then the definition of Object.freeze
could be updated like so:
declare class Object {
static freeze<T>(o: T): $Frozen<T>;
}
Might try to make a PR for this just to get discussion going and also as a good excuse to learn OCaml 馃槃
This feature basically exists now.
When using an object, you can mark all keys as covariant which makes them read-only. Plus, use an exact type to avoid allowing extra keys as well.
type ImmutablePerson = {|
+name: string,
+age: number
|};
As for Arrays, you can use $ReadOnlyArray
instead of Array
.
There are still edge-cases when dealing with other in-built Objects such as Date, Function etc. but that seems like enough of an edge-case.
I think we should close this issue now.
I think it would still be useful to mark an argument as read-only, as opposed to properties.
@glenjamin That can easily be done with a lint rule.
I was thinking about having a type which is normally mutable, but defining a function which uses that type in a read-only way. I don't think that can be done with per-property covariance or a lint rule?
Immutability is now supported!
Please let us know if this does not cover all of your use cases.
If you want to assume that all function arguments are immutable we have an experimental option experimental.const_params
. We want to explore making params constant by default and then disable that assumption if you assign to the function argument.
const a: {
+prop: number
} = {
prop: 42,
};
const b: $ReadOnlyArray<number> = [1, 2, 3];
const c = Object.freeze({
prop: 42,
});
a.prop = 1; // Error
b[0] = 2; // Error
c.prop = 3; // Error
Thanks! This sounds great. Does it enforce the immutability of nested objects and arrays? And is it possible to annotate a function declaration to guarantee that a return value will be constant?
Most helpful comment
Immutability is now supported!
Please let us know if this does not cover all of your use cases.
If you want to assume that all function arguments are immutable we have an experimental option
experimental.const_params
. We want to explore making params constant by default and then disable that assumption if you assign to the function argument.