value objects

# David Herman (12 years ago)

I had a great conversation today with my colleagues Michael Bebenita and Shu-Yu Guo, and we came up with what I think is a nicely conservative way to add new kinds of numbers to JS without breaking the intuition that JS has only one type of primitive numbers.

tl;dr: Pure, immutable objects that can be optimized to unboxed integers.

Examples:

let x = uint64(17);
let y = uint64(17);
console.log(x === y)            // true
console.log(typeof x)           // "object"
console.log(Object.isValue(x))  // true
console.log(Object.isValue({})) // false

function factorial(n) {
    return n <= 1 ? bignum(1) : n * factorial(n - 1);
}

console.log(factorial(bignum(500)));
// 122013682599111006870123878542304692625357434280319284219241
// 358838584537315388199760549644750220328186301361647714820358
// 416337872207817720048078520515932928547790757193933060377296
// 085908627042917454788242491272634430567017327076946106280231
// 045264421887878946575477714986349436778103764427403382736539
// 747138647787849543848959553753799042324106127132698432774571
// 554630997720278101456108118837370953101635632443298702956389
// 662891165897476957208792692887128178007026517450776841071962
// 439039432253642260523494585012991857150124870696156814162535
// 905669342381300885624924689156412677565448188650659384795177
// 536089400574523894033579847636394490531306232374906644504882
// 466507594673586207463792518420045936969298102226397195259719
// 094521782333175693458150855233282076282002340262690789834245
// 171200620771464097945611612762914595123722991334016955236385
// 094288559201872743379517301458635757082835578015873543276888
// 868012039988238470215146760544540766353598417443048012893831
// 389688163948746965881750450692636533817505547812864000000000
// 000000000000000000000000000000000000000000000000000000000000
// 0000000000000000000000000000000000000000000000000000000

By defining new object types that are immutable, implementations are free to choose among many representations. They can copy the data or share references whenever they want. Since the integers encapsulate only their bits, optimizing implementations can use a 64-bit integer payload. And fully unboxed representations can literally be 64 bit integers (at least, in optimized code where types have been properly inferred). Finally, it's possible to partially evaluate constructors like

uint64(17)

because the constructor is pure. So even if we didn't have custom literal syntax like 17u64 (which, incidentally, we could), we should still be able to get the same performance.

This proposal involves one major change in semantics: the built-in operators have to be overloaded to handle the new types. Rather than go all the way towards user-defined value types, though, I've stuck for now with just a fixed set of built-in ones. It's forward-compatible with user-defined overloading, in case we ever wanted to go all the way.

But the nice thing is, since they are all just objects, there's no shame in having a multiplicity of new types, including:

  • uint8, int8
  • uint16, int16
  • uint32, int32
  • uint64, int64 <-- hey everyone, look at this, this is the important part, right here
  • bignums
  • IEEE754r decimal
  • complex numbers (if anyone cares?)
  • exact rationals (a bridge too far?)

For all of these, the answer to typeof is still just "object". For the most part, they just feel like a nice "batteries included" library.

There's a rough draft of the strawman here:

http://wiki.ecmascript.org/doku.php?id=strawman:value_objects

Comments welcome!

# Axel Rauschmayer (12 years ago)

Beautiful!

  • I would leave typeof as it is. But there should be a proposal for a strategy on how to clean up the whole instanceof/typeof mess. Possibilities: just a set of best practices, a new operator, etc.

  • There should be literals for ints, but I would have at most two kinds – for whatever is considered most common, e.g.: let a = -123i; // int let b = -123l; // long Rationale: There are only a few ways in which integers are normally used, those should be convenient, especially for newbies. In contract, for scientific computing, bignum(500) should not be a problem.

# Herby Vojčík (12 years ago)

David Herman wrote:

I had a great conversation today with my colleagues Michael Bebenita and Shu-Yu Guo, and we came up with what I think is a nicely conservative way to add new kinds of numbers to JS without breaking the intuition that JS has only one type of primitive numbers.

