Repository: TypeScript
Issue: "Isolated" annotation for functions that only act on inputs (distinct from 'pure')
Labels: Suggestion,Awaiting More Feedback

<!-- 🚨 STOP 🚨 𝗦𝗧𝗢𝗣 🚨 𝑺𝑻𝑶𝑷 🚨

Half of all issues filed here are duplicates, answered in the FAQ, or not appropriate for the bug tracker.

Please help us by doing the following steps before logging an issue:
  * Search: https://github.com/Microsoft/TypeScript/search?type=Issues
  * Read the FAQ, especially the "Common Feature Requests" section: https://github.com/Microsoft/TypeScript/wiki/FAQ

-->

## Search Terms

<!-- List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily -->
pure
isolated
functional
testing
functions

## Suggestion

I'd like to be able to assert that a function is `isolated`, which I define as "acts only on arguments passed in as inputs". Isolated functions can be extracted more easily during refactors and can be tested in isolation from the rest of the codebase, opening the door to things like doctests.

A function that applies inherited scope to arguments cannot be `isolated`. If you're using jquery for instance, you'd want to pass `$` in as an argument to your isolated functions even if it's available in the global scope.

**NOTE**: Claiming that a function is `isolated` is not the same thing as claiming that it's `pure`. At first glimpse they seem like synonyms, but I don't see how a `pure` function is possible in javascript given that you can't control the side-effects of invoking methods on arguments, etc. Passing `$` in and then acting on it is by definition *NOT* a pure function, but we can still treat it as isolated! :)

The desired behavior is that if a function is annotated as `isolated` there'll be a compiler error thrown if it tries to access a value that's not one of its arguments.


## Use Cases

### Big Win: Easier Refactoring
An isolated function can be relocated into any source file without worrying about breaking its behavior by changing the scope it's situated in. It carries its own scope with it no matter where it lives. This opens the door too for interesting and novel approaches to the way code inhabits a filesystem. I'm imagining something like the Eve editor, where "files" are abstractions that may not be as helpful moving forward as they were in the past.

### Bigger Win: Testing
If a function doesn't rely on external scope then testing it becomes trivial - every part of the function's behavior can be explored by tweaking input arguments. This not only makes the functions easier to test in the immediate case (simpler mocking, isolation etc) but also opens the door for things like doctests in the future. (see [Elixir's doctests](https://elixir-lang.org/getting-started/mix-otp/docs-tests-and-with.html#doctests) for an example of what I mean)

## Examples

Think of isolation as a less strict purity. Because this is javascript we can't control what happens when we e.g. call a method on an argument to our function. The runtime behavior is unpredictable enough that purity can't be guaranteed without adding significant runtime-breaking constraints.

What we can do, though, is check to see if our function is only acting on inputs. 
```
/*
* @isolated 
*/
function sum(x, y) { return x + y } // this works

/*
* @isolated
*/
function addX(value) { return value + x } // this throws a compiler error
```

### this
Anything explicitly on `this` counts as an argument input, since you can always call/apply/bind to invoke it and pass a `this` value in. This would complicate refactoring if you're using prototypes, since you'd have to grab all references to the function not just the definition, but it still feels viable and useful to me?

This allows us to have the seeming contradiction of `isolated` methods on objects and instances, I _think_. There's probably some complexity here I'm not thinking about?
```
/*
* @isolated 
*/
function updateModel(delta) {
  this.model.patch(delta)
}

// and elsewhere

updateThisModel = updateModel.bind(model)
```

### Existing code
Because this is an opt-in assertion there are no changes to existing codebases. Further, because the goal is to throw a compiler error when isolation is violated there's a happy path for incremental adoption within a codebase. A lot of core behavior should already be fairly isolated, and being able to make that assertion would bring a lot of new stability to the codebase.

```
/*
* @isolated
*/
function myReducer(accumulator, value) { return accumulator.push(value * 2) }
```

### Purity (and its conspicuous absence)
Let's look at the following isolated function, which just does some JQuery shenanigans. We can assert that this function is `isolated` because `$`, `selector` and `value` are all passed in as arguments. But this code isn't `pure` because invoking jQuery like this has side effects.

We still get the benefit of isolation, though, because now we know we can test this function and all we have to do is mock `$`.

```
/*
* @isolated
*/
function setValue($, selector, value) { $(selector).value(value) }
```


## Checklist

My suggestion meets these guidelines:

* [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
* [x] This wouldn't change the runtime behavior of existing JavaScript code
* [x] This could be implemented without emitting different JS based on the types of the expressions
* [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
* [x] This feature would agree with the rest of [TypeScript's Design Goals](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals).