GithubHelp home page GithubHelp logo

olson-sean-k / decorum Goto Github PK

View Code? Open in Web Editor NEW
72.0 4.0 8.0 484 KB

Making floating-point behave.

License: MIT License

Rust 100.00%
rust library floating-point hashing ordering ieee-754 equivalence

decorum's Introduction

Decorum

Decorum is a Rust library that provides total ordering, equivalence, hashing, constraints, error handling, and more for IEEE 754 floating-point representations. Decorum requires Rust 1.65.0 or higher and, except for specific features, does not require the std nor alloc libraries.

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::{OutputOf, Proxy};

    // A 64-bit floating point type that must represent a real number and returns
    // `Result`s from fallible operations.
    pub type Real = Proxy<f64, IsReal<OrError<AsResult>>>;
    pub type Result = OutputOf<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 Proxy types, which transparently wrap primitive IEEE 754 floating-point types and configure their behavior. Proxy 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 Proxy 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 Proxy 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 Proxy type is configured using two mechanisms: constraints and divergence.

use decorum::constraint::IsReal;
use decorum::divergence::OrPanic;
use decorum::proxy::Proxy;

// `Real` must represent a real number and otherwise panics.
pub type Real = Proxy<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:

Branch Type Continue Break
AsSelf Self Self
AsOption Option<Self> Some(Self) None
AsResult Result<Self, E> Ok(Self) Err(E)
AsExpression Expression<Self, E> Defined(Self) Undefined(E)

In the table above, Self refers to a Proxy 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::{OutputOf, Proxy};
use decorum::real::UnaryRealFunction;
use decorum::try_expression;

pub type Real = Proxy<f64, IsReal<OrError<AsExpression>>>;
pub type Expr = OutputOf<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)?;

Proxy 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 Proxy types, though some more bespoke constructions are available for specific configurations.

Proxy 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. Proxy 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
FloatEq Eq
FloatHash Hash
FloatOrd Ord
use decorum::cmp::FloatEq;

let x = 0.0f64 / 0.0f64; // `NaN`.
let y = f64::INFINITY + f64::NEG_INFINITY; // `NaN`.
assert!(x.float_eq(&y));

