#nan #order #real-number #hash #float #finite

no-std decorum

Total ordering, equivalence, hashing, and constraints for floating-point types

20 releases

0.4.0 Nov 22, 2024
0.3.1 May 31, 2020
0.2.0 May 4, 2020
0.1.3 Jan 11, 2019
0.0.7 Nov 15, 2017

#53 in Rust patterns

Download history 22244/week @ 2024-08-25 18986/week @ 2024-09-01 20893/week @ 2024-09-08 19346/week @ 2024-09-15 21386/week @ 2024-09-22 20912/week @ 2024-09-29 20591/week @ 2024-10-06 21459/week @ 2024-10-13 24518/week @ 2024-10-20 22428/week @ 2024-10-27 22011/week @ 2024-11-03 19714/week @ 2024-11-10 21598/week @ 2024-11-17 7217/week @ 2024-11-24 19643/week @ 2024-12-01 19327/week @ 2024-12-08

69,226 downloads per month
Used in 62 crates (27 directly)

MIT license

180KB
4K SLoC

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 docs.rs crates.io

Basic Usage

Panic when a NaN is encountered:

use decorum::NotNan;

let x = NotNan::<f64>::assert(0.0);
let y = NotNan::<f64>::assert(0.0);
let z = x / y; // Panics.

Hash totally ordered IEEE 754 floating-point representations:

use decorum::real::UnaryRealFunction;
use decorum::Real;
use std::collections::HashMap;

let key = Real::<f64>::PI;
let mut xs: HashMap<_, _> = [(key, "pi")].into_iter().collect();

Configure the behavior of an IEEE 754 floating-point representation:

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<f64, IsReal<OrError<AsResult>>>;
    pub type Result = OutputFor<Real>;
}

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 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
  2. the output type of fallibe operations (that may produce non-member values w.r.t. a subset)
  3. 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.

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<f64, IsReal<OrPanic>>;

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<Self> Some(self) None
AsResult Result<Self, E> Ok(self) Err(error)
AsExpression Expression<Self, E> 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.

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<f64, IsReal<OrError<AsExpression>>>;
pub type Expr = OutputFor<Real>;

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 enabled, Expression also supports the (at time of writing) unstable Try trait and try operator ?.

// 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.

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
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), NaNs are considered empty inhabitants. EmptyOrd functions forward NaNs when comparing these types just like most numeric operations (unlike f64::max, etc.).

use decorum::cmp;
use decorum::real::{Endofunction, RealFunction, UnaryRealFunction};

pub fn f<T>(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.

use decorum::real::{Endofunction, RealFunction};
use decorum::InfinityEncoding;

fn f<T>(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.

Dependencies

~0.5–1.1MB
~24K SLoC