Decorum

**Decorum** is a Rust library that provides total ordering, equivalence, hashing, constraints, error handling, and more for IEEE 754 floating-point representations. Decorum does **not** require the `std` nor `alloc` libraries, though they are necessary for some features. [![GitHub](https://img.shields.io/badge/GitHub-olson--sean--k/decorum-8da0cb?logo=github&style=for-the-badge)](https://github.com/olson-sean-k/decorum) [![docs.rs](https://img.shields.io/badge/docs.rs-decorum-66c2a5?logo=rust&style=for-the-badge)](https://docs.rs/decorum) [![crates.io](https://img.shields.io/crates/v/decorum.svg?logo=rust&style=for-the-badge)](https://crates.io/crates/decorum) ## Basic Usage Panic when a `NaN` is encountered: ```rust use decorum::NotNan; let x = NotNan::::assert(0.0); let y = NotNan::::assert(0.0); let z = x / y; // Panics. ``` Hash totally ordered IEEE 754 floating-point representations: ```rust use decorum::real::UnaryRealFunction; use decorum::Real; use std::collections::HashMap; let key = Real::::PI; let mut xs: HashMap<_, _> = [(key, "pi")].into_iter().collect(); ``` Configure the behavior of an IEEE 754 floating-point representation: ```rust pub mod real { use decorum::constraint::IsReal; use decorum::divergence::{AsResult, OrError}; use decorum::proxy::{Constrained, OutputFor}; // A 64-bit floating-point type that must represent a real number and returns // `Result`s from fallible operations. pub type Real = Constrained>>; pub type Result = OutputFor; } use real::Real; pub fn f(x: Real) -> real::Result { ... } let x = Real::assert(0.0); let y = Real::assert(0.0); let z = (x / y)?; ``` ## Proxy Types The primary API of Decorum is its `Constrained` types, which transparently wrap primitive IEEE 754 floating-point types and configure their behavior. `Constrained` types support many numeric features and operations and integrate with the [`num-traits`] crate and others when [Cargo features](#cargo-features) are enabled. Depending on its configuration, a proxy can be used as a drop-in replacement for primitive floating-point types. The following `Constrained` behaviors can be configured: 1. the allowed subset of IEEE 754 floating-point values 1. the output type of fallibe operations (that may produce non-member values w.r.t. a subset) 1. what happens when an error occurs (i.e., return an error value or panic) Note that the output type of fallible operations and the error behavior are independent. A `Constrained` type may return a `Result` and yet panic if an error occurs, which can be useful for conditional compilation and builds wherein **behavior** changes but types do not. The behavior of a `Constrained` type is configured using two mechanisms: _constraints_ and _divergence_. ```rust use decorum::constraint::IsReal; use decorum::divergence::OrPanic; use decorum::proxy::Constrained; // `Real` must represent a real number and otherwise panics. pub type Real = Constrained>; ``` Constraints specify a subset of floating-point values that a proxy may represent. IEEE 754 floating-point values are divided into three such subsets: | Subset | Example Member | |---------------|----------------| | real numbers | `3.1459` | | infinities | `+INF` | | not-a-numbers | `NaN` | Constraints can be used to strictly represent real numbers, extended reals, or complete but totally ordered IEEE 754 types (i.e., no constraints). Available constraints are summarized below: | Constraint | Members | Fallible | |------------------|-----------------------------------------|-----------| | `IsFloat` | real numbers, infinities, not-a-numbers | no | | `IsExtendedReal` | real numbers, infinities | yes | | `IsReal` | real numbers | yes | `IsFloat` supports all IEEE 754 floating-point values and so applies no constraint at all. As such, it has no fallible operations w.r.t. the constraint and does not accept a divergence. Many operations on members of these subsets may produce values from other subsets that are illegal w.r.t. constraints, such as the addition of two real numbers resulting in `+INF`. A _divergence type_ determines both the behavior when an illegal value is encountered as well as the output type of such fallible operations. | Divergence | OK | Error | Default Output Kind | |------------|----------|-----------|---------------------| | `OrPanic` | continue | **panic** | `AsSelf` | | `OrError` | continue | break | `AsExpression` | In the above table, _continue_ refers to returning a **non**-error value while _break_ refers to returning an error value. If an illegal value is encountered, then **the `OrPanic` divergence panics** while the `OrError` divergence constructs a value that encodes the error. The output type of fallible operations is determined by an _output kind_: | Output Kind | Type | Continue | Break | |----------------|-----------------------|-----------------|--------------------| | `AsSelf` | `Self` | `self` | | | `AsOption` | `Option` | `Some(self)` | `None` | | `AsResult` | `Result` | `Ok(self)` | `Err(error)` | | `AsExpression` | `Expression` | `Defined(self)` | `Undefined(error)` | In the table above, `Self` refers to a `Constrained` type and `E` refers to the associated error type of its constraint. Note that only the `OrPanic` divergence supports `AsSelf` and can output the same type as its input type for fallible operations (just like primitive IEEE 754 floating-point types). With the sole exception of `AsSelf`, the output type of fallible operations is extrinsic: fallible operations produce types that differ from their input types. The `Expression` type, which somewhat resembles the standard `Result` type, improves the ergonomics of error handling by implementing mathematical traits such that it can be used directly in expressions and defer error checking. ```rust use decorum::constraint::IsReal; use decorum::divergence::{AsExpression, OrError}; use decorum::proxy::{Constrained, OutputFor}; use decorum::real::UnaryRealFunction; use decorum::try_expression; pub type Real = Constrained>>; pub type Expr = OutputFor; pub fn f(x: Real, y: Real) -> Expr { let sum = x + y; sum * g(x) } pub fn g(x: Real) -> Expr { x + Real::ONE } let x: Real = try_expression! { f(Real::E, -Real::ONE) }; // ... ``` When using a nightly Rust toolchain with the `unstable` [Cargo feature](#cargo-features) enabled, `Expression` also supports the (at time of writing) unstable `Try` trait and try operator `?`. ```rust // As above, but using the try operator `?`. let x: Real = f(Real::E, -Real::ONE)?; ``` `Constrained` types support numerous constructions and conversions depending on configuration, including conversions for references, slices, subsets, supersets, and more. Conversions are provided via inherent functions and implementations of the standard `From` and `TryFrom` traits. The following inherent functions are supported by all `Constrained` types, though some more bespoke constructions are available for specific configurations. | Method | Input | Output | Error | |------------------------|-----------|-----------|---------------| | `new` | primitive | proxy | break | | `assert` | primitive | proxy | **panic** | | `try_new` | primitive | proxy | `Result::Err` | | `try_from_{mut_}slice` | primitive | proxy | `Result::Err` | | `into_inner` | proxy | primitive | | | `from_subset` | proxy | proxy | | | `into_superset` | proxy | proxy | | The following type definitions provide common proxy configurations. Each type implements different traits that describe the supported encoding and elements of IEEE 754 floating-point based on its constraints. | Type Definition | Sized Aliases | Trait Implementations | Illegal Values | |-----------------|---------------|-------------------------------------------------|-----------------------| | `Total` | | `BaseEncoding + InfinityEncoding + NanEncoding` | | | `ExtendedReal` | `E32`, `E64` | `BaseEncoding + InfinityEncoding` | `NaN` | | `Real` | `R32`, `R64` | `BaseEncoding` | `NaN`, `-INF`, `+INF` | ## Relations and Total Ordering Decorum provides the following non-standard total ordering for IEEE 754 floating-point representations: ``` -INF < ... < 0 < ... < +INF < NaN ``` IEEE 754 floating-point encoding has multiple representations of zero (`-0` and `+0`) and `NaN`. This ordering and equivalence relations consider all zero and `NaN` representations equal, which differs from the [standard partial ordering](https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN). Some proxy types disallow unordered `NaN` values and therefore support a total ordering based on the ordered subset of non-`NaN` floating-point values. `Constrained` types that use `IsFloat` (such as the `Total` type definition) support `NaN` but use the total ordering described above to implement the standard `Eq`, `Hash`, and `Ord` traits. The following traits can be used to compare and hash primitive floating-point values (including slices) using this non-standard relation. | Floating-Point Trait | Standard Trait | |----------------------|------------------| | `CanonicalEq` | `Eq` | | `CanonicalHash` | `Hash` | | `CanonicalOrd` | `Ord` | ```rust use decorum::cmp::CanonicalEq; let x = 0.0f64 / 0.0f64; // `NaN`. let y = f64::INFINITY + f64::NEG_INFINITY; // `NaN`. assert!(x.eq_canonical(&y)); ``` Decorum also provides the `EmptyOrd` trait and the `min_or_empty` and `max_or_empty` functions. This trait defines a particular ordering for types that may have a notion of empty inhabitants. An empty inhabitant is considered incomparable, and comparisons return an empty inhabitant when encountered. For example, `None` is the empty inhabitant for `Option`. For floating-point types (including proxy types), `NaN`s are considered empty inhabitants. `EmptyOrd` functions forward `NaN`s when comparing these types just like most numeric operations (unlike `f64::max`, etc.). ```rust use decorum::cmp; use decorum::real::{Endofunction, RealFunction, UnaryRealFunction}; pub fn f(x: T, y: T) -> T where T: Endofunction + RealFunction, { // `min` is assigned an empty inhabitant if either `x` or `y` are an empty // inhabitant. For `T`, the empty inhabitants are `NaN`s, so this function // forwards any input `NaN`s to its output. let min = cmp::min_or_empty(x, y); min * T::PI } ``` ## Mathematical Traits The `real` module provides various traits that describe real numbers and constructions via IEEE 754 floating-point types. These traits model functions and operations on real numbers and specify a codomain for functions where the output is not mathematically confined to the reals or a floating-point exception may yield a non-real approximation or error. For example, the logarithm of zero is undefined and the sum of two very large reals results in an infinity in IEEE 754. For proxy types, the codomain is the same as the branch type of its divergence (see above). Real number and IEEE 754 encoding traits can both be used for generic programming. The following code demonstrates a function that accepts types that support floating-point infinities and real functions. ```rust use decorum::real::{Endofunction, RealFunction}; use decorum::InfinityEncoding; fn f(x: T, y: T) -> T where T: Endofunction + InfinityEncoding + RealFunction, { let z = x / y; if z.is_infinite() { x + y } else { z + y } } ``` ## Cargo Features Decorum supports the following feature flags. | Feature | Default | Description | |------------|---------|--------------------------------------------------------------| | `approx` | yes | Implements traits from [`approx`] for `Constrained` types. | | `serde` | yes | Implements traits from [`serde`] for `Constrained` types. | | `std` | yes | Integrates the `std` library and enables dependent features. | | `unstable` | no | Enables features that require an unstable compiler. | [`approx`]: https://crates.io/crates/approx [`num-traits`]: https://crates.io/crates/num-traits [`serde`]: https://crates.io/crates/serde