tl;dr: Pure, immutable objects that can be optimized to unboxed integers.

Examples:

 let x = uint64(17);
 let y = uint64(17);
 console.log(x === y)            // true
 console.log(typeof x)           // "object"
 console.log(Object.isValue(x))  // true
 console.log(Object.isValue({})) // false

 function factorial(n) {
     return n<= 1 ? bignum(1) : n * factorial(n - 1);
 }

 console.log(factorial(bignum(500)));
 // 122013682599111006870123878542304692625357434280319284219241
 // 358838584537315388199760549644750220328186301361647714820358
 // 416337872207817720048078520515932928547790757193933060377296
 // 085908627042917454788242491272634430567017327076946106280231
 // 045264421887878946575477714986349436778103764427403382736539
 // 747138647787849543848959553753799042324106127132698432774571
 // 554630997720278101456108118837370953101635632443298702956389
 // 662891165897476957208792692887128178007026517450776841071962
 // 439039432253642260523494585012991857150124870696156814162535
 // 905669342381300885624924689156412677565448188650659384795177
 // 536089400574523894033579847636394490531306232374906644504882
 // 466507594673586207463792518420045936969298102226397195259719
 // 094521782333175693458150855233282076282002340262690789834245
 // 171200620771464097945611612762914595123722991334016955236385
 // 094288559201872743379517301458635757082835578015873543276888
 // 868012039988238470215146760544540766353598417443048012893831
 // 389688163948746965881750450692636533817505547812864000000000
 // 000000000000000000000000000000000000000000000000000000000000
 // 0000000000000000000000000000000000000000000000000000000

By defining new object types that are immutable, implementations are free to choose among many representations. They can copy the data or share references whenever they want. Since the integers encapsulate only their bits, optimizing implementations can use a 64-bit integer payload. And fully unboxed representations can literally be 64 bit integers (at least, in optimized code where types have been properly inferred). Finally, it's possible to partially evaluate constructors like

 uint64(17)

because the constructor is pure. So even if we didn't have custom literal syntax like 17u64 (which, incidentally, we could), we should still be able to get the same performance.

This proposal involves one major change in semantics: the built-in operators have to be overloaded to handle the new types. Rather than go all the way towards user-defined value types, though, I've stuck for now with just a fixed set of built-in ones. It's forward-compatible with user-defined overloading, in case we ever wanted to go all the way.

But the nice thing is, since they are all just objects, there's no shame in having a multiplicity of new types, including:

  • uint8, int8
  • uint16, int16
  • uint32, int32
  • uint64, int64<-- hey everyone, look at this, this is the important part, right here
  • bignums
  • IEEE754r decimal
  • complex numbers (if anyone cares?)
  • exact rationals (a bridge too far?)

For all of these, the answer to typeof is still just "object". For the most part, they just feel like a nice "batteries included" library.

There's a rough draft of the strawman here:

 http://wiki.ecmascript.org/doku.php?id=strawman:value_objects

Comments welcome!

Hm, I feel a bit conservative... I think having single number type has its pluses.

I'd only include bignum and added MAX_INT (and probably LIMIT_INT_POWER2 and LIMIT_INT_POWER10) to Number. If there is a chance that IEEE754r will be good enough to adopt in next five years, I'd just update Number to be 128bit and so MAX_INT gets higher as well as LIMIT_INT_POWER2 (that goes well beyond 64) and LIMIT_INT_POWER10, so they will have their int64 and uint64 there.

I understand lots of apps want "native" int64 as soon as possible, but I do not like "uint64" etc. "types" - they are too technical, there are lots of them, one then must check what conversions are safe ... whatever.

# Andrew Paprocki (12 years ago)

On Tue, Mar 20, 2012 at 4:27 AM, Herby Vojčík <herby at mailbox.sk> wrote:

I understand lots of apps want "native" int64 as soon as possible

I would love to see IEEE754r as soon as possible. Being able to represent certain financial calculations accurately and interface with other parts of a working system that use IEEE754r is ideal. I have found very little need for treating full 64-bit values as Numbers within script. (They usually wind up being unique identifiers or keys that can be passed through as hex/strings.) These are just my observations, though.

# Rick Waldron (12 years ago)

+1

This is reminiscent of existing bignum/bigint js libs, which is nice because anyone that's used that lib will immediately get this and be able to be productive.

See Also: substack/node

# Rick Waldron (12 years ago)

On Tue, Mar 20, 2012 at 10:26 AM, Rick Waldron <waldron.rick at gmail.com>wrote:

+1

This is reminiscent of existing bignum/bigint js libs, which is nice because anyone that's used that lib will immediately get this

sorry, s/that lib/those libs/

# Grant Husbands (12 years ago)

David Herman wrote:

There's a rough draft of the strawman here:

strawman:value_objects

Comments welcome!

Overall, I think it's a good idea. I have to admit that I did at first get excited that custom value types and operator overloading might be in, but I can cope.

This feature could also lead towards types that aid in SIMD optimisation for graphics work, if such is problematic right now. Even if they aren't part of this proposal, they might usefully guide some detail.

Even though it's listed as a to-do, I wondered whether a discussion on overflow might be fruitful. I have no great ideas, myself, but I think that significant nimbers of people would vote for each of wrap-around, saturation, error values and exceptions. I wonder whether there's any easy way of supporting them all. Anyone have any interesting ideas? Of course, implementation costs are an important aspect.

, Grant Husbands.

# Brendan Eich (12 years ago)

We've been over this, see my recent post which linked to older ones. We're not switching the spec's "Number" type (typeof x == "number") to IEEE 754r DFP. The hardware's not there, and the differences will break graphics and other such apps.

We're also not inclined to hardcode just a decimal addition.

Dave's new strawman hardcodes the types people need -- especially Node.js hackers, but also anyone using typed arrays => binary data. And

as he pointed out we can extend ES7 to enable user-defined value objects with extensible operators and literals, and remap the hardcodings Dave proposes as built-ins using the same extension mechanism.

This looks winning to me. The Node and Binary Data use-cases are upon us and they're valid.

# Allen Wirfs-Brock (12 years ago)

Dave,

I don't actually see how this is substantially different, in concept from Sam Ruby's decimal work. The main difference is that you have many more types all of which have to be dealt with (including coercions/promotions, etc.) I don't see why you aren't going to run into exactly the same issues (but more so, because of multiple types) that Sam did. Of course, I don't think Sam's issues were insurmountable, we just couldn't deal with them with in a timely manner. The biggest concern, for me, was that all the issue were handled in a single special case basis. Your proposal is better in that you try to cover most of the common special cases. But that just makes the effort bigger while still leaving fringe cases unsupported (you mention complex and rationales as likely past the cut line).

When we abandoned the decimal work, we said we hoped we could find a generalization that would permit a more open ended set of value types. I don't think we should give up on that before we have really tried. Conversion of operators into method calls and dynamic double dispatch for operand type promotion are well understood techniques. I've noodled around with these concepts for ES and I don't see why they wouldn't work just fine. When I get a chance I can walk you through how it could work.

To me, your key insight seems to be that operations upon well specified immutable (include key methods) object types (I hate to use that term because it is so loaded) can be optimized in a fairly straightforward manner. We probably should all be saying to ourselves, well of course... However, we have a history wanting to keep built-in objects as mutable as possible so we tend to not think about new built-ins as generally being harden in this manner. Perhaps we should.

Of course, well specified, immutable, hardened built-in numeric object of various varieties can be optimized just as easily in the context of an open-ended extensible design.

I agree that this is an area where we should be applying on going attention, but I don't think we need to close the door on an extensible set of numeric types yet.

# Thaddee Tyl (12 years ago)

On Tue, Mar 20, 2012 at 2:20 PM, Andrew Paprocki <andrew at ishiboo.com> wrote:

On Tue, Mar 20, 2012 at 4:27 AM, Herby Vojčík <herby at mailbox.sk> wrote:

I understand lots of apps want "native" int64 as soon as possible

I would love to see IEEE754r as soon as possible. Being able to represent certain financial calculations accurately and interface with other parts of a working system that use IEEE754r is ideal. I have found very little need for treating full 64-bit values as Numbers within script. (They usually wind up being unique identifiers or keys that can be passed through as hex/strings.) These are just my observations, though.

How is IEEE754r able to more accuracy when dealing with financial calculations? I always thought that integers were enough for that purpose, all calculations being as precise as it can get.

# David Herman (12 years ago)

On Mar 20, 2012, at 3:55 PM, Allen Wirfs-Brock wrote:

I don't actually see how this is substantially different, in concept from Sam Ruby's decimal work.

I assume you mean this?

https://mail.mozilla.org/pipermail/es-discuss/2008-September/007466.html

The important difference here is that I'm saying the typeof should be "object".

The main difference is that you have many more types all of which have to be dealt with (including coercions/promotions, etc.)

No, that misses the point. The main difference is that we have a new class of object that can represent value types (rather similar to how regular expressions were introduced into JS), so that we can preserve compatibility with code that assumes there are exactly six typeof types, and so that we can then be free to introduce as many value types as we want.

We cannot add new typeof types willy-nilly. We could conceivably add just one more, say, "value", for value types. But even that would break many programs. I say, let's not add any new typeof types; let's just add a new [[Class]] of object for value types.

I don't see why you aren't going to run into exactly the same issues (but more so, because of multiple types) that Sam did. ... Of course, I don't think Sam's issues were insurmountable, we just couldn't deal with them with in a timely manner.

Sam was only working on decimal. Meanwhile there's demand for 64-bit ints. I'm proposing an approach that can a) be used to provide multiple new value types and b) be grown in the future to provide more value types or even user-extensible ones. I'm going to work on it. If we can't get it done, we can't get it done.

When we abandoned the decimal work, we said we hoped we could find a generalization that would permit a more open ended set of value types. I don't think we should give up on that before we have really tried.

As the strawman says, this approach doesn't give up on that. This proposal is to do the hard work to spec what needs to be specified for the high-priority value types we actually need. From there we can consider generalizing to a user-extensible version. But note that generalized operator overloading will probably not be uncontroversial. Let's solve the important needs first and future-proof for generalization, but let's not stall everything to hold out for perfection.

Conversion of operators into method calls and dynamic double dispatch for operand type promotion are well understood techniques. I've noodled around with these concepts for ES and I don't see why they wouldn't work just fine. When I get a chance I can walk you through how it could work.

Yeah, I know how these would work. My point is that we can get 64-bit ints and bignums into JS, in a backwards-compatible manner, without breaking the conceptual simplicity of only having one type of primitive number in the language, and without having to reach consensus on arbitrary user-extensible operator overloading. And we can do it without closing the door to the more general system later.

To me, your key insight seems to be that operations upon well specified immutable (include key methods) object types (I hate to use that term because it is so loaded) can be optimized in a fairly straightforward manner. We probably should all be saying to ourselves, well of course...

Um, OK... I'm not trying to get this published in a peer-reviewed journal. I'm just trying to solve problems.

I agree that this is an area where we should be applying on going attention, but I don't think we need to close the door on an extensible set of numeric types yet.

As I say, not closing the door. Just solving problems incrementally. The perfect (and non-existent) is the enemy of the good.

# Allen Wirfs-Brock (12 years ago)

As a followup, I've written a strawman[1] that explains how we could specify and implement operator overloading that supports an open-ended set of value types. I look at this as an adjunct to Dave's proposal that suggests that the coercion/promotion rules should be pushed out of the actual operator semantics and placed into an extensible set of methods.

[1] strawman:operator_overloading_with_double_dispatch

# Tab Atkins Jr. (12 years ago)

