-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add a modifier for pure functions #7770
Comments
We need more information about what you would expect this to do |
A pure function does still not guarantee that a callback is invoked immediately: function wrap(f: () => void) {
return { f };
}
let x = Math.random() > 0.5 ? 'hey' : 1;
let obj: { f: () => void };
if (typeof x === 'number') {
obj = wrap(() => x + 2);
}
x = '';
obj.f(); In my opinion, a better idea would be to narrow constant variables only in callbacks, as that would be sound. |
I'd then say that only the callback has to be pure. In your example, narrowing could even happen when |
This would be boon to productivity, and would afford developers a practical tool to isolate side effects
|
@edevine, i would say it means "const declaration + readonly modifier" for all sub-objects all the way down to the last primitives |
For context we have discussed similar proposals as part of the readonly modifier support. see #6614 for more information. |
related #8381 |
Little bit OT, but I would like if TS can add an declare interface Array<a> {
map<b>(map: immediate (value: a) => b): b[]:
}
let x = Math.random() > 0.5 ? 'hey' : 1;
if (typeof x === 'number') {
[].map(() => x + 2); // no error callback is called immediately
}
function f(f: immediate () => void) {
return { f }; // error callback must be called.
}
function f(f: immediate () => void) {
return fs.readFileAsync(f); // error cannot call immediate callback async.
} |
'Pure' function means (wikipedia):
In order to satisfy (1.) this would mean it would also exclude read operations to any captured entity and global scope entity ( It may be that the analysis is actually easier than the one that's needed for #8353, but I'm not sure.. maybe it's safe to simply say it's 'different'. |
Maybe the intention here wasn't really for 'pure' functions in the conventional sense, but a form of a non-side-effecting function, that could still return different values at each execution but is 'guaranteed' not to silently influence the state (including, say things like I/O). That would be closer to the analysis needed for #8353, but would include more components like modification of properties (which isn't really included there, it is only about reassignments), having an understanding of I/O related operations etc. |
I can see possible side effects from getters being a problem, so this would mean that there should be some way to detect regular interface properties (not methods) that could still have side effects, so using interface MyInterface {
readonly prop: number;
}
class MyClass implements MyInterface {
get prop(): number {
mutateGlobalState();
return 1;
}
}
nonmutating function imSupposedToHaveNoSideEffects(arg: MyInterface) {
let num = arg.prop;
}
imSupposedToHaveNoSideEffects(new MyClass()) It needs to be something like: interface MyInterface {
nonmutating readonly prop: number;
} (I'm using the [Edit: Modified the code example to make it a bit clearer] |
this is what 3 is about: anything closed over by a pure function has to be
|
I'm sorry, I might have misunderstood, the intention wasn't very clear from the way it was described. You are right that other 'pure' functions can be called from 'pure' functions (I didn't mention function calls). But variables, properties or even whole classes and interfaces would need to somehow be 'deeply' immutable as well, otherwise it wouldn't really work. const globalVar = { prop: 1 };
pure function func(): { prop: number } {
return globalVar;
}
func(); // result is { prop: 1 }
globalVar.prop = 2;
func(); // result is { prop: 2 } So what you mean is that there should be a 'deeper' form of immutablity, which also includes properties. This would either require something like a keyword or an immutable globalVar = { prop: 1 };
globalVar.prop = 2; // error Or a type trait: const globalVar = <immutable> { prop: 1 }; // type of globalVar is 'immutable { prop: number }'
globalVar.prop = 2; // error I also considered the fact that although strange, in Javascript even primitives may have properties, and const x: number = 1;
x["secret"] = 123; // no error [I've tested this in both Firefox and Chrome and it doesn't error, however the resulting property value is Anyway, both A further question that needs to be considered is whether a non-mutating function should still be allowed to mutate external state through one of its arguments: nonmutating function func(obj: { prop: number }) {
obj.prop = 2;
}
var globalVar = { prop: 1 };
function example() {
func(globalVar);
} I believe this is may be unavoidable, so the answer would have to be 'yes' (this also means that Trying to detect the passing of captured entities wouldn't really help here: var globalVar = { prop: 1 };
function example() {
let x = { a: globalVar };
func(x.a); // This would be very difficult to reliably detect..
} I was thinking of this in the context of ideas for a hypothetical programming language, that is still imperative and 'impure' but has strong safeguards when it comes to side-effects. Having this 'middle ground', where functions cannot have 'silent' side-effects but could still modify global state through arguments seemed like an interesting compromise. However, this may become uncomfortable to the programmer, say, to have to pass the enclosed function iPrintStuff(printFunction: (message: string) => void, text: string) {
printFunction(text);
}
iPrintStuff(console.log, "Hi"); One way of mitigating this (mostly for this 'hypothetical' language, but perhaps also relevant here) would be using a rather different way to organize how the program interacts with state. Perhaps the general pattern would be to use what I call 'enclosed' classes instead of functions, where the class would receive all the external entities that it would need for its internal operations during construction, but otherwise cannot silently influence external state. enclosed class PrintHelper {
constructor(private printFunction: (message: string) => void) {
}
print(message: string) {
this.printFunction(message);
}
}
let printHelper = new PrintHelper(console.log)
printHelper.print("hi"); This may seem somewhat strange or unneccsary, but it does provide a 'controlled' way to guarantee several properties that may be important from a design perspective, although there are probably more 'elegant' ways to model this, but would require some further syntax. One that I can think of is having a special rule that static constructors can reference captured variables as well as mutating functions like declare var globalVar;
enclosed class PrintHelper {
static private prop;
static private printFunction: (message: string) => void
static constructor() {
// These assignments are only allowed in the constructor:
this.prop = globalVar;
this.printFunction = console.log;
}
static print(message: string) {
this.printFunction(message);
}
}
PrintHelper.print("hi"); (I'm still in an early state of developing this..) |
I think this may be better syntax. Here, the compiler can easily become aware and analyze what external state the class can possibly 'touch' (the var globalVar = 1;
enclosed class PrintHelper {
foreign globalVar; // This just captures 'globalVar' above,
foreign log = console.log; // This provides a local alias to 'console.log',
static updateGlobalVar() {
globalVar = 2; // foreign references don't require 'this'
}
static print(message: string) {
log(message);
}
}
PrintHelper.print("hi"); My intention is that A stronger version of this could also disallow all members of silently reading non-immutable external entities, so they would be closer to real 'pure' functions: Having no (By saying this I do assume here that these methods will only accept immutable arguments, however that restriction can also be weakened, so there's a range of possibilities here). Edit: Conceptually this seems somewhat like a hybrid between an isolated 'module' and a class. Sort of like an 'instantiable module' - when the class is not completely static, I mean. Edit: Reworked the example a bit for a more compact syntax. |
The more I look at it, I start to feel that what I'm really 'looking' for here may be better expressed (at least in TypeScript) as an 'enclosed namespace', rather than a class, which would have no access to non-immutable outside state (including the standard library and DOM) unless an explicit var globalVar = 1;
enclosed namespace Example {
export function updateGlobalVar(value: number) {
foreign globalVar; // This just captures 'globalVar' above
globalVar = value;
}
export function print(message: string) {
foreign log = console.log; // This provides a local alias to 'console.log'
log(message);
}
export function pureFunction(x: number) {
return x + 1;
}
} The main problem here is convenience: how to avoid requiring the programmer to write many Edit: I've decided to experiment with having the Update: I've completely 'rebooted' the whole thing and started from scratch using a different approach, which is conceptually closer to what was originally proposed but concentrates on the weaker, but more useful 'spectator-only' mode rather than trying to achieve referential transparency. However, it will take some time to work out all the details, perhaps up several weeks to get this to something approaching a real 'full' proposal. |
Proposal draft: the 'reader' modifierSummaryThe 'reader' modifier is a way to tell the compiler that a particular function, method, class or interface member does not induce any side-effects outside of its own internal scope. This is not, however, a sufficient condition to characterize the affected entity as 'pure' in the same sense that a 'pure' function would be, as it does not guarantee referential transparency, that is, the property that for a given input, the same output would be returned at all subsequent calls. It can be seen as a 'middle-ground' between the 'extreme' mutability of imperative languages and the extreme 'purity', or, non-mutability trait in some functional languages. The purpose and usefulness of having this is both for programmers, to be able to create better and safer contracts, for themselves and for others, and compilers, to better reason about the code and become more closely aware of the intention of the programmer. 'Reader' functionsA
A
Examples: This wouldn't work (it guarantees 'no side effects') var mutableVar = 1;
const obj = { a: "hi" };
let func = () => { mutableVar = 3 };
reader function doSomething() {
mutableVar = 2; // Error
obj.a = "bye"; // Error
func(); // Error
}
doSomething(); But this would (no special guarantee for referential transparency): var mutableVar = 1;
reader function doSomething(): number {
let x = mutableVar; // this assignment is by value, so this would work
x += 1;
return x;
}
doSomething(); // returns 2;
mutableVar++;
doSomething(); // returns 3; 'Reader' classes, class methods and interface membersA class annotated as a
A class method annotated as a
A non-
A
Examples: interface DOM {
reader getNode(path: string): DOMNode
setNode(path: string, value: DOMNode)
// ...
}
declare var dom: DOM;
declare reader class Dictionary<T> {
reader lookup(key: string): T;
reader clone(): Dictionary<T>;
add(key: string, value: T);
// ...
}
reader function getNodes(paths: string[]): List<DOMNode> {
let result = new Dictionary<DOMNode>(); // It is only possible to instantiate this class
// here because it is a 'reader' class.
for (let path of paths) {
let node = dom.getNode(path); // Despite the fact that DOM is not 'purely' a
// 'reader' interface (as it changes the internal state of
// the browser), 'getNode' is a reader, so it is guranteed
// not to modify any state, including the state of any
// external class that may lie 'beneath' its interface
result.add(path, node); // The 'add' operation is destructive, however, since 'Dictionary'
// is a 'reader' class the effect would be only local to this
// function, so this is allowed.
}
return result;
} Open questions
|
Related #3882 |
I hadn't considered this. |
When will this be implemented? |
Proposal draft : the
|
Why clean instead of pure? Pure seems to be the accepted jargon in functional programming for having no side effects. |
Is this a dupe of #3882? |
@isiahmeadows I would say this is an incarnation of #3882 as this threads had more ideas and activities. |
Just came upon this topic because I liked the way that D implements |
Any movement on this? It fits nicely with TypeScript's design goal of leaving no footprints on runtime code, unlike immutable.js, the current standard. Preventing side-effects statically would be a minor revolution for the JavaScript ecosystem I think. |
What about |
|
Perhaps instead of a fix modifier something that allow us to control what can be done inside a function, it will allow more things like a function that can run only some small set of functions. Or at least some "official statement" would be cool, even if is "if someone is willing to contribute feel free" |
Any news on this issue? Would love to have this kind of types in TypeScript! |
Are there any news? This is one of those things that could really improve the code quality in bigger projects. |
Any news? |
Related stack overflow questions: |
This is the one thing I really want to see next from typescript. Many good points already in this thread, but I have a few more to add.
The implementation described above is limited, but it's a good start imo. Some issues, with potential solutions for more advanced support: Defining new functions inside a pure function to operate on local state: pure function test(arr){
const acc = {}
function addToAcc(key, val){
acc[key] = val
}
for(let x of arr){
addToAcc(x.key, x.val) // ERROR: addToAcc is not pure (but that's actually ok)
}
return acc
} A possible solution here is to treat every non-pure function defined inside a pure function as part of the pure function. Calling non-pure functions that only modify variables local to the function: pure function test(){
const a = []
a.push(5) // ERROR: push is not pure (but that's actually ok)
return a
} In this case, "push" has a side effect of modifying the content of variable a. Edit: More cases: Using reduce: pure function sum(arr){
return arr.reduce(pure (a, b) => a + b, 0) // No error. Reduce accepts a pure function. The accumulator cannot possibly be mutated.
} pure function test(arr){
return arr.reduce((a, b) => {
a[b.key] = b.val
return a
}, {}) // Should be ok. Reduce accepts a non-pure function, but operates on a local object. Since the function is defined here, treat it the same as the parent pure function.
} function objSet(a, b){
a[b.key] = b.val
return a
}
pure function test(arr){
return arr.reduce(objSet, {}) // Not ok. In this case we don't know if objSet has other side effects. We'd have to mark the function as side-effect free, i.e. only mutates input params. Of course we could also just make it pure in this case.
} pure function test(arr, obj){
return arr.reduce((a, b) => {
a[b.key] = b.val
return a
}, obj) // Not ok since reduce operates on a non-local object AND the accepted function is non-pure.
} In summary, reduce can be called in a pure function if either:
In other words, a second keyword might be necessary if we wanted to support all these cases (which might not be worth it of course). |
I agree with @magnusjt. A good first step for implementing this would be to introduce a Later, support for non-functional function can be added, maybe in subsequent releases. Doing this should be fine as we'd only be expanding the scope of "pure", not reducing it at all; so no breaking changes. |
This would be great if it could get implemented. My use-case: // this runs fine
const toUpper = (x: string): string => x.toUpperCase()
const fn1 = (x?: string) => x === undefined ? x : toUpper(x)
console.log(fn1('hello1'))
console.log(fn1())
// I should be able to pull out the comparison to a function like this, right? Referential transparency?
const isNil = (x?: string): boolean => x === undefined
const fn2 = (x?: string) => isNil(x) ? x : toUpper(x)
console.log(fn2('hello1'))
console.log(fn2())
// yeah nah, typescript freaks out :( I think if |
@kabo I think that you're looking for type predicates. const isNil = (x?: string): x is undefined => x === undefined
const fn2 = (x?: string) => isNil(x) ? x : toUpper(x)
console.log(fn2('hello1'))
console.log(fn2()) |
Thanks @romain-faust , that does indeed work. TIL :) |
@kabo Probably not. TypeScript would have to analyze what your code actually does in order for this to work. That would be a completely different issue to this one. |
@RebeccaStevens OK, interesting. The biggest thing I'm after would be for TypeScript to be able to do referential transparency with functions that are marked as pure. Is there another GitHub issue for this somewhere please? |
For those interested, I've started working on a TS-like language that can actually enforce function purity because it's a whole new language, with none of the JS baggage |
This is what pure means:
The text was updated successfully, but these errors were encountered: