tc39 / proposal-decimal

Built-in exact decimal numbers for JavaScript
http://tc39.es/proposal-decimal/
497 stars 18 forks source link

Interoperation between BigInt and BigDecimal #39

Open littledan opened 4 years ago

littledan commented 4 years ago

Several people have independently raised the idea that BigInt and BigDecimal could transparently work together. For example, Fabrice Bellard wrote:

BigInt compatibility: it would make sense to allow mixing BigInt and BigDecimal in binary operations (the result would be a BigDecimal). On the other hand it is simpler to require identical types for both operands.

While we could allow this mixed operation, I'd prefer to start out with strictness, following the pattern established by BigInt. I want to continue to encourage a programming style of being consistent about numeric types.

In personal communication, Fabrice responded, "I prefer strictness too."

ljharb commented 4 years ago

Coercion that’s lossless isn’t not strict, though - if it can be made such, maximal interoperability seems preferable to me.

littledan commented 4 years ago

Yeah, I see these both as plausible, consistent options, even if I have my own subjective preference to be more limited.

littledan commented 4 years ago

@ljharb and I had a call to discuss decimal, and I can understand the idea for why we'd want BigInt and decimal to interoperate: We could conceive of BigInt and decimal to be part of a new world of numerical types that we incrementally build, that generally meets most user intuitions, following the successful examples of numeric towers in languages like Python and Ruby.

I'm not decided one way or another on this issue, but I appreciate this other frame, as an alternative to the one I opened this thread with.

ljharb commented 4 years ago

(re https://github.com/tc39/proposal-decimal/issues/36#issuecomment-603462789)

My hope for Decimal is that I can never use Number again - ie, that other than converting incoming Numbers to Decimals, and converting Decimals to Numbers at the very last moment before sending them to APIs that require Numbers, I'd prefer to never have to use them again. It's much easier to lint/test/review a codebase that has one Best Way to do a set of things, and much easier to teach JS to newcomers when that's the case. Having 3 number types to teach, explain, and keep in one's head at all times will make the language more complex and harder to understand, teach, and maintain.

This can be achieved either by Decimal subsuming both Number and BigInt; or, by allowing BigInt and Decimal to transparently interop such that the union of Decimal and BigInt has subsumed all the use cases of Number. I'd ideally prefer both - ie, transparent interop, and Decimal subsuming both Number and BigInt, to achieve maximum usability as well as the smoothest migration story from "pre-Decimal number madness" to "post-Decimal elegance".

littledan commented 4 years ago

@ljharb When we spoke about this in a call, IIRC you mentioned Ruby as a success story here for a good numerical tower. In Ruby, my understanding is that no one type subsumes the others, but rather they do interoperate. I'm wondering if you have strong concerns about going ahead with that sort of model.

As I've explained in the README about how this isn't fractions, and why bitwise operators aren't supported, I have a hard time seeing how a single type could subsume everything--many decimal operations don't logically make sense on fractions, and bitwise operators don't logically make sense on decimals--but I could imagine this interoperation vision.

ljharb commented 4 years ago

Interop is totally fine as well; it just seems nicer to me to have a single uber-type.

littledan commented 4 years ago

I don't have a great idea for how to make a single uber-type work. Maybe you or someone else could propose that, if you have ideas for an acceptable design? We could consider it alongside BigDecimal and Decimal128.

Given the constraints that Decimal128 can't represent all BigInts accurately, does this mean that, if we take interop as a goal, then Decimal128 would be ruled out?

ljharb commented 4 years ago

I don't consider Decimal128 a desirable approach regardless, but yes, i'd say so.

Rudxain commented 2 years ago

Even though I like the idea of the elegance and simplicity of only having to deal with 1 numeric type, we can't deny that constraints and categories sometimes help. A good example is var, let, and const: With let, we can be sure the variable is only accesible locally to the current scope and inner scopes, with const we can be sure any assignment will immediately throw an error. If everyone used var because of its flexibility, debugging and developing would sometimes by a nightmare. There's no "one-size-fits-all". An example is that if you only want your values to be integers regardless of what you do, BigInts give you concise int division for free, no need to trunc or floor it. Same goes for other programming langs, if your code benefits from modular arithmetic, you wouldn't want to use floats, because they have "clamping behavior" rather than "wrap-around behavior", instead we use ints modulo something, where something could be a power of 2, a prime number, or maybe even a power of 10, depending on use case

jessealama commented 4 months ago

Chiming in with an update about the state of affairs, as of writing:

We have settled on using Decimal128 as the data model for decimal. BigInt has an in-principle unlimited range but Decimal128 has an in-principle limited range. Still, we handle the case of converting BigInts to decimals and vice versa, throwing an exception when the conversion cannot be done:

new Decimal128(12345n); // works
new Decimal128(BigInt("1" + "0".repeat(10000000))); // throws
new Decimal128(12345n).toBigInt(); // 12345n
new Decimal128("1.234").toBigInt(); // throws
new Decimal128("2.00").toBigInt(); // 2n
jessealama commented 4 months ago

Also, the current approach doesn't forsee a BigInt argument being passed in to arithmetic operations or comparisons. (The constructor, however, does currently allow a BigInt.) Perhaps BigInts should be allowed as arguments, though, with the understanding that the operation may throw because the BigInt cannot be mapped to a Decimal128.