Skip to content

"Isolated" annotation for functions that only act on inputs (distinct from 'pure') #39949

Open
@mbilokonsky

Description

@mbilokonsky

Search Terms

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

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
    This wouldn't change the runtime behavior of existing JavaScript code
    This could be implemented without emitting different JS based on the types of the expressions
    This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
    This feature would agree with the rest of TypeScript's Design Goals.

Activity

MartinJohns

MartinJohns commented on Aug 9, 2020

@MartinJohns
Contributor

This sounds like a duplicate of #17818 / #7770.

orta

orta commented on Aug 10, 2020

@orta
Contributor

I think it's a different enough issue, pure functions have a different meaning here (no side-effects) this issue is about offer a way to declare that this a function does not have access to the outer scopes

mbilokonsky

mbilokonsky commented on Aug 11, 2020

@mbilokonsky
Author

Yeah, I'm explicitly not pursuing function purity - that feels like a really complicated and hard problem for me that may not be tractable at all in a JS runtime.

This is about a compile-time check to ensure that a function's scope is isolated from its environment.

added
Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this feature
SuggestionAn idea for TypeScript
on Aug 20, 2020
mbilokonsky

mbilokonsky commented on Mar 15, 2021

@mbilokonsky
Author

Anyone have any further thoughts about this? Rejecting is fine but I wanna make sure it's clear that this isn't a duplicate of pure functions, isolated speaks directly to the problems with function purity in a JS/TS context.

jsejcksn

jsejcksn commented on Jul 10, 2023

@jsejcksn

If there is a term to describe a category of function that is not pure, but is also not a closure, then perhaps that can be put into the title to help disambiguate.

senyaak

senyaak commented on Dec 21, 2023

@senyaak

Actually it would be great to have such keyword. Currently I work with web workers and this could save a lot of work for me :(
look at this example:
main.ts

const myWorker = new Worker("worker.js");
const data = [1, /* a lot of stuff*/ 9999];

myWorker.onmessage = function(e) {
    console.log('Message received from worker', e);
  }

myWorker.postMessage(`${function(data) { return data.join(',') + 'some stuff'}}`, data);

worker.js

onmessage = function(fn, data) {
  const result = eval(fn)(data);
  postMessage(result);
}

this will work only with isolated function, and since there now way to say that some function is isolated - I have a lot of headache to refactor function I want to use in that way

mbilokonsky

mbilokonsky commented on Jan 25, 2024

@mbilokonsky
Author

Yes, that's a great use case. There are lots of cases especially when doing parallel or "thread"-style programming where having the ability to ensure that there are no implicit scope dependencies would be really helpful!

changed the title [-]"Isolated" annotation for functions that only act on inputs[/-] [+]"Isolated" annotation for functions that only act on inputs (distinct from 'pure')[/+] on Jan 25, 2024
jennings

jennings commented on May 15, 2024

@jennings

I found a use case for this today while working on a React app. I wanted to ensure that an anonymous function defined in a component only operated on its parameters and didn't close over any variables in the outer scope:

function MyComponent() {
  useMutation({
    mutatationFn: /** @isolated */ async (params: Params) => {
      // implementation
    },
  }),
}

I think isolated functions would be similar to static local functions in C#, which "can't capture local variables or instance state".

One pragmatic addition to this feature would be specifying variables that should be closed over. Having no exceptions might be too limiting. For example, utility packages like lodash would be hard to use without some kind of exception:

import { head } from "lodash-es";

/**
 * @isolated
 * @closesOver head
 */
function getFirst<T>(arr: T[]) {
  return head(arr);
}
mbilokonsky

mbilokonsky commented on Sep 7, 2024

@mbilokonsky
Author

My strategy for stuff like lodash would be that you define your actual function implementation in an @isolated function, but then you do myFunction.bind({_}) as the sort of ready-to-use version. Your implementation gets access to this._ and because this is explicitly considered a valid isolated input you're not violating anything.

I like this approach because it means that the same function can be bound to different implementations of specific things, for instance. I dunno I just like Function#bind.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @jennings@orta@mbilokonsky@jsejcksn@MartinJohns

        Issue actions

          "Isolated" annotation for functions that only act on inputs (distinct from 'pure') · Issue #39949 · microsoft/TypeScript