3.2 Numbers
Pyret numbers are of two kinds: exact numbers, or Exactnums, and rough numbers or Roughnums. Both are real; finite; and written in base ten.
Note that imaginary numbers were implemented in earlier versions of Pyret, but are not currently supported.
Exactnums are arbitrarily precise rational numbers, including integers and rational fractions. For integers whose magnitude is less than (num-expt(2, 53) - 1), Pyret internally uses JavaScript fixnums, in order to optimize basic arithmetic.
Roughnums are numbers that are necessarily or deliberately imprecise. These correspond to the same set of values covered by JavaScript fixnums (a.k.a. doubles), and thus cover a large but limited range (magnitude less than 1.7976931348623157e308).
Operations on Exactnums typically return Exactnums. However, if the operation can yield irrationals, and it is not possible to determine that a particular result is definitely rational, that result is returned as a Roughnum. Thus, trigonometric functions on Exactnums typically yield Roughnum answers, except for well-known edge cases such as the sine or cosine of zero. Fractional powers of rationals are usually Roughnum, except for small roots where it can be ascertained that an exact root is possible.
Operations that are non-casting and with at least one argument that is Roughnum automatically coerce the result to be a Roughnum. This is known as Roughnum contagion.
Exactnums allow the usual comparison predicates. Roughnums do too, with the significant exception that trying to compare Roughnums for equality throws an error. To write an equality function that handles Roughnums, use within, as documented in Bounded Equalities.
An operation whose numerical result is not determinate or finite throws an error, with the message signaling either an overflow or some more specific problem.
3.2.1 Number Annotations
Several specific type annotations are provided for numbers to allow more precise value requirements to be specified.
fun round-distance(d :: NumNonNegative) -> Exactnum: num-round(d) end
3.2.2 Number Literals
Exactnums can be integers, fractions represented with a solidus, or decimals, with an optional exponent. In the following, the numerals on the same line all denote the same Pyret number.
42 +42 -42 22/7 -22/7 2.718281828 +2.718281828 -2.718281828 1/2 0.5 6.022e23 +6.022e23 6.022e+23 +6.022e+23 -6.022e23 -6.022e+23 -6.022e-23
Exactnums are of arbitrary precision.
Roughnums are represented with a leading tilde. You can think of the tilde as representing a person waving his or her hands vaguely.
They are integers, fractions or decimals, with an optional exponent.
~42 ~+42 ~-42 ~2.718281828 ~+2.718281828 ~-2.718281828 ~6.022e23 ~+6.022e23 ~6.022e+23 ~+6.022e+23 ~-6.022e23 ~-6.022e+23 ~-6.022e-23
Roughnums cannot be made arbitrarily precise. The absolute value ranges between 0 and 1.7976931348623157e+308 (JavaScript’s Number.MAX_VALUE) with a granularity of 5e-324 (JavaScript’s Number.MIN_VALUE).
3.2.3 Number Constants
The mathematical constant π, approximated as a Roughnum, or ~3.141592653589793.
3.2.4 Number Functions
If both arguments are Exactnums, returns a Boolean. If either argument is Roughnum, raises an error.
check: num-equal(2, 2) is true num-equal(2, 3) is false num-equal(1/2, 0.5) is true num-equal(1 / 2, 0.5) is true num-equal(1/3, 0.33) is false num-equal(1/3, ~0.33) raises "roughnums cannot be compared for equality" end
Returns the greater of the two arguments.
check: num-max(1, 2) is 2 num-max(2, ~3) is-roughly ~3 num-max(4, ~4) is 4 num-max(~4, 4) is-roughly ~4 num-max(-1.1, 0) is 0 end
Returns the lesser of the two arguments.
check: num-min(1, 2) is 1 num-min(2, ~3) is 2 num-min(4, ~4) is 4 num-min(~4, 4) is-roughly ~4 num-min(-1.1, 0) is -1.1 end
Returns the absolute value of the argument. The result is an Exactnum only if the argument is.
check: num-abs(2) is 2 num-abs(-2.1) is 2.1 num-abs(~2) is-roughly ~2 num-abs(~-2.1) is-roughly ~2.1 end
Returns the sine of the argument (an angle in radians), usually as a Roughnum. If the argument is Exactnum 0, the result is Exactnum 0 too.
check: num-sin(0) is 0 num-sin(1) is%(within-abs(0.01)) 0.84 end
Returns the cosine of the argument (an angle in radians), usually as a Roughnum. If the argument is Exactnum 0, the result is Exactnum 1.
check: num-cos(0) is 1 num-cos(1) is%(within-abs(0.01)) 0.54 end
Returns the tangent of the argument (an angle in radians), usually as a Roughnum. If the argument is Exactnum 0, the result is Exactnum 1.
check: num-tan(0) is 0 num-tan(1) is%(within-abs(0.01)) 1.56 end
Returns the arcsine of the argument as an angle in radians in the range [-π/2, π/2], usually as a Roughnum. If the argument is Exactnum 0, the result is Exactnum 0.
check: num-asin(0) is 0 num-asin(0.84) is%(within-abs(0.01)) 1 end
Returns the arccosine of the argument as an angle in radians in the range [0, π], usually as a Roughnum. However, if the argument is Exactnum 1, the result is Exactnum 0.
check: num-acos(1) is 0 num-acos(0.54) is%(within-abs(0.01)) 1 end
Returns the arctangent of the argument as an angle in radians in the range (-π/2, π/2), usually as a Roughnum. However, if the argument is Exactnum 0, the result is Exactnum 0.
check: num-atan(0) is 0 num-atan(1) is-roughly (3.141592 * 1/4) # 45 degrees = π/4 radians num-atan(-1) is-roughly (-3.141592 * 1/4) # 315 degrees = -π/4 radians num-atan(100000000000) is-roughly (3.141592 / 2) # 90 degrees = π/2 radians num-atan(-100000000000) is-roughly (-3.141592 / 2) # 270 degrees = -π/2 radians end
The num-atan function takes a tangent value and returns a corresponding angle, but it is not clear which angle to return: for example, both num-tan(3.141592 * 1/4) and num-tan(3.141592 * 5/4) have a tangent of ~1. The num-atan2 function produces an angle in radians in the range [0, 2π], where the tangent value is the ratio of the two arguments: the two arguments represent the (signed) height and width of a triangle whose angle is unknown (i.e., their ratio is the "rise over run", defining the tangent of that angle). The return value of num-atan2 chooses which angle to return based on the following table:
If... | dx < 0 | dx > 0 |
dy > 0 | Quadrant II | Quadrant I |
dy < 0 | Quadrant III | Quadrant IV |
check: num-atan2(0, 1) is 0 num-atan2(1, 1) is-roughly (3.141592 * 1/4) # 45 degrees num-atan2(1, -1) is-roughly (3.141592 * 3/4) # 135 degrees num-atan2(-1, -1) is-roughly (3.141592 * 5/4) # 225 degrees num-atan2(-1, 1) is-roughly (3.141592 * 7/4) # 315 degrees num-atan2(1, 0) is-roughly (3.141592 * 1/2) # 90 degrees num-atan2(-1, 0) is-roughly (3.141592 * 3/2) # 270 degrees end
Returns the modulus of the first argument with respect to the second, i.e. the remainder when dividing the first number by the second.
check: num-modulo(5, 2) is 1 num-modulo(-5, 2) is 1 num-modulo(-5, -2) is -1 num-modulo(7, 3) is 1 num-modulo(0, 5) is 0 num-modulo(-7, 3) is 2 end
It is useful for calculating if one number is a multiple of another, by checking for a zero remainder.
fun is-even(n :: Number) -> Boolean: num-modulo(n, 2) == 0 where: is-even(6) is true is-even(3) is false end
Returns the integer part of its argument by cutting off any decimal part. Does not do any rounding.
check: num-truncate(3.14) is 3 num-truncate(-3.14) is -3 num-truncate(~3.14) is-roughly ~3 num-truncate(~-3.14) is-roughly ~-3 end
Returns the square root of the given argument. If the argument is an Exactnum and a perfect square, the result is an Exactnum, otherwise, it is a Roughnum.
check: num-sqrt(4) is 2 num-sqrt(5) is%(within-abs(0.001)) ~2.236 num-sqrt(~4) is%(within-abs(0.001)) ~2 num-sqrt(~5) is%(within-abs(0.001)) ~2.236 num-sqrt(0.04) is 1/5 num-sqrt(-1) raises "negative argument" end
Returns the square of the given argument.
check: num-sqr(4) is 16 num-sqr(5) is 25 num-sqr(-4) is 16 num-sqr(~4) is-roughly ~16 num-sqr(0.04) is 1/625 end
Returns the smallest integer Exactnum greater than or equal to the argument.
check: num-ceiling(4.2) is 5 num-ceiling(-4.2) is -4 end
Returns the largest integer Exactnum less than or equal to the argument.
check: num-floor(4.2) is 4 num-floor(-4.2) is -5 end
Returns the closest integer Exactnum to the argument.
check: num-round(4.2) is 4 num-round(4.8) is 5 num-round(-4.2) is -4 num-round(-4.8) is -5 end
If the argument is midway between integers, returns the integer further away from zero.
check: num-round(3.5) is 4 num-round(2.5) is 3 end
Similar to num-round, except that if the argument is midway between integers, returns the even integer Exactnum.
check: num-round-even(3.5) is 4 num-round-even(2.5) is 2 end
Returns the natural logarithm (ln) of the argument, usually as a Roughnum. If the argument is Exactnum 1, the result is Exactnum 0. If the argument is non-positive, an error is thrown.
check: num-log(1) is 0 num-log(0) raises "non-positive argument" num-log(-1) raises "non-positive argument" num-log(2.718281828) is%(within-abs(0.01)) 1 num-log(10) is%(within-abs(0.1)) 2.3 end
Returns e raised to the argument, usually as a Roughnum. However, if the argument is Exactnum 0, the result is Exactnum 1.
check: num-exp(-1) is%(within-abs(0.0001)) (1 / num-exp(1)) num-exp(0) is 1 num-exp(1) is%(within-abs(0.0001)) 2.718281828 num-exp(3) is%(within-abs(0.0001)) num-expt(2.718281828, 3) num-exp(710) raises "exp: argument too large: 710" end
Returns the first argument raised to the second argument. An error is thrown if the first argument is 0 and the second is negative. If the first argument is Exactnum 0 or 1, or the second argument is Exactnum 0, then the result is an Exactnum even if the other argument is a Roughnum.
check: num-expt(3, 0) is 1 num-expt(1, 3) is 1 num-expt(0, 0) is 1 num-expt(0, 3) is 0 num-expt(0, -3) raises "division by zero" num-expt(2, 3) is 8 num-expt(2, -3) is 1/8 end
Given a number, returns the Roughnum version.
check: num-is-roughnum(num-to-roughnum(3.14)) is true num-is-roughnum(num-to-roughnum(~3.14)) is true end
Returns true if argument is an Exactnum integer.
check: num-is-integer(2) is true num-is-integer(1/2) is false num-is-integer(1.609) is false num-is-integer(~2) is false end
Returns true if argument is an Exactnum rational.
check: num-is-rational(2) is true num-is-rational(1/2) is true num-is-rational(1.609) is true num-is-rational(~2) is false end
check: num-is-roughnum(2) is false num-is-roughnum(1/2) is false num-is-roughnum(1.609) is false num-is-roughnum(~2) is true end
Returns true if argument is greater than zero.
check: num-is-positive(~-2) is false num-is-positive(-2) is false num-is-positive(0) is false num-is-positive(-0) is false num-is-positive(2) is true num-is-positive(~2) is true end
Returns true if argument is less than zero.
check: num-is-negative(~-2) is true num-is-negative(-2) is true num-is-negative(0) is false num-is-negative(-0) is false num-is-negative(2) is false num-is-negative(~2) is false end
check: num-is-non-positive(~-2) is true num-is-non-positive(-2) is true num-is-non-positive(0) is true num-is-non-positive(-0) is true num-is-non-positive(2) is false num-is-non-positive(~2) is false end
Returns true if argument is greater than or equal to zero.
check: num-is-non-negative(~-2) is false num-is-non-negative(-2) is false num-is-non-negative(0) is true num-is-non-negative(-0) is true num-is-non-negative(2) is true num-is-non-negative(~2) is true end
Returns a String representing a literal form of the number.
check: num-to-string(2.5) is "5/2" num-to-string(2) is "2" num-to-string(2/3) is "2/3" num-to-string(~2.718) is "~2.718" num-to-string(~6.022e23) is "~6.022e+23" end
Converts the number to a String, providing digits precision in the output. If digits is positive, provides that many digits to the right of the decimal point (including adding zeroes beyond the actual precision of the number). If digits is negative, rounds that many positions to the left of the decimal, replacing them with zeroes.
Note that num-to-string-digits is only for formatting, and its output’s apparent precision may be unrelated to the actual precision of the input number, which may have been an approximation, or unrepresentable in decimal.
check: num-to-string-digits(2/3, 3) is "0.667" num-to-string-digits(-2/3, 3) is "-0.667" num-to-string-digits(5, 2) is "5.00" num-to-string-digits(5, 0) is "5" num-to-string-digits(555, -2) is "600" end
Returns a predicate that checks if the difference of its two arguments is less than tol.
check: 1 is%(num-within-abs(0.1)) 1 1 is%(num-within-abs(0.1)) ~1 ~3 is%(num-within-abs(0.1)) ~3 ~2 is-not%(num-within-abs(0.1)) ~3 ~2 is%(num-within-abs(1.1)) ~3 ~2 is%(num-within-abs(~1)) ~3 2 is%(num-within-abs(1)) ~3 5 is%(num-within-abs(4)) 3 num-within-abs(-0.1)(1, 1.05) raises "negative tolerance" end
Returns a predicate that checks that its first number argument is no more than the fraction tol off from its second argument.
check: 100000 is%(num-within-rel(0.1)) 95000 100000 is-not%(num-within-rel(0.1)) 85000 end
An alias for num-within-rel, much as within and within-rel are synonyms.
These comparison functions compare both numbers and structures, and are documented in Bounded Equalities.
These comparison functions are like the ones above, but return EqualityResults, and are documented in Total Equality Functions (Avoiding Incomparability Errors).
3.2.5 Random Numbers
Returns a pseudo-random integer from 0 to max - 1.
check: fun between(min, max): lam(v): (v >= min) and (v <= max) end end for each(i from range(0, 100)): block: n = num-random(10) print(n) n satisfies between(0, 10 - 1) end end end
Sets the random seed. Setting the seed to a particular number makes all future uses of random produce the same sequence of numbers. Useful for testing and debugging functions that have random behavior.
check: num-random-seed(0) n = num-random(1000) n2 = num-random(1000) n is-not n2 num-random-seed(0) n3 = num-random(1000) n3 is n n4 = num-random(1000) n4 is n2 end
The random seed is set globally. If it is set in tests in a game or another program that should not run the same way every time, add an identifier you can set as a flag indicating if you are running the code in testing or production.
IS-TESTING = true # change as needed when IS-TESTING: num-random-seed(...) end
3.2.6 Other Number Functions
A few other number functions are useful in limited cases that don’t come up in most programs.
Returns true if the argument is represented directly as a primitive JavaScript number (i.e., JavaScript double).
check: num-is-fixnum(10) is true num-is-fixnum(~10) is false num-is-fixnum(1000000000000000) is true num-is-fixnum(10000000000000000) is false num-is-fixnum(1.5) is false end
Pyret represents Exactnums that are non-integers as tuples, and hence even small rationals such as 1.5 are considered non-fixnum, although they could be represented as JavaScript doubles.
Given a Roughnum, returns an Exactnum number most equal to it. Given an Exactnum num, returns it directly.
It is not good practice to indiscriminately convert Roughnums to Exactnums to make comparison easier. Use within() or is-roughly.
check: num-sqrt(2) is%(within-abs(0.000001)) ~1.4142135623730951 num-exact(num-sqrt(2)) is 1.4142135623730951 num-to-rational(num-sqrt(2)) is 1.4142135623730951 end