Type Fixing

The idea of adding types to ES has been around for quite some time, but there's no real interest in overhauling the engines at the level required to make it happen. It's understandable. So, what if there's a way to introduce a form of typing that shouldn't require so much work?

Objective:

Provide a means of constraining the type of a variable.

Minimum Requirements:

  • Where the desired type is primitive, it should only allow values of that primitive type to be assigned.
  • Where the desired type is an object, it should only allow values possessing the same prototype to be assigned.

The Nutshell:

Instead of trying to find a way to implement types cheaply without butchering load times, why not implement type fixing? What people want from typing is the speed increases that come along with a guarantee that the shape of a variable will not change. Right now that's being weakly handled by making things immutable, but that puts more stress on the garbage collector and can sometimes actually cost performance while the GC cleans up.

What I suggest instead is a keyword of some kind that can flag a variable as being unable to alter its type. Here's an example.

/* The "fixed" keyword tells the engine to only allow values of the 
 * initialized type to be assigned to the specified variables. Attempts to
 * do otherwise throws a TypeError.
 */
function percentChanged(fixed oldVal = 0, fixed newVal = 0) {
   return 100 * (newVal - oldVal) / oldVal;
}

//Both val1 & val2 are fixed as numbers
let fixed val1 = ~~(Math.random() * 100), val2 = ~~(Math.random() * 100);
console.log(`val2 differs from val1 by ${percentChanged(val1, val2)}%`);

//Assuming the field exists....
let val3 = document.queryElement("someField").value;

try {
   console.log(`someField differs from val1 by ${percentChanged(val1, val3)}%`);
}
catch(e) {
   console.log(`Guess somefield.value wasn't a number... Should've validated first.`, e);
   console.log(`someField differs from val1 by ${percentChanged(val1, parseFloat(val3))}%`);
}

The fixed keyword requires that an initializer be provided. The type of the initializer becomes the static type of the variable. Objects are a little trickier but still follow similar logic. Since the "type" of an object is usually defined by its prototype, if the initializer value of a fixed variable is an object, all assignments to the variable would require that the new value is an object and that the prototype of the initializer be in the prototype chain of of the new value.

It should also be possible to type fix member properties of an object.

let a = {
   fixed alpha = 0,
   fixed beta = "",
   fixed gamma = true,
   fixed delta = Symbol(),
   fixed epsilon = {}
}

class b {
   static fixed #alpha = 0;
   fixed #beta = "";
   fixed gamma = true;
   fixed delta = Symbol();
   fixed epsilon = {};
}

Limitations:

The fixed keyword:

  • must throw a TypeError if the determined "type" is null or undefined. This is also the reason why fixed requires an initializer.
  • must not perform any coersion on assignment. The type of the value being assigned must already match the type of the fixed variable or a TypeError will be thrown.

TL;DR:

If we have a way of telling the engine that it doesn't need to worry about the type of a given variable changing, that should give us all the benefit of static typing with very little cost. No new types need to be defined in the engine, and the parsing cost is relatively low. This means very little additional overhead to parse.

What do you think?

1 Like

If this were true, than typescript wouldn't exist, because it offers no speed increases at all once it compiles. What people want from types is "code stability".

Your primary argument for this added syntax seems to be that you want to provide users with the ability to micro-optimize their code a little bit more - it seems to have nothing to do with usability. Is this correct?

This sounds like a hard thing to sell, as such a performance feature would be useless in most code, simply because the micro performance gain would be shadowed by other, more non-performant coding choices. I've written before over here about tons of different ways people can micro-optimize code today. A feature such as this "fixed" keyword should not be used, unless the user is also doing all sorts of other, unreadable tricks to micro-optimize their code, and such micro-optimization should only happen in small sections of code that actually need the performance gains (trying to squeeze every last nano-second out of your program will just cause you to write buggy code).

And, if you're actually writing code that needs to be extremely efficient, then it might be better to encourage developers to choose a language like Rust for the performance-critical portions (and compile it to web-assembly), rather than providing little micro-optimization syntaxes such as "fixed".

If your argument for "fixed" was about usability/readability instead, then that's a whole different story. But, as you mentioned, we already have "const" which arguably does a better job on the usability side of things.

The way I understand it, people want types because it makes the programming so much more clear and easy to understand. However, people argue for the inclusion of types using performance gain as a benefit. The suggestion I've offered provides for that performance gain while subtly allowing for types in a way that shouldn't cause a major upset in the language. In fact, the only type argument I'm aware of that won't be satisfied by a proposal like this is the "self documenting" argument, but I can conceive of tooling that would easily be able to generate JSDoc-like documentation for fixed variables and properties.

