Add pure and immutable keywords to ensure code has no unintended side-effects
See original GitHub issueThe aim of this proposal is to add some immutability and pure checking into the typescript compiler. The proposal adds two new keywords that would give developers a means to define functions that are pure - meaning that the function has no-side effects, and define variables that are immutable - meaning that they can never be used in an impure context.
Pure
The pure
keyword is used to define a function with no side-effects (it is allowed in the same places as the async
keyword).
pure function x(arg) {
return arg
}
The keyword should be not be emitted into compiled javascript code.
A pure function:
- May not call an function not tagged as pure in the context of arguments, or this.
this.nonPure()
,arg.nonPure()
,nonPure(arg)
, andnonPure(this)
are all disallowed.
- May make non-pure calls on instance variables, as the non-pureness applies to instance variables, so the function is technically still side-effect free:
pure function x() {
const arr = []
arr.push(1)
return arr
}
-
- this one i’m not entirely sure about, but it seems like it would be very hard to build pure functions without it.
-
- languages like elm get around this by having a push function which returns a new array.
- Modify the input variables
arg.x = 1
is disallowed within the function body.
- Modify variables on
this
this.y = 1
is disallowed within the function body.
- Must return a value (otherwise there’s no point to the function!).
Immutable
Similarly a variable may be tagged as immutable:
immutable x = []
(maybe keyword should be shortened to immut
, or the pure
keyword could be reused for consistency?).
This keyword is replaced with const
in emitted code.
An immutable variable:
- Is treated as if it were
const
(i.e. its reference may not be reassigned). - May not have any impure instance methods called on it.
x.nonPure()
is disallowed.
- May not be passed as an argument to impure functions.
nonPure(x)
is disallowed.
- May not be reassigned to a variable reference that is also not marked as pure.
const y = x;
is disallowed.const y = { z: x }
is disallowed.
- May not have instance properties set on it.
x.foo = 1
is disallowed.
- May be passed to pure functions.
pureFn(x)
is allowed.
- May have pure instance methods called on it.
x.toString()
is allowed.
- For arrays, its type is strictly set at definition time, meaning that element-wise type checks will pass (fixes: https://github.com/Microsoft/TypeScript/issues/16389)
- i.e. the following code will now pass
pure function fn(arg : ('a' | 'b')[]) { }
immutable x = ['a', 'b']
fn(x)
With objects/interfaces
The keyword(s) should also be allowed in object
(and by extension interface
) definitions:
const obj1 = {
// immutable and non-pure
immutable fn1: function () { },
// mutable and pure
fn2: pure function () { },
// immutable and pure
immutable fn3: pure function () { },
// immutable and non-pure
immutable fn3() { },
// immutable and pure
pure fn3() { },
}
interface IFace {
pure toString() : string // pure must always return a value
immutable prop : number
immutable pure frozenFn() : boolean
}
With existing typings
With this proposal, the base javascript typings could be updated to support it. I.e. the array interface would become:
interface Array<T> {
pure toString(): string;
pure toLocaleString(): string;
pure concat(...items: T[][]): T[];
pure concat(...items: (T | T[])[]): T[];
pure join(separator?: string): string;
pure indexOf(searchElement: T, fromIndex?: number): number;
pure lastIndexOf(searchElement: T, fromIndex?: number): number;
pure every(callbackfn: (this: void, value: T, index: number, array: T[]) => boolean): boolean;
pure every(callbackfn: (this: void, value: T, index: number, array: T[]) => boolean, thisArg: undefined): boolean;
pure every<Z>(callbackfn: (this: Z, value: T, index: number, array: T[]) => boolean, thisArg: Z): boolean;
pure some(callbackfn: (this: void, value: T, index: number, array: T[]) => boolean): boolean;
pure some(callbackfn: (this: void, value: T, index: number, array: T[]) => boolean, thisArg: undefined): boolean;
pure some<Z>(callbackfn: (this: Z, value: T, index: number, array: T[]) => boolean, thisArg: Z): boolean;
pure forEach(callbackfn: (this: void, value: T, index: number, array: T[]) => void): void;
pure forEach(callbackfn: (this: void, value: T, index: number, array: T[]) => void, thisArg: undefined): void;
pure forEach<Z>(callbackfn: (this: Z, value: T, index: number, array: T[]) => void, thisArg: Z): void;
pure map<U>(this: [T, T, T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U): [U, U, U, U, U];
pure map<U>(this: [T, T, T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U, thisArg: undefined): [U, U, U, U, U];
pure map<Z, U>(this: [T, T, T, T, T], callbackfn: (this: Z, value: T, index: number, array: T[]) => U, thisArg: Z): [U, U, U, U, U];
pure map<U>(this: [T, T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U): [U, U, U, U];
pure map<U>(this: [T, T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U, thisArg: undefined): [U, U, U, U];
pure map<Z, U>(this: [T, T, T, T], callbackfn: (this: Z, value: T, index: number, array: T[]) => U, thisArg: Z): [U, U, U, U];
pure map<U>(this: [T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U): [U, U, U];
pure map<U>(this: [T, T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U, thisArg: undefined): [U, U, U];
pure map<Z, U>(this: [T, T, T], callbackfn: (this: Z, value: T, index: number, array: T[]) => U, thisArg: Z): [U, U, U];
pure map<U>(this: [T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U): [U, U];
pure map<U>(this: [T, T], callbackfn: (this: void, value: T, index: number, array: T[]) => U, thisArg: undefined): [U, U];
pure map<Z, U>(this: [T, T], callbackfn: (this: Z, value: T, index: number, array: T[]) => U, thisArg: Z): [U, U];
pure map<U>(callbackfn: (this: void, value: T, index: number, array: T[]) => U): U[];
pure map<U>(callbackfn: (this: void, value: T, index: number, array: T[]) => U, thisArg: undefined): U[];
pure map<Z, U>(callbackfn: (this: Z, value: T, index: number, array: T[]) => U, thisArg: Z): U[];
pure filter(callbackfn: (this: void, value: T, index: number, array: T[]) => any): T[];
pure filter(callbackfn: (this: void, value: T, index: number, array: T[]) => any, thisArg: undefined): T[];
pure filter<Z>(callbackfn: (this: Z, value: T, index: number, array: T[]) => any, thisArg: Z): T[];
pure reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T): T;
pure reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;
pure reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T): T;
pure reduceRight<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U;
push(...items: T[]): number;
pop(): T | undefined;
reverse(): T[];
shift(): T | undefined;
slice(start?: number, end?: number): T[];
sort(compareFn?: (a: T, b: T) => number): this;
splice(start: number, deleteCount?: number): T[];
splice(start: number, deleteCount: number, ...items: T[]): T[];
unshift(...items: T[]): number;
[n: number]: T;
}
Issue Analytics
- State:
- Created 6 years ago
- Reactions:52
- Comments:19 (9 by maintainers)
Top GitHub Comments
An example of an implementation of a “purity” system is the contexts and capabilities system built into Hack: https://docs.hhvm.com/hack/contexts-and-capabilities/available-contexts-and-capabilities This system is more flexible than just my “disallow impure actions” proposal.
In the hack system you can opt a function into this system by adding
[]
before the return annotationThis declares the function as having no capabilities - it has to be completely pure. Within the brackets you can include the name of a “context”. Contexts declare what “capabilities” a function can have (for example - can it write properties, or can it do IO operations).
A function with capabilities can only call other functions with the same capabilities. This system is leveraged heavily at Facebook to ensure that codegen pipelines are consistent and stable.
How is it a duplicate of #13721? That issue is entirely about adding a comment to emitted code so that uglyifyjs can optimise it away? This is about adding features to the typescript language, pre compilation. In fact it emits no different code.
Similarly for #6532, that issue only pertains to reference assignment. You can still mess with the underlying object, which is the entire problem that I am attempting to solve here.
The problem is that there is no way to do compile time checks to ensure that an object has not been modified (which causes issues such as #16389 where the compiler cannot easily be sure that an object has been unmodified).