Decorum also provides the IntrinsicOrd trait and the min_or_undefined and max_or_undefined functions. These pairwise comparisons can be used with partially ordered types that have an intrinsic representation for undefined, such as Option (None) and IEEE 754 floating-point representations (NaN). For floating-point representations, this provides an ergonomic method for comparison that naturally propogates NaNs just like floating-point operations do (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,
{
    // If the comparison is undefined, then `min` is assigned some
    // representation of undefined. For floating-point types, `NaN` represents
    // undefined and cannot be compared, so `min` is `NaN` if `x` or `y` is
    // `NaN`.
    let min = cmp::min_or_undefined(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 Proxy types.
serde yes Implements traits from serde for Proxy types.
std yes Integrates the std library and enables dependent features.
unstable no Enables features that require an unstable compiler.

decorum's People

Contributors

athre0z avatar dependabot-preview[bot] avatar olson-sean-k avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

decorum's Issues

Improve `Debug` implementation

Hello there and thank you for the lib!

Currently, std::fmt::Debug is implemented for the float types via a derive. However, due to it containing a PhantomData field, the output is rather chunky. It looks like like:

ConstrainedFloat {
    value: -0.00025,
    phantom: PhantomData,
}

With structs that have a lot of floating point members, this printing is very noisy. Personally, I'd prefer if we could perhaps instead just print the floating point literal?

Please let me know what you think -- If you're ok with this, I'd be happy to PR this!

Is there a way to create custom constraints?

I'd like to define custom constraints on floating points, like

  • positive real
  • unit-range (between 0.0 and 1.0)

Is that possible already? If so, am I missing the documentation?

R64 does not satisfy nalgebra::Real

I get the following error while tryring to use magnitude() on a nalgebra::Vector3<decorum::R64>:

no method named `magnitude` found for type `na::Matrix<decorum::ConstrainedFloat<f64, decorum::constraint::FiniteConstraint<f64>>, na::U3, na::U1, na::ArrayStorage<decorum::ConstrainedFloat<f64, decorum::constraint::FiniteConstraint<f64>>, na::U3, na::U1>>` in the current scope

note: the method `magnitude` exists but the following trait bounds were not satisfied:
      `decorum::ConstrainedFloat<f64, decorum::constraint::FiniteConstraint<f64>> : na::Real`rustc(E0599)

The same problem comes up with normalize() but I can bypass using plexus::geomertry::ops::Normalize trait. Is this how it's supposed to be? Should I bypass the magnitude issue by implementing my own trait?

(I'm new to Rust, sorry if I'm missing something obvious here)

Point of the std feature

I've had a look at the code and I don't see the std feature being used anywhere. Why is there a std feature and a conditional extern crate std; declaration if that is never used anywhere?

Note that removing these will still result in the default features being no_std-incompatible, due to serde(-derive)'s default features being enabled. You probably don't depend on serde's std feature though (at least I don't see how you would). If your crate indeed works with serde = { version = "1.0", default-features = false }, your crate could unconditionally be no_std compatible. It would be a breaking change for anyone manually specifying features though.

Serialization of proxy types does not encode the proxy type, only the inner type.

The implementation of serialization with serde is incorrect. Only the raw floating point value is encoded, omitting any information about the originating proxy type. For example, serialization should probably use serialize_newtype_struct instead of into_raw_float + serialize.

Consider using cfg_attr with derive(Deserialize, Serialize), though this may not be possible due to the type parameters on ConstrainedFloat.

Implement custom de/serialization for non-real floating-point values.

Proxy implements de/serialization using Serde, but currently serializes as a structure with a single field. It would probably be better to serialize proxy types as raw floating-point primitives instead (as seen in #25).

Care must be taken to enforce constraints when deserializing, especially if the serialized format gives no indication that any such constraints should be applied. I have a working approach in d93535c on the serde branch. It uses an intermediate type with transparent de/serialization and a conversion into/from proxy types that applies constraints.

One remaining problem is that serde_json does not support serialization of non-real values for floating-point primitives out of the box. NaNs and infinities are serialized as "null", which cannot be deserialized. Not only does this lose information, but there is no way to round-trip a non-real value. Note that commonly used serializations like "nan" are not supported. One option for improving this is custom de/serialization via additional types gated by a Cargo feature. Gating would be necessary, since the de/serialization would be non-standard, but could be used on a case-by-case basis for any downstream crates that want to be able to de/serialize non-real floating-point values.

Provide functions for checking raw floating point values for equality.

Hashing functions like hash_float_array are provided because sometimes it is not possible or ergonomic to use wrapper types within another type. README.md mentions this example:

use decorum;

#[derive(Derivative)]
#[derivative(Hash)]
pub struct Vertex {
    #[derivative(Hash(hash_with = "decorum::hash_float_array"))]
    pub position: [f32; 3],
    ...
}

This vertex type may be fed to graphics code that expects raw floating point data (for example, see the gfx pipeline macros and shader compilers).

A similar problem exists for implementing Eq: if it is not possible or ergonomic to use wrapper types, there is currently no convenient way to implement Eq. This can be done via conversions, but that gets messy fairly quickly. Instead, Decorum should provide eq_float, eq_float_slice, and eq_float_array functions that are analogous to the hashing functions.

Mimic the standard library in regards to checked operations.

Right now, constraints are controlled by a crate feature. This comes with some complications, but it notably disagrees with the established patterns in the standard library. Integer types check for things like overflow in debug builds but do not in release builds. Moreover, integer types provide explicitly checked operations for code that cannot reasonably guarantee that input values or the results of operations are valid (even in release builds).

Mimic the standard library instead:

  • Remove the enforce-constraints feature.
  • Only check constraints in debug builds.
  • Implement the checked operation traits in the num crate.
  • Provide any additional checked operations.

Cycle detected when const-evaluating NAN

Newer versions of decorum (0.2.0 or later) fail to compile using Rust 1.42.0 with the error:

error[E0391]: cycle detected when const-evaluating + checking `primitive::<impl at /Users/me/.cargo/registry/src/github.com-1ecc6299db9ec823/decorum-0.3.1/src/primitive.rs:23:9: 29:10>::NAN`

If there is some incompatibility and there is a minimal supported version of Rust, this should be explained in the README.

Consider expanding the scope of Decorum or creating related crates.

Some of the basic wrapper traits like Proxy and Primitive can be useful in a broader context. For example, see this code that provides a numeric wrapper that clamps values.

Perhaps tools like this should be provided by Decorum. Another possibility is refactoring basic traits into another crate and then providing more specific crates atop that.

Consider allowing for different orderings (with some reasonable default).

Decorum is opinionated about the total ordering it provides for floating-point values. Users cannot modify this behavior, and it is essentially hard-coded (see the canonical and constraint modules).

Maybe this should be parameterized via an additional type parameter. That parameter should have a default, and that default should probably use the "natural" ordering provided today (i.e., NaN and zero have single canonical forms). However, some users may want to use something more akin to the IEEE-754 ordering, where -NaN is less than -INF, NaN is greater than INF, and negative zero is less than zero (i.e., there is a distinction between negative and positive variants of NaN and zero).

I've seen some discussion about this regarding similar libraries, and I think alternative ordering may be useful for some applications.

Is there a way to optimize Option<R64> ?

R64 does have some niche values (e.g. Nan values), is it possible to somehow communicate them to the compiler to optimize the size of Option<R64>, so that it's no bigger ?

Improve documentation.

The documentation could use some work. Fix any inconsistencies, document errors, and provide examples.

This issue should be examined after any other 0.1.0 milestone issues that affect the API have been closed.

Improve testing.

There are basically no tests. At the very least, create unit tests to ensure that contraints are properly upheld and that conversions work as expected.

Integrate with num-traits 0.2.*.

The 0.2.* series of the num-traits crate includes a Real trait (introduced in 0.1.42) that is nearly identical to Decorum's. Before hitting 0.1.0, Decorum should integrate with these changes and replace it's Real trait with num-traits'.

This will be tricky though. num-traits currently provides a blanket implementation such that T: Float โ‡’ T: Real (i.e., impl<T> Real for T where T: Float { ... }). This makes it impossible to write implementations of the Float and Real traits for ConstrainedFloat that are parameterized over the input FloatConstraint type. I'm still not sure how to work around this.

Use more precise names for associated constants of the `Encoding` trait.

The Encoding trait expresses some notion of minimum and maximum values that floating-point types can represent (and the Bounded trait from num-traits is similar). For floating-point types, this is a bit misleading, because these traits only consider representations of real numbers and disagree with ordering when considering all classes of values that floating-point can represent.

For example, <f64 as Encoding>::MAX and <f64 as Bounded>::max_value() do not yield +INF despite the fact that the partial ordering for f64 considers infinity greater than these values. The name MAX suggests "the maximum possible value", but is really "the maximum possible real value".

These semantics are fine and useful, but Encoding should reflect the restriction to real numbers in the names of its associated constants. Perhaps MAX should be MAX_REAL or MAX_NUM, for example.

Zero values are not handled consistently.

Zero values are handled differently for comparison (cmp_float) than hashing (hash_float). Hashing canonicalizes zeroes to a single representation, but ordering does not handle zeroes at all. In other words, hashing assumes -0 == 0 and ordering assumes -0 < 0. Ordering should detect zeroes and agree with the -0 == 0 relation.

This is somewhat related to #7. If different orderings are implemented, it will be important to ensure that they interact well with hashing.

Implement `ToCanonicalBits` for all proxy types.

The ToCanonicalBits trait is implemented for a type T with the bounds T: Encoding + Nan. This is a convenient implementation in which Encoding + Nan implies ToCanonicalBits, but it does not include the NotNan and Finite types, because they do not implement Nan.

It may be better to implement ToCanonicalBits explicitly for the primitive f32 and f64 types and provide a blanket implementation for ConstrainedFloat that simply delegates to the implementation of its wrapped floating-point type (supporting all of its type definitions).

For the 0.4.x series, a99c247 introduces an associated type so that the size of the output bit vector can depend on the implementation. The implementations for f32 and f64 would be identical before this change, but should diverge and use u32 and u64 as the output types, respectively.

Implement field and other numeric traits from alga.

Other crates like approx and alga (used by nalgebra) define traits and implement them for native floating point numbers. They do use generics, so that Decorums numbers can be used. Some of their functionality isn't available though with Decorum's numbers, as their traits are not implemented for Decorums numbers.

Due to Rust's Orphan Rule users of Decorum and those libraries cannot implement the other libraries' traits for their use of Decorum. Either the libraries declaring the traits or Decorum must implement them. What strategy does Decorum use for implementing foreign traits? What dependency hierarchy should be created? Should those libraries depend on Decorum or should Decorum depend on those libraries?

I can imagine creating features in Decorum for use with well known libraries, like the above mentioned, might work.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.