The point of this isn't micro-optimization at all, but rather to put forth an option for typing that should be less objectionable to engine developers.

My argument for fixed is also a matter of usability and readability. Even though we have const, it does nothing for variables and properties that must remain mutable. This is where fixed comes in. Even in the case of const, if it references an object, nothing is stopping the properties of the object itself from being mutated to a different type.

In the end where const guarantees the referenced value of the variable, fixed would guarantee the "type" of the variable (and/or its members). From where I sit, const, seal/freeze, and fixed would work in concert to give both the engine and the developer guarantees about what they are working with.

How? I can't see that in your proposal.

let fixed o = { a: 1 };
o = { b: 2 }; // same prototype (Object)
o = x => x + 5; // still has Object in prototype chain

How do you guarantee the type of a member?

let o = { fixed a: 1 } // like so?
someFunc(o);
// what if someFunc sets o.a = 'duh'?

edit: after some thought, I can see how.
You'd make the property non-configurable, put the "type" in descriptor and check it on write

Ah, sorry I mis-understood the basis of your argument.

There's a number of useful type-safe features this doesn't provide, like interfaces, generics, union types, etc.

I'm also not sure if it would be very performant if you're dealing with small functions. e.g. if all you're doing is receiving a parameter and passing it along to another function call, then you're not actually operating on that variable, so no performance gains will be received, but you're still spending time checking if the variable is the correct type. Thus, when dealing with smaller functions, where a single variable may only be used one or two times, it'll probably slow things down, not speed them up.

There's also the problem of, what if you don't want to default-assign a particular variable, but still want it to have type checking? Or what if you want type checking of local variables? To do local variables, under the current proposal, you would have to do something like this (assuming "fixed" is also allowed in destructuring):

const { fixed onlyInt = 0 } = { onlyInt: x + y }

In general, I'm not sure how this is any more user-friendly or more performant than just introducing a type assertion system into the language, similar to what Python did. e.g. you define a function like this - function f(x: number, y: number) { ... } - and if you call it with strings, it'll throw an error.

I'm sure there are a lot. However, the usefulness of such features in a dynamically typed language is speculative at best. For instance, generics is fairly pointless in a dynamically typed language. If you want generics, don't use fixed. Done. Besides, some of those features can actually exist with the help of fixed.

// Interface example
class NumberInterface {
   fixed add(fixed a=0, fixed b=0) { throw TypeError("Not Implemented"); }  // a + b
   fixed sub(fixed a=0, fixed b=0) { throw TypeError("Not Implemented"); }  // a - b
   fixed mul(fixed a=0, fixed b=0) { throw TypeError("Not Implemented"); }  // a * b
   fixed div(fixed a=0, fixed b=1) { throw TypeError("Not Implemented"); }  // a / b
   fixed pow(fixed a=0, fixed b=0) { throw TypeError("Not Implemented"); }  // a ** b
}

On the one hand, you can do this without fixed. However you could replace the functions with non-functions in that case. With fixed, extending this means you must replace each function you use with another function. Forcing this for every function is beyond the scope of fixed, but it does become a potential near target.

From this, a "union type" would be a single class capable of exposing 2 different interfaces for the same data. Again, not a direct feature, but a path to such features becomes visible. For instance what if

class Example {
   fixed somefunc(...args) = 0;
}

became an allowable syntax that desugars to a function body that throws an "unimplemented" error, and requires Example to be extended with somefunc defined in the subclass? That would be the definition of an abstract method, the basis of an interface.

That's precisely why I said that it would impose a little overhead. Since fixed would most likely be used in cases where we currently do need type guarantees and do work to give some assurances, there are definite performance benefits to be had. In simple, passthru cases like you mentioned, I don't think there are many such cases where fixed would be of any benefit. Those cases are not within the set of potential uses I'm considering.

Already thought of that, but I haven't yet thought up a good solution. It can be deferred, much like was done for so many desired things surrounding private fields.

Why all the grief?

const fixed onlyInt = ~~x + ~~y;

At the same time, why used const and fixed together like this at all?

const onlyInt =  ~~x + ~~y;

If it's a primitive, the type is fixed by const.

You're getting something mixed up. The "type" of o didn't change. It's still an Object, so that re-assignment is valid. What changed is the instance value, or if you wish to think of it this way, the subclass of Object in use. The guarantee is that the "type" (i.e. the prototype object and all the members accessible through it) is still available.

That's the idea in general. It'd look something like this:

let o = {};
Object.defineProperty(o, 'a', {
   enumerable: true,
   writable: true,
   fixed value: 1
});
someFunc(o);

OK, those are fair arguments.

I'm just going to point out an alternative solution to providing type assertions. In this thread, I had brought up an idea of using a "from" keyword, to allow arbitrary operations to happen during assignment. I believe @mmkal might be making a separate topic soon to discuss it more fully. This would allow us to do things like the following:

const assertNumb = n => {
  if (typeof n !== 'number') throw new Error('Bad type!')
  return n
}

function addNumbs(x from assertNumb, y from assertNumb) {
  return x + y
}
// is the same as this:
function addNumbs(x_, y_) {
  let x = assertNumb(x_)
  let y = assertNumb(y_)
  return x + y
}

A library of different type assertions could be created, that could be inlined into any declaration position. These assertions could become very rich, e.g. you could assert that a number was even, or that a string matched a regex, etc.

Just an alternative direction I thought I would point out.

It doesn't help with the scenario of making sure a local variable stays the same type, even after assignment. But, IMO, that's not a big deal, because ideally reassignment should be kept to a minimum. I would do zero reassignment if the language provided the facilities to do so, but things like try-catch don't make it easy to avoid reassignment.

It also doesn't help with object members. This would be a place where your proposal has certain strengths over the one I'm proposing.

You seem to be a fan of FP. That's all well and good. However, there are many, many scenarios where it is arguably better and more efficient both in terms of memory and execution to reassign. Truth be told, even when you replace, you're still just reassigning. The only difference is which variable. Ultimately, computers are state machines. Trying to avoid changing state the state in one address by creating a new state at another address (a reassignment of that address) then reassigning the reference of the desired state from the old address to the new one is just doing extra work.

Mind you, that's just my opinion as someone who's probaby been programming for far too long.

You can't guarantee that, either.

let fixed o = { a: 1 };
let evil = { b: 2 };
o = evil; // same prototype, ok
Object.setPrototypeOf(evil, null);
o.toString(); // oops!

I am indeed a fan of FP, (though there are parts of it I don't like as much). I also tend to code mostly from a readability perspective and try and save performance concerns for only a select few locations of a particular project. This means I'm normally not worrying about the potential performance cost of assigning to a new local variable instead of reusing an existing one. I also love immutable data structures, but I know those are much harder to optimize, and can understand it when mutable structures are chosen instead for precisely this reason. Yes, I recognize it can be harder for computers to optimize things when everything is immutable, but most of the time, I'm not in a situation where the performance differences would be noticeable (every once in a while I am in that kind of situation, and in those cases, I need to be more careful and write code that's a little uglier in order to get better performance).

What about null objects?

Technically, typescript doesn't guarantee that either. This produces a runtime error.

class Data {
  f() { return 2 }
}

const x: Data = new Data()
Object.setPrototypeOf(x, null)
console.log(x.f())

Maybe I should have been clearer with my wording. The fixed keyword is not responsible for what's in the prototype, but that the prototype object and the members accessible through it are still available to the referenced object. If someone does something like remove some of the members from the prototype object or its prototype, then obviously those members are no longer accessible through that prototype. So the guarantee still holds.

I covered that under "Limitations" in the original post. The fixed keyword must throw a TypeError if the determined "type" is null or undefined. The reason is because it can create artificial constants, or completely ruin it's own purpose.

//Can't be changed
let fixed unintended = undefined;
//Can be reassigned to literally any object because
//Object.getPrototypeOf(Object.prototype) = null
let fixed bad_idea = null;

I could be persuaded that allowing null types is ok, but at first blush, it seems like a bad idea.

Forget about Typescript. That will fail in Javascript directly. By setting the prototype of x to null you took away access to f(). The function f() lived on the prototype you removed.

Not null, a null object - Object.create(null). You’ve been talking about Object.prototype but those don’t have that.

By “type” i can only assume you mean typeof, or the spec’s Type() operation, neither of which are about the [[Prototype]], so i was confused.

Where fixed is concerned, it's exactly the same.

let fixed nullConst = null;
let fixed nullObject = { _proto_: null };

The first is assigned the type "object" due to the weird quirk of null not being of type "null". The second is assigned the type "null" because the instance object has null as a prototype. The net result is that both are replaceable by any non-primitive since ES only has primitives and objects.

By "type" I mean typeof result for primitives (or however that plays out in the spec) since that's the only discernible type for them, and prototype for objects since an object's inheritance chain is it's classical "type".

A null object being interchangeable for a non-null object seems like a pretty severe flaw. If the prototype being Foo matters, then so too does it matter that it’s null.

A non-null object still has null at the end of its prototype chain. Seems consistent to me.