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

Mark/infer a function or property as pure/stateless #3882

Closed
dead-claudia opened this issue Jul 15, 2015 · 14 comments
Closed

Mark/infer a function or property as pure/stateless #3882

dead-claudia opened this issue Jul 15, 2015 · 14 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

@dead-claudia
Copy link

I think a way to mark and/or infer a function as having no state would be extremely beneficial for code optimization. If a function incurs no state, there are a variety of optimizations that can be applied to it that otherwise couldn't be done. Also, getters and functions that don't modify state can be removed if their results aren't used. Class methods could do the same thing.

Another area where this could be helpful is that if it's explicitly marked, it would be compile-time checked to be fully immutable, i.e. it cannot indirectly modify state. The checks to infer/assert can be independent of let/const, which would make things easier.

An explicit marker would also be very useful for type definition files, given some libraries like Lodash and React mostly consist of mutable methods.

@danquirk
Copy link
Member

#12 covers readonly-ness which would be required for this. Inferring purity is unlikely to ever happen, if it were easily feasible you'd have seen it added to many other static languages by now. As it stands the TypeScript compiler is not really in the business of type directed optimizations/emit so it's unclear how much value this would really add as far as performance and optimizations. Obviously there is some value in the annotations as documentation and contracts but they'd need to be enforced to be valuable.

@danquirk danquirk 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 Jul 16, 2015
@dead-claudia
Copy link
Author

Although a keyword modifier for enforcing purity in a whole function may work. Maybe something like this?

// Function with side effects
function doSomething(x: number, y: number): void;

imm function add1(x: number, y: number) {
  doSomething(x, y); // Error
  return x + y;
}

// No error.
imm function add2(x: number, y: number) {
  return x + y;
}

// Entire class is designated immutable
imm class Monad<T> {
  constructor(public value: T);
  fmap<U>(f: (value: T | Monad<T>) => U): U;
}

imm function add3(x: Monad<number>, y: Monad<number>) {
  return x.fmap(imm x => y.fmap(imm y => x + y));
}

Maybe also, potentially allowing a block to be declared immutable, which can simplify such code. The block would be evaluated as if it didn't exist (no new block scope introduced), but everything in it would be considered immutable (no setting external variables/properties, no calling stateful methods, see notes).

function calculateAndPrint(bar: number) {
  console.log(doCalculation(bar));
}

imm {
function complexCalculation(x: number, y: number) {
  return x + y;
}

function doCalculation(x: number) {
  return complexCalculation(x, Math.sqrt(x));
}
}

Another thing is that function types should also be able to be required to be immutable, and immutable types considered distinct from mutable ones:

type ImmArrayLike<T> = {imm [key: number]: T};

interface Array<T> {
  imm map<U>(this: ImmArrayLike<T>, f: imm (value: T, index: number, array: T[]) => U): U[];
  map<U>(f: (value: T, index: number, array: T[]) => U): U[];

  imm filter(this: ImmArrayLike<T>, f: imm (value: T, index: number, array: T[]) => any): T[];
  filter(f: (value: T, index: number, array: T[]) => any): T[];

  imm reduce<U>(this: ImmArrayLike<T>, f: imm (acc: U, value: T, index: number, array: T[]) => U): U;
  reduce<U>(f: (acc: U, value: T, index: number, array: T[]) => U): U;

  imm join(this: {imm [key: number]: T, imm toString(): string}, sep: string): string;
  join(sep: string): string;

  forEach(f: (value: T, index: number, array: T[]) => any): void;

  imm every(this: ImmArrayLike<T>, f: imm (value: T, index: number, array: T[]) => any): boolean;
  every(f: (value: T, index: number, array: T[]) => any): boolean;

  imm some(this: ImmArrayLike<T>, f: imm (value: T, index: number, array: T[]) => any): boolean;
  some(f: (value: T, index: number, array: T[]) => any): boolean;

  imm indexOf(this: ImmArrayLike<T>, entry: imm T, start?: number): number;
  indexOf(entry: T, start?: number): number;

  // ES7
  imm includes(this: ImmArrayLike<T>, entry: imm T): number;
  includes(entry: T): number;
}

Note 1: immutability does not guarantee 0 state within a function. It just means the state is limited to only setting variables local to the function. These should count as immutable:

imm function map<T, U>(list: T[], f: imm (value: T) => U): U[] {
  const list: U[] = new Array(list.length); // Array constructor is stateless
  for (let i = 0; i < list.length; i++) list[i] = f(list[i]);
  return list;
}

imm function foo() {
  let bar = 1;
  function inner() {
    bar = 2;
  }
  inner();
  return bar;
}

Note 2: implemented class instance properties are implicitly immutable if they are a primitive (i.e. booleans, numbers, strings, symbols, etc.), but getters, interface members, and ambient declarations must be explicitly so.

I'm definitely open to criticism here, and I have no real attachment to imm as the keyword here, other than it's short.

@kitsonk
Copy link
Contributor

kitsonk commented Jul 16, 2015

imm is confusing at best. What would be wrong with static? Also, what would some of the emits look like?

@dead-claudia dead-claudia changed the title Mark/infer a function as pure/stateless Mark/infer a function or property as pure/stateless Jul 16, 2015
@dead-claudia
Copy link
Author

@kitsonk

  1. That could work as well. I also mentally toyed with const, which IMO would fit with the theme of const variables and const enums. The specific keyword I don't really care about.
  2. The emits wouldn't change, other than calls to immutable functions would not be emitted unless they are assigned to something or passed as an argument to something, with (preferably) a compiler warning for each call. Minifiers can take advantage of removing unused functions.

@mariusschulz
Copy link
Contributor

A pure function is not the same as a static function. Therefore, the static keyword shouldn't be used to denote that a function is pure. A better (and new, of course) keyword would be pure, in my opinion.

@dead-claudia
Copy link
Author

@mariusschulz I agree, which is why I initially thought of imm and const as possible candidates. And I think I also considered pure for a couple seconds as well. Let's just say I'm not the best one at bikeshedding the keyword in question here... ;)

@RyanCavanaugh
Copy link
Member

I'm skeptical of how well this could work out in practice. A common problem with purity/constness enforcement is that it's "infectious" - once you start trying to make part of your codebase respect this flag, you have to convert everything, which will soon run in to third-party libraries that do wacky stuff (e.g. Knockout, where one overload of a function is pure and one isn't).

@danquirk
Copy link
Member

@jbondc there is a reason almost no mainstream languages infer purity, especially those that aren't of the functional heritage with immutability as a default. It is immensely complicated and expensive, and not even desirable for many people. Surely Java, Scala, C#, F# and many others would benefit from this feature as much or more than TypeScript, and some of those languages are far more biased to a pure functional world already. Yet none of them have purity inference (or even explicit purity annotations) despite 10+ years more feature development time than TypeScript has had. There's no shortage of serious academic work in this area and none of it has percolated up into a functioning system in a popular language.

@dead-claudia
Copy link
Author

@danquirk Nit: F# is immutable by default. And Scala doesn't have an explicit default (var vs val), but immutability (val) is preferred.

@dead-claudia
Copy link
Author

@jbondc Nit: C# does have some limited type inference, but it's limited to local variable types. C++ looks to be getting more and more type inference capabilities, including inferred auto lambda argument and return types in C++14, and correctly inferred direct list-initialization (x would be inferred to be of y's type in auto x{y};) in the C++17/C++1z draft.

@danquirk
Copy link
Member

@IMPinball F#'s let bindings are immutable by default but that does not do anything in terms of purity as phrased here. There're no pure functions as far the compiler is concerned, checking/validation around that fact, or optimizations related to purity.

@lll000111
Copy link

Is this included in/superseded by #7770? Because this issue was last commented on over three years ago while #7770 has recent activity, so I'm just curious if maybe that's because the discussion has moved there and this one here is obsolete?

@dead-claudia
Copy link
Author

BTW, I'd like to see this inferred for implementation functions, explicit in type declarations. readonly could swap the default - getters really shouldn't be mutating anything apart from lazy accesses.

Whatever route is chosen, I'd like the ability to override inferred impurity going "this is pure even though it doesn't look like it", in cases like cached computations.

@RyanCavanaugh
Copy link
Member

Tracking at the more popular #7770

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

6 participants