Add d128::quantize_with_mode function
It would be useful to be able to be able to set the rounding mode to use in quantize. This pull request adds a new d128::quantize_with_mode function that allows exactly that.
I think it is better if we make the context a bit more ergonomic and then this function won't be necessary. How about a public function with_mode that is a thin wrapper on top of with_context:
let result = d128::with_mode(MODE, | | x.quantize(y));
I actually think we should be very careful about exposing the context at all to end users.
With the exception of get_status and set_status, all the public functions on d128 are currently pure and entirely stateless.
I'm worried about the potential consequences of introducing dynamically scoped state (in the form of a with_mode function, or even a public with_context function) into d128.
This will allow for non-local effects - rather than being able to reason about arithmetic in isolation, the way arithmetic works will depend on the entire state of the call stack. If any callers of a function have called into d128::with_mode, that changes how that function works.
The subtle and hard-to-reason-about effects of introducing dynamically scoped state become even trickier when multiple crates use decimal arithmetic. If both crates rely on different versions of d128, they'll end up using different thread local contexts and everything is ok. On the other hand, if both crates rely on the same version, either crate can now affect how arithmetic in the other works. Changing a dependency of one crate should not affect how another unrelated crate works.
Introducing dynamically scoped state also forces the decimal crate to design around unwind safety. If the callback function to with_mode panics, the rounding mode will never be restored back to its original value. If the panic is caught and further computation is performed on the unwinding thread, decimal arithmetic could behave unexpectedly.
I think there are a lot of difficult, subtle issues that are introduced by exposing dynamically scoped state to end users. These issues will make using the decimal crate correctly harder and affect the ability for users to reason about arithmetic. For the reasons outlined above I'd strongly prefer that the functions that this crate provides remain mostly pure, rather than relying on any mutable state.
I think with_mode gives you a lot of power and with it come some rough edges you need to think about. The nice thing about it is that the user can choose:
let result = d128::with_mode(MODE, | | x.quantize(y));
has no surprises. The following:
let result = d128::with_mode(MODE, | | calculate());
can be super powerful: the same computation can be done with different rounding modes.
If we go this route then we need to figure out how to make the rounding mode stick across library versions and also find a compromise for when code panics. For the latter I think it is ok to restore the context and let the unwinding thread run with the default.