microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.26k stars 12.52k forks source link

add a modifier for pure functions #7770

Open zpdDG4gta8XKpMCd opened 8 years ago

zpdDG4gta8XKpMCd commented 8 years ago

This is what pure means:

  1. no destructive operations on parameters
  2. all parameters must be guaranteed from being changed from the outside (immutable?)
  3. no calls to any other callback/function/method/constructor that doesn't have the pure modifier
  4. no reads from mutable values from a scope the pure function is closed over
  5. no writes to values in the closed scope
RyanCavanaugh commented 8 years ago

We need more information about what you would expect this to do

ivogabe commented 8 years ago

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.

ivogabe commented 8 years ago

I'd then say that only the callback has to be pure. In your example, narrowing could even happen when map is not pure, as it cannot modify the value of x (and if it would, the callback wouldn't be pure at all).

edevine commented 8 years ago

This would be boon to productivity, and would afford developers a practical tool to isolate side effects

const declaration + readonly modifier give us this:

no variables defined in the outside scope that are proven to possibly mutate can be used in a pure function

zpdDG4gta8XKpMCd commented 8 years ago

@edevine, i would say it means

"const declaration + readonly modifier" for all sub-objects all the way down to the last primitives

mhegazy commented 8 years ago

For context we have discussed similar proposals as part of the readonly modifier support. see https://github.com/Microsoft/TypeScript/issues/6614 for more information.

zpdDG4gta8XKpMCd commented 8 years ago

related #8381

tinganho commented 8 years ago

A pure function does still not guarantee that a callback is invoked immediately:

Little bit OT, but I would like if TS can add an immediate modifier also to resolve the above problem:

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.
}
malibuzios commented 8 years ago

@aleksey-bykov

'Pure' function means (wikipedia):

  1. The function always evaluates the same result value given the same argument value(s). The function result value cannot depend on any hidden information or state that may change while program execution proceeds or between different executions of the program, nor can it depend on any external input from I/O devices (usually—see below).
  2. Evaluation of the result does not cause any semantically observable side effect or output, such as mutation of mutable objects or output to I/O devices (usually—see below).

In order to satisfy (1.) this would mean it would also exclude read operations to any captured entity and global scope entity (window, document, etc.). The only remotely possible exceptions here are captured constants with a primitive type like number, string, boolean etc., however even they can technically have properties, so even if property access is prevented on them, they could still be returned from the function, and potentially the caller would receive a different return value between calls. This means the function couldn't basically read or write from/to anything outside of it, including captured variables (or possibly constants) from another pure function, as those are not guaranteed to have the same value at each execution.

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'.

malibuzios commented 8 years ago

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.

malibuzios commented 8 years ago

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 readonly wouldn't be sufficient here:

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 nonmutating modifier here temporarily, just for illustration, perhaps there's a better one for this)

[Edit: Modified the code example to make it a bit clearer] [Edit: Or maybe the whole interface or class should be tagged as nonmutating?]

zpdDG4gta8XKpMCd commented 8 years ago

this is what 3 is about: anything closed over by a pure function has to be immutable, or else being pure should not typecheck On May 1, 2016 5:14 AM, "malibuzios" notifications@github.com wrote:

I can see possible side effects from getters being a problem, so this would mean that there should be some way to detect readonly interface properties that could still have side effects, so using readonly wouldn't be sufficient here:

interface MyInterface { readonly prop: number; } class MyClass implements MyInterface{ get prop(): number { mutateGlobalState(); return 1; } }

nonmutating function imSupposedToHaveNoSideEffects() { let x = new MyClass(); let num = x.prop; }

It needs to be something like:

interface MyInterface { nonmutating readonly prop: number; }

(I'm using the nonmutating modifier here temporarily, just for illustration, perhaps there's a better one for this)

— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/7770#issuecomment-216029209

malibuzios commented 8 years ago

@aleksey-bykov

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 and readonly don't guarantee immutability of properties.

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 type trait for variables and members. I still don't have a great idea on how to model this, especially with anonymous object literals, maybe:

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 doesn't prevent these to be modified:

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 undefined, same for let, so I might open an issue for this. I still need to check if this happens in all cases and what the standard says about this, though, both in strict and non-strict mode]


Anyway, both pure (in the conventional sense) and nonmutating are useful ways to model different kinds of scenarios, so I'm also exploring the 'weaker' nonmutating variation (that although less 'safe' is more useful in practice) as well in the context of a (currently 'hypothetical') programming language I'm thinking about, but it may be interesting to share here, so bear with me:

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 nonmutating wouldn't be an ideally precise name for the modifier, since it can still mutate through arguments).

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 print function for any function that may print something to the display:

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 print:

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..)

