Skip to content
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

Open
zpdDG4gta8XKpMCd opened this issue Apr 1, 2016 · 48 comments
Open

add a modifier for pure functions #7770

zpdDG4gta8XKpMCd opened this issue Apr 1, 2016 · 48 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Apr 1, 2016

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 RyanCavanaugh added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Apr 1, 2016
@RyanCavanaugh
Copy link
Member

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

@ivogabe
Copy link
Contributor

ivogabe commented Apr 1, 2016

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
Copy link
Contributor

ivogabe commented Apr 1, 2016

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
Copy link

edevine commented Apr 1, 2016

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
Copy link
Author

@edevine, i would say it means

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

@mhegazy
Copy link
Contributor

mhegazy commented Apr 6, 2016

For context we have discussed similar proposals as part of the readonly modifier support. see #6614 for more information.

@zpdDG4gta8XKpMCd
Copy link
Author

related #8381

@tinganho
Copy link
Contributor

tinganho commented May 1, 2016

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
Copy link

malibuzios commented May 1, 2016

@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
Copy link

malibuzios commented May 1, 2016

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
Copy link

malibuzios commented May 1, 2016

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
Copy link
Author

zpdDG4gta8XKpMCd commented May 1, 2016

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" [email protected] 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
#7770 (comment)

@malibuzios
Copy link

malibuzios commented May 1, 2016

@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
Copy link

malibuzios commented May 1, 2016

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
Copy link

malibuzios commented May 1, 2016

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
Copy link

malibuzios commented May 2, 2016

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

  • 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?
  • Can the 'reader' trait be detected implicitly? This would be very important for anonymous functions and object literals (or even classes), in order for them to be easily passed to reader functions or methods.
  • If an instance of a reader class is passed to a reader function, should the function be allowed to call the non-reader methods of that class? (doing that could indirectly mutate external state, but only through that particular class instance)
  • What about a more precise annotation for a member of a non-reader class that can only mutate its own class instance?
  • What about functions declared inside of a reader function, can they be non-readers but still bound by the scope of the enclosing function, just like in classes?
  • What about the returned values of a reader function? should they be 'reduced' to their reader members as well? maybe this would happen implicitly but only inside of a reader scope?
  • Since objects are assigned by reference, how would an object be read from an argument or a closure for the purpose of non-destructive modifications by using a copy? some form of cloning?
  • Any better names than reader? or maybe a different name for the class modifier?
  • Millions of other small or subtle issues.. :)

@lastmjs
Copy link

lastmjs commented Oct 12, 2016

Related #3882

@ghost
Copy link

ghost commented Oct 3, 2017

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.
jonaskello/tslint-immutable#41

@wongjiahau
Copy link

When will this be implemented?

@wongjiahau
Copy link

wongjiahau commented Oct 7, 2017

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 :

  • By default, any function in TypeScript maybe dirty (means they may have side effects)
  • But, if a function is marked with pure, then it shall be pure (no side effects) or else the compiler will throw error

Regarding function call

  • To make it easy, all pure function can only call pure function.
  • Calling a function that is not marked pure should be an error even though the function does not bring any side effects.
  • For example:
function add(x: int, y: int){
    return x + y;
}

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

  • Classes can be mark as pure (as suggested by @Dessix and @LewisAndrewCampbell), however it must satisfied all of the following characteristics:
    • Every field in the class is readonly
    • Every method is pure

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

  • Methods within a class can be marked as pure :
class Example {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    pure sayHello() {        
        this.greeting += "!!!!"; // Error as pure method cannot modify variable outside of own scope
        return this.greeting; // No error
    }
}
  • Constructor cannot be mark as pure.
  • pure methods behave just like pure function, they can only call other function or method that is mark pure and vice versa.
  • For example :
class Starfish {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    
    shout() {
        return "Hey I'm Patrick";
    }
    
    pure jump() {
        return "How can I jump?";
    }      
}

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. For example, a pure function/method in C++ would be declare as such:

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

  • Added description for pure class. (2018-05-15)

@lastmjs
Copy link

lastmjs commented Oct 7, 2017

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

@dead-claudia
Copy link

Is this a dupe of #3882?

@wongjiahau
Copy link

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

@dbartholomae
Copy link

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
Copy link

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
Copy link
Contributor

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

@robyoder
Copy link

robyoder commented Mar 2, 2019

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
Copy link

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
Copy link

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

@Feirell
Copy link

Feirell commented Oct 20, 2019

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

@ciriousjoker
Copy link

Any news?

@HaleTom
Copy link

HaleTom commented Nov 29, 2020

Related stack overflow questions:

@magnusjt
Copy link

magnusjt commented Dec 19, 2020

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:

  • Add a pure keyword that goes in the same place that the async keyword does. A keyword is needed to mark the intention of purity.
    Some functions will be pure by coincidence, and might change in the future. If we don't have a keyword, it will be painful.
  • A pure function can only call other pure functions
  • A pure function cannot access "this", i.e. unlike a previous proposal in this thread (which could not guarantee purity btw)
  • A pure function cannot mutate input params, i.e. treat them as readonly
  • A pure function can only read variables defined outside its scope if they are const. Property access can only be done if readonly.
  • A pure function cannot mutate any variable/property defined outside its scope
  • Magic getters and proxies and whatnot may break purity, as would type coercion of different kinds, but this is a limitation that is acceptable imo.
    It's even nice to have an escape hatch for things like this (e.g. (console.log as FakePureFn)('just debugging temporarily..'))

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:

  • The accepted function is pure
  • The accepted function is defined inline, and found to be pure within the parent AND the accumulator is local to the pure function
  • The accepted function is defined somewhere else but marked as only mutating input parameters / side-effect free (the same as with "this" and array.push mentioned above), AND the accumulator is local to the pure function

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
Copy link

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
Copy link

kabo commented Apr 22, 2022

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
Copy link

romain-faust commented Apr 22, 2022

@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
Copy link

kabo commented Apr 22, 2022

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
Copy link

@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
Copy link

kabo commented Apr 22, 2022

@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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests