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.
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)?;
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:
- the allowed subset of IEEE 754 floating-point values
- the output type of fallibe operations (that may produce non-member values w.r.t. a subset)
- 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 |
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), NaN
s are considered empty inhabitants. EmptyOrd
functions forward NaN
s 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
}
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
}
}
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. |