Add Num, Int, Frac sections

This commit is contained in:
Richard Feldman 2022-01-07 20:50:10 -05:00
parent 6fd9736886
commit 97cbba2eb6

View File

@ -1114,113 +1114,70 @@ As such, it's very important to design your integer operations not to exceed the
Roc has three fractional types:
* `F32`, a 32-bit [floating-point number](https://en.wikipedia.org/wiki/IEEE_754)
* `F64`, a 32-bit [floating-point number](https://en.wikipedia.org/wiki/IEEE_754)
* `F64`, a 64-bit [floating-point number](https://en.wikipedia.org/wiki/IEEE_754)
* `Dec`, a 128-bit decimal [fixed-point number](https://en.wikipedia.org/wiki/Fixed-point_arithmetic)
All of these are different from integers in that they can represent numbers with fractional components,
These are different from integers in that they can represent numbers with fractional components,
such as 1.5 and -0.123.
## [Dec] is the best default choice for representing base-10 decimal numbers
## like currency, because it is base-10 under the hood. In contrast,
## [F64] and [F32] are base-2 under the hood, which can lead to decimal
## precision loss even when doing addition and subtraction. For example, when
## using [F64], running 0.1 + 0.2 returns 0.3000000000000000444089209850062616169452667236328125,
## whereas when using [Dec], 0.1 + 0.2 returns 0.3.
##
## Under the hood, a [Dec] is an [I128], and operations on it perform
## [base-10 fixed-point arithmetic](https://en.wikipedia.org/wiki/Fixed-point_arithmetic)
## with 18 decimal places of precision.
##
## This means a [Dec] can represent whole numbers up to slightly over 170
## quintillion, along with 18 decimal places. (To be precise, it can store
## numbers betwween `-170_141_183_460_469_231_731.687303715884105728`
## and `170_141_183_460_469_231_731.687303715884105727`.) Why 18
## decimal places? It's the highest number of decimal places where you can still
## convert any [U64] to a [Dec] without losing information.
##
## There are some use cases where [F64] and [F32] can be better choices than [Dec]
## despite their precision issues. For example, in graphical applications they
## can be a better choice for representing coordinates because they take up
## less memory, certain relevant calculations run faster (see performance
## details, below), and decimal precision loss isn't as big a concern when
## dealing with screen coordinates as it is when dealing with currency.
##
## ## Performance
##
## [Dec] typically takes slightly less time than [F64] to perform addition and
## subtraction, but 10-20 times longer to perform multiplication and division.
## [sqrt] and trigonometry are massively slower with [Dec] than with [F64].
Dec : Float [ @Decimal128 ]
`Dec` is the best default choice for representing base-10 decimal numbers
like currency, because it is base-10 under the hood. In contrast,
`F64` and `F32` are base-2 under the hood, which can lead to decimal
precision loss even when doing addition and subtraction. For example, when
using `F64`, running 0.1 + 0.2 returns 0.3000000000000000444089209850062616169452667236328125,
whereas when using `Dec`, 0.1 + 0.2 returns 0.3.
## A fixed-size number with a fractional component.
##
## Roc fractions come in two flavors: fixed-point base-10 and floating-point base-2.
##
## * [Dec] is a 128-bit [fixed-point](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) base-10 number. It's a great default choice, especially when precision is important - for example when representing currency. With [Dec], 0.1 + 0.2 returns 0.3.
## * [F64] and [F32] are [floating-point](https://en.wikipedia.org/wiki/Floating-point_arithmetic) base-2 numbers. They sacrifice precision for lower memory usage and improved performance on some operations. This makes them a good fit for representing graphical coordinates. With [F64], 0.1 + 0.2 returns 0.3000000000000000444089209850062616169452667236328125.
##
## If you don't specify a type, Roc will default to using [Dec] because it's
## the least error-prone overall. For example, suppose you write this:
##
## wasItPrecise = 0.1 + 0.2 == 0.3
##
## The value of `wasItPrecise` here will be `True`, because Roc uses [Dec]
## by default when there are no types specified.
##
## In contrast, suppose we use `f32` or `f64` for one of these numbers:
##
## wasItPrecise = 0.1f64 + 0.2 == 0.3
##
## Here, `wasItPrecise` will be `False` because the entire calculation will have
## been done in a base-2 floating point calculation, which causes noticeable
## precision loss in this case.
##
## The floating-point numbers ([F32] and [F64]) also have three values which
## are not ordinary [finite numbers](https://en.wikipedia.org/wiki/Finite_number).
## They are:
## * ∞ ([infinity](https://en.wikipedia.org/wiki/Infinity))
## * -∞ (negative infinity)
## * *NaN* ([not a number](https://en.wikipedia.org/wiki/NaN))
##
## These values are different from ordinary numbers in that they only occur
## when a floating-point calculation encounters an error. For example:
## * Dividing a positive [F64] by `0.0` returns ∞.
## * Dividing a negative [F64] by `0.0` returns -∞.
## * Dividing a [F64] of `0.0` by `0.0` returns [*NaN*](Num.isNaN).
##
## These rules come from the [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754)
## floating point standard. Because almost all modern processors are built to
## this standard, deviating from these rules has a significant performance
## cost! Since the most common reason to choose [F64] or [F32] over [Dec] is
## access to hardware-accelerated performance, Roc follows these rules exactly.
##
## There's no literal syntax for these error values, but you can check to see if
## you ended up with one of them by using [isNaN], [isFinite], and [isInfinite].
## Whenever a function in this module could return one of these values, that
## possibility is noted in the function's documentation.
##
## ## Performance Notes
##
## On typical modern CPUs, performance is similar between [Dec], [F64], and [F32]
## for addition and subtraction. For example, [F32] and [F64] do addition using
## a single CPU floating-point addition instruction, which typically takes a
## few clock cycles to complete. In contrast, [Dec] does addition using a few
## CPU integer arithmetic instructions, each of which typically takes only one
## clock cycle to complete. Exact numbers will vary by CPU, but they should be
## similar overall.
##
## [Dec] is significantly slower for multiplication and division. It not only
## needs to do more arithmetic instructions than [F32] and [F64] do, but also
## those instructions typically take more clock cycles to complete.
##
## With [Num.sqrt] and trigonometry functions like [Num.cos], there is
## an even bigger performance difference. [F32] and [F64] can do these in a
## single instruction, whereas [Dec] needs entire custom procedures - which use
## loops and conditionals. If you need to do performance-critical trigonometry
## or square roots, either [F64] or [F32] is probably a better choice than the
## usual default choice of [Dec], despite the precision problems they bring.
Float a : Num [ @Fraction a ]
`F32` and `F64` have direct hardware support on common processors today. There is no hardware support
for fixed-point decimals, so under the hood, a `Dec` is an `I128`; operations on it perform
[base-10 fixed-point arithmetic](https://en.wikipedia.org/wiki/Fixed-point_arithmetic)
with 18 decimal places of precision.
This means a `Dec` can represent whole numbers up to slightly over 170
quintillion, along with 18 decimal places. (To be precise, it can store
numbers betwween `-170_141_183_460_469_231_731.687303715884105728`
and `170_141_183_460_469_231_731.687303715884105727`.) Why 18
decimal places? It's the highest number of decimal places where you can still
convert any `U64] to a `Dec` without losing information.
While the fixed-point `Dec` has a fixed range, the floating-point `F32` and `F64` do not.
Instead, outside of a certain range they start to lose precision instead of immediately overflowing
the way integers and `Dec` do. `F64` can represent [between 15 and 17 significant digits](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) before losing precision, whereas `F32` can only represent [between 6 and 9](https://en.wikipedia.org/wiki/Single-precision_floating-point_format#IEEE_754_single-precision_binary_floating-point_format:_binary32).
There are some use cases where `F64` and `F32` can be better choices than `Dec`
despite their precision drawbacks. For example, in graphical applications they
can be a better choice for representing coordinates because they take up less memory,
various relevant calculations run faster, and decimal precision loss isn't as big a concern
when dealing with screen coordinates as it is when dealing with something like currency.
### Num, Int, and Frac
Some operations work on specific numeric types - such as `I64` or `Dec` - but operations support
multiple numeric types. For example, the `Num.abs` function works on any number, since you can
take the [absolute value](https://en.wikipedia.org/wiki/Absolute_value) of integers and fractions alike.
Its type is:
```elm
abs : Num a -> Num a
```
This type says `abs` takes a number and then returns a number of the same type. That's because the
`Num` type is compatible with both integers and fractions.
There's also an `Int` type which is only compatible with integers, and a `Frac` type which is only
compatible with fractions. For example:
```elm
Num.xor : Int a, Int a -> Int a
```
```elm
Num.cos : Frac a -> Frac a
```
When you write a number literal in Roc, it has the type `Num *`. So you could call `Num.xor 1 1`
and also `Num.cos 1` and have them all work as expected; the number literal `1` has the type
`Num *`, which is compatible with the more constrained types `Int` and `Frac`. For the same reason,
you can pass number literals to functions expecting even more constrained types, like `I32` or `F64`.
## Interface modules