From 97cbba2eb6873900da7e367eb579f1d35f399c92 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Jan 2022 20:50:10 -0500 Subject: [PATCH] Add Num, Int, Frac sections --- TUTORIAL.md | 159 +++++++++++++++++++--------------------------------- 1 file changed, 58 insertions(+), 101 deletions(-) diff --git a/TUTORIAL.md b/TUTORIAL.md index af9be56e83..4a7b3a3c61 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -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