On Mon, Mar 26, 2012 at 6:23 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

As a followup, I've written a strawman[1] that explains how we could specify and implement operator overloading that supports an open-ended set of value types.  I look at this as an adjunct to Dave's proposal that suggests that the coercion/promotion rules  should be pushed out of the actual operator semantics and placed into an extensible set of methods.

[1] strawman:operator_overloading_with_double_dispatch

Python (and others, like my own slow homebrew version of operator overloading in JS) provide all binary operations as 'foo' and 'rfoo'. If the lvalue overloads 'foo', that's used. Otherwise, if the rvalue overloads 'rfoo', that's used.

This makes it easier to define symmetric operations without having to monkeypatch another class. It's limited, of course, since you have to ensure that the lval doesn't preempt you with its own overload that doesn't understand how to use you. In the future when we have guards (and, presumably, guard-based overloads), this limitation can be avoided as well.

# Brendan Eich (12 years ago)

What Tab said about having "right" versions for the binary operators. Otherwise you can't properly handle number + decimal or number + complex, e.g.

The right and left methods proposed in strawman:value_proxies seems worth a look, as well as the OOPSLA paper linked at the bottom.

# Axel Rauschmayer (12 years ago)

I never liked simulating double dispatch in a single dispatch language and once I came in contact with Common Lisp, I found out why: For some problems, multiple dispatch and generic functions (in CL terminology) are a better fit, conceptually.

The obvious contra argument is that it would add a lot of new complexity, possibly too much of it. On the plus side, it would provide a very elegant solution for operator overloading. If one is to support multiple dispatch at all, it should probably be done in conjunction with type guards, resulting in a Common-Lisp-like solution (where I always liked that it was an elegant compromise between more static languages such as Java and more dynamic languages such as JavaScript).

# John J Barton (12 years ago)

Just a bit related and perhaps of interest: Back when I believed in operator overloading, I wrote a package to allow C++ classes to inherit consistent overloading. The package consisted of templatized base classes that defined say the binary + operator in terms of +=. The base class parameter was the derived class, so the signatures of the operators would be correct, even though they were defined in a base class: en.wikipedia.org/wiki/Barton–Nackman_trick.

The bit about pumping types up into a base class is what interested most folks, but abstracting systematic overloading was the goal. Operator overloading is pretty hard on readers. A meta-type that succinctly describes the nature of the operators helps. It might be a lot easier in JS.

jjb

# Allen Wirfs-Brock (12 years ago)

On Mar 27, 2012, at 12:03 PM, Brendan Eich wrote:

What Tab said about having "right" versions for the binary operators. Otherwise you can't properly handle number + decimal or number + complex, e.g.

yes you can:

Number.prototype. at operatorMinus = function(rval) {return rval. at subFromNumber(this)}; Complex.prototype. at operatorMinus = function(rval) {return rval. at subFromComplex(this)}; Decimal. at operatorMinus = function(rval) {return rval. at subFromDecimal(this)};

