elves / elvish

Powerful scripting language & versatile interactive shell

Home Page:https://elv.sh/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`math:trunc` should output an exact num, not an inexact num

krader1961 opened this issue · comments

A question on the IM channel about how to round a float to the nearest int caused me to notice this behavior:

elvish> math:round 1.7
▶ (num 2.0)

That behavior made sense when float was the only number type, but it should output an exact num under the new number type hierarchy.

This behavior is intentional.

The short answer is that Elvish's mental model of inexact/exact number is inherited from R6RS, and it's really about exactness, a dimension entirely separate from integer-ness.

The long answer is:

In R6RS's mental model, inexact numbers are considered approximations of exact numbers, with an implicit error margin. For example, the inexact number 1.0 could be approximation of any exact number in the range [9/10, 11/10], or [4/5, 6/5], depending on the error margin.

In the example you've given - math:round 1.7, the inexact number 1.7 could be, say, an approximation of an exact number in the interval [13/10, 21/10]. Not all numbers in this interval round to 2, so it doesn't make sense for math:round 1.7 to return an exact 2.

I'm perplexed by the R6RS behavior, but then I didn't get past first year Calculus in my math education. An operation such as math:round or math:trunc is defined to produce an exact number if the original number isn't a special case such as infinity or NaN. There are no error margins in the result. The documentation for both commands says "This function is exactness-preserving." (and "function" should probably be changed to "command" but that's a different issue).

The R6RS documentation says:

A number object is exact if it is the value of an exact numerical literal or was derived from exact number objects using only exact operations. Exact number objects correspond to mathematical numbers in the obvious way.

Conversely, a number object is inexact if it is the value of an inexact numerical literal, or was derived from inexact number objects, or was derived using inexact operations. Thus inexactness is contagious.

However, section 11.7.4.3 Arithmetic operations says:

(floor x)‌‌procedure 
(ceiling x)‌‌procedure 
(truncate x)‌‌procedure 
(round x)‌‌procedure 
These procedures return inexact integer objects for inexact arguments that are not infinities or NaNs, and exact integer objects for exact rational arguments. For such arguments, floor returns the largest integer object not larger than x. The ceiling procedure returns the smallest integer object not smaller than x. The truncate procedure returns the integer object closest to x whose absolute value is not larger than the absolute value of x. The round procedure returns the closest integer object to x, rounding to even when x represents a number halfway between two integers.

Note:‌ If the argument to one of these procedures is inexact, then the result is also inexact. If an exact value is needed, the result should be passed to the exact procedure.

Note the final "note" in the above R6RS documentation. Perhaps the Elvish documentation should include some text about applying exact-num as in the following example:

elvish> math:round 1.7
▶ (num 2.0)
elvish> exact-num (math:round 1.7)
▶ (num 2)
elvish> > math:round inf
▶ (num +Inf)
elvish> > exact-num (math:round inf)
Exception: bad value: argument here must be finite float, but is +Inf
[tty 11]:1:1: exact-num (math:round inf)

Although, I'd prefer that math:round (and friends) implicitly applied that exact-num operation. In a language like Elvish it seems to me preferable that a user who inadvertently does math:round inf gets an exception rather than a num inf since 99.99% of the time they are going to expect an exact integer.