malibuzios commented 8 years ago

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 foreign deceleration would be required in order to reference an external entity in the body of the class):

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 foreigns can also possibly be set on construction, but I'm still working on how to do it elegantly.

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 foreign members would essentially mean all of its methods are almost pure, as they cannot have any side effects outside the boundaries of the class. Having no properties - only methods - would mean they are 'truly' pure, in the sense of providing referential transparency.

(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.

malibuzios commented 8 years ago

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 foreign declaration is used:

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 foreign declarations for each little thing they need from the outside context..

Edit: I've decided to experiment with having the foreign declarations scoped just like type declarations and move them as close as possible to the position they are actually used (they essentially only serve to tell the compiler to 'import' something from outside of the 'fenced' namespace and are erased in the resulting compiled code).

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.

malibuzios commented 8 years ago

Proposal draft: the 'reader' modifier

Summary

The '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' functions

A reader function:

  1. Can 'spectate' its outside scope, this includes both constants and variables, which may either be primitives or objects. These are not required to be immutable or have any special declared modifier like readonly or const.
  2. Cannot mutate any variable or object property outside of itself.
  3. Can only call other reader functions or methods.
  4. Can only instantiate reader classes.
  5. Can only call the reader members of an external class instance or interface (this may be implicitly detected, see the 'open questions' section below).

A reader function or class constructor arguments may only include:

  1. Immutable values or objects.
  2. reader functions.
  3. Any class instance or interface, however it will be implicitly reduced only to its reader members.

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 members

A class annotated as a reader:

  1. Can 'spectate' the outside scope, similarly to a reader function.
  2. Cannot mutate any variable or property outside of itself.
  3. Similarly to a reader function, it can internally call reader functions, instantiate reader classes, and use reader members of an interface.

A class method annotated as a reader (which may be enclosed both in a reader or non-reader class), is very similar to a reader function.

  1. Can 'spectate' the outside scope.
  2. Cannot mutate any variable or property outside of itself, including its enclosing class' instance properties.
  3. Has all the other traits of reader functions.

A non-reader method in a reader class:

  1. Can 'spectate' the outside scope.
  2. Can mutate instance members.
  3. Cannot mutate anything outside of its class.

A reader interface member:

  1. Guarantees the same behavior as would be expected from reader function or class method.

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

lastmjs commented 8 years ago

Related https://github.com/Microsoft/TypeScript/issues/3882

ghost commented 7 years ago

Since interface properties, (i.e. not methods) can have getters behind them, and these getters technically may have side effects, should they be required to be marked as readers as well?

I hadn't considered this. https://github.com/jonaskello/tslint-immutable/issues/41

wongjiahau commented 7 years ago

When will this be implemented?

wongjiahau commented 7 years ago

Proposal draft : the pure modifier

I will suggest using the name pure instead of using reader to have more clarification.
For example :

var mutableVar = 1;
const obj = { a: "hi" };
let func = () => { mutableVar = 3 };

pure function doSomething() {
    mutableVar = 2; // Error
    obj.a = "bye"; // Error
    func(); // Error
}

doSomething();

So the metaphor goes like this :

Regarding function call

pure function getMyName(){ return "Spongebob"; }

pure function doSomething() { var x = add(1, 2); // Error var s = getMyName(); // No error }

## Regarding I/O operations
* Since `pure` function should have no side effects, it is also reasonable that a `pure` function should not call any I/O functions. 
* Actually, it is also due to the reason that I/O functions are not possibly marked as `pure`
* For example :  
```ts
pure function sayCheese(){
    console.log("Cheese!") //Error
}

Regarding pure classes

* Classes cannot be marked as pure as this violates the whole purpose of object-oriented paradigm, as classes are meant to store state.

For example, the following is a valid pure class:

pure class Color {
  public readonly red  : number;
  public readonly green: number;
  public readonly blue : number;

  public constructor(red:number, green:number, blue:number) {
    this.red   = red;
    this.green = green;
    this.blue  = blue;
  }

  public pure darker(factor = 0.25): Color {
    return new Color(
      this.red   * factor, 
      this.green * factor,
      this.blue  * factor
    );
  }

}

Regarding pure methods

class Sponge { friend: StarFish; constructor(friend: Patrick){ this.friend = friend; }

pure play(){
    friend.shout(); // Error because shout() is not pure
    friend.jump(); // No error as jump() is pure
}

}

 * Based on the rule above, a function is also allowed to call a pure method of  a class. 

## Final words
If you have a C++ experience, you'll notice that `pure` is actually the [`const` modifier](http://www.geeksforgeeks.org/const-member-functions-c/). For example, a pure function/method in C++ would be declare as such: 
```cpp
class Example {
    private:
        string name;
    public:
        string GetName() const {
            return name;
        }
}

As a good practice, methods/function should be marked as pure whenever possible so that the process of debugging will be as smooth as jelly.

Suggestion

If this pure modifier is implemented, the TypeScript linter TSLint should suggest coder to mark any function that does not have side effect as pure.

Changelog

lastmjs commented 7 years ago

Why clean instead of pure? Pure seems to be the accepted jargon in functional programming for having no side effects.

lastmjs commented 7 years ago

Other than that it's looking nice

wongjiahau commented 7 years ago

@lastmjs Changed to pure already.

Conaclos commented 6 years ago

@wongjiahau Immutable objets (strings, numbers, ...) and then "pure" classes make sense...

ghost commented 6 years ago

@wongjiahau

I love your proposal, but I do have a serious issue with this:

Classes cannot be marked as pure as this violates the whole purpose of object-oriented paradigm, as classes are meant to store state.

It's fairly contentious to say that's the entire point of OO. I mean there are multiple definitions, it's not like there's a set of commandments saying objects are this, or objects are that. More concretely - immutable objects are a very common design pattern. If every field in the class is readonly and every method is pure, it makes sense to me to say that the object is pure.

Dessix commented 6 years ago

@wongjiahau In agreement with @LewisAndrewCampbell, Scala serves as an example, as it has its record types (known as "Case classes"), which are often treated as immutable (and, often, pure). As with most languages' record types, to "change" these objects, you use a special syntactic construct to clone them while altering only the properties you explicitly specify. As for purity opposing OO- Scala is probably one of the more strict OO languages I've seen- it doesn't include static for example, as even the static instance is a class (referred to as a "Companion Object"). Many developers use the language as if purity were enforced- and the language team is in the midst of designing an "Effects System" which allows enforced purity.

wongjiahau commented 6 years ago

@LewisAndrewCampbell @Dessix I had updated the proposal based on your comments.

Strate commented 6 years ago

If TS can detect error in pure function, it can infer state on function, pure or not. So, I think pure modified could be avoided.

zpdDG4gta8XKpMCd commented 6 years ago

@Strate explicit modifiers like pure is what holds the code from falling apart

imagine you chose to deduce them automatically, and in an ideal situation everything compiles... then you broke something that rendered one function impure... and got 4236 cascading errors because of one place that is not pure anymore, now go figure what went wrong, i guarantee you will be looking for a needle in a stack of hay

instead if you chose to go with explicit modifiers the error would be localized within a few files

dead-claudia commented 6 years ago

Is this a dupe of #3882?

wongjiahau commented 6 years ago

@isiahmeadows I would say this is an incarnation of #3882 as this threads had more ideas and activities.

dbartholomae commented 6 years ago

Just came upon this topic because I liked the way that D implements pure functions and would love to have similar abilities in JavaScript. Does anyone know if this topic is still discussed for TypeScript? Or how to best progress to get it on the roadmap?

brundonsmith commented 5 years ago

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.

KSXGitHub commented 5 years ago

What about const keyword in place of pure? pure is a valid identifier in JavaScript while const isn't.

robyoder commented 5 years ago

const already has a meaning in JS and TS, so that would break it. pure is a valid JS identifier, but so are number, string, etc. TS necessarily has to reserve more keywords than JS to do its job.

EduardoRFS commented 5 years ago

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"

JulianLang commented 5 years ago

Any news on this issue? Would love to have this kind of types in TypeScript!

Feirell commented 5 years ago

Are there any news? This is one of those things that could really improve the code quality in bigger projects.

ciriousjoker commented 4 years ago

Any news?

HaleTom commented 4 years ago

Related stack overflow questions:

magnusjt commented 3 years ago

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. To implement it, I would start with a simple but restricted version:

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. Check it as if it is the parent function. To be more specific: find the closest pure parent, and treat that as the scope for purity check.

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. Or said differently, it mutates "this", which can be thought of as an input parameter (like "self" in python). Since the parameter lives in the scope of the pure function, it should be a legal mutation. I guess figuring out the lifetime of "this" is the key here. Also, it might be necessary to somehow indicate that a function ONLY mutates input parameters, but does not have any other side effects.

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).

RebeccaStevens commented 3 years ago

I agree with @magnusjt. A good first step for implementing this would be to introduce a pure keyword that only handles the case of purely functional functions.

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.

kabo commented 2 years ago

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 isNil could be marked as pure, typescript would understand that all is well instead of complaining that toUpper can't take string|undefined.

romain-faust commented 2 years ago

@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())
kabo commented 2 years ago

Thanks @romain-faust , that does indeed work. TIL :) However, had there been a way to mark the function as pure, shouldn't TypeScript be able to work as I expected without special type syntax?

RebeccaStevens commented 2 years ago

@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.

kabo commented 2 years ago

@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?

brundonsmith commented 2 years ago

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 https://github.com/brundonsmith/bagel