Complex.prototype. at subFromNumber = Complex.prototype. at subFromDecimal = function(minuend) {return new Complex(nimuend,0)-this}; Complex.prototype. at subFromComplex = function(minuend) {return new Complex(minuend.real-this.real,minuend-imag-this.imag)}; Decimal.prototype. at subFromNumber = function(minuend) {return Decimal.forNumber(nimuend)-this}; Decimal.prototype. at subFromDecimal = function(minuend) {return primitiveDecimalSub(nimuend,this)}; Decimal.prototype. at subFromComplex = function(minuend) {return minuend-new Complex(this,0}; Number.prototype. at subFromComplex = function(minuend) {return minuend-new Complex(this,0}; Number.prototype. at subFromDecimal = function(minuend) {return minuend-Decimal.forNumber(this)};

I probably should have provided the above definition of Number.prototype. at operatorMinus as part of the srtrawman proposal as that is really the definition you always want to use. The job of all @operatorX methods is simply to pass the type of the lval and the specific operator to the rval. These are both encoded in the method name. To add a new type you just have to define a single method for each operator cross each type that the rval can be combined with. See [2] for a more detailed discussion on building a complex numeric hierarchy using double dispatch.

If we assume this standard definition for Number.prototype. at operatorMinus then

lines 5-7 of my overloadable subtract operator algorithm, which the wiki shows as:

  1. If Type(lval) is Number and Type(rval) is Number, then I Return the result of applying the subtraction operation to lval and rval. See the note below 11.6.3.
  2. Let dispatchable be the result of calling the [[Get]] internal methods of lval with the private name @operatorMinus as argument.
  3. If IsCallable(dispatchable) is true, then I Return the result of calling the [[Call]] internal method of dispatchable with providing lval as the this value and rval as the argument.

could be expand like:

  1. If Type(lval) is Number, then and Type(rval) is Number, then II If Type(rval) is Number, then a Return the result of applying the subtraction operation to lval and rval. See the note below 11.6.3. Else, a Let dispatchable be the result of calling the [[Get]] internal methods of rval with the private name @subFromNumber as argument. b If IsCallable(dispatchable) is true, then
  2. Return the result of calling the [[Call]] internal method of dispatchable with providing rval as the this value and lval as the argument.
  3. Let dispatchable be the result of calling the [[Get]] internal methods of lval with the private name @operatorMinus as argument.
  4. If IsCallable(dispatchable) is true, then I Return the result of calling the [[Call]] internal method of dispatchable with providing lval as the this value and rval as the argument.

This is essentially an inlining of the standard definition of Number.prototype. at operatorMinus given above. It is conceptually similar to right operator methods in the value proxy paper, expect that it is essential to double dispatch that the type of one of the operands is encoded in the second (or in my above restatement inlined) method call. The value proxy strawman apparently depends upon a separate mechanism that apparently does some sort of table lookup in the proxy hander to do type base dispatch. I suspect that, it reduces to a double type based look, that ultimately is logically equivalent to the the selection done using double dispatch. It isn't clear (to me) how it actually works but it certainly appears more opaque and possible less (or harder) extensible.

One of the challenges with double dispatch, is that it is so brain dead simple and efficient (if you have a high perf method dispatch) that sometimes people have a hard time believing that it actually works.

The right and left methods proposed in strawman:value_proxies seems worth a look, as well as the OOPSLA paper linked at the bottom.

[3] is really nice survey of known techniques for do multi-dispatch within a single dispatch language (and isn't everything ultimately built on single dispatch)

/be

Allen Wirfs-Brock wrote:

As a followup, I've written a strawman[1] that explains how we could specify and implement operator overloading that supports an open-ended set of value types. I look at this as an adjunct to Dave's proposal that suggests that the coercion/promotion rules should be pushed out of the actual operator semantics and placed into an extensible set of methods.

[1] strawman:operator_overloading_with_double_dispatch

  [2]  https://wiki.engr.illinois.edu/download/attachments/186744921/double-dispatch.pdf 
  [3]  www.laputan.org/reflection/Foote-Johnson-Noble-ECOOP-2005.pd
# Allen Wirfs-Brock (12 years ago)

On Mar 27, 2012, at 12:18 PM, Axel Rauschmayer wrote:

I never liked simulating double dispatch in a single dispatch language and once I came in contact with Common Lisp, I found out why: For some problems, multiple dispatch and generic functions (in CL terminology) are a better fit, conceptually.

I think a more precise statement of what you dislike is using double dispatch in a single dispatch language to simulate multi-methods. "double dispatch" is the simulation technique rather than what is being simularted.

Interestingly, it is my understanding (but I don't have any references) that at least some Common Lisp implementations internally use double dispatch (actually chained, single dispatch to implement multiple method dispatch

# Axel Rauschmayer (12 years ago)

True. OK, given that it’ll be mostly library implementers (as in “smart people”) who will use this mechanism, nicer surface syntax might not be necessary (and could even be provided by yet another library).