Coder Social home page Coder Social logo

decorum's Introduction

Decorum

Decorum is a Rust library that provides total ordering, equivalence, hashing, and constraints for floating-point representations. Decorum requires Rust 1.43.0 or higher and does not require the std library.

GitHub docs.rs crates.io

Total Ordering

The following total ordering is exposed via traits for primitive types and proxy types that implement Ord:

[ -INF < ... < 0 < ... < +INF < NaN ]

IEEE-754 floating-point encoding provides multiple representations of zero (-0 and +0) and NaN. This ordering considers 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 (see below).

Proxy Types

Decorum exposes several proxy (wrapper) types. Proxy types provide two primary features: they implement total ordering and equivalence via the Eq, Ord, and Hash traits and they constrain the set of floating-point values they can represent. Different type definitions apply different constraints, with the Total type applying no constraints at all.

Type Aliases Trait Implementations Disallowed Values
Total Encoding + Real + Infinite + Nan + Float
NotNan N32, N64 Encoding + Real + Infinite NaN
Finite R32, R64 Encoding + Real NaN, -INF, +INF

Proxy types implement common operation traits, such as Add and Mul. These types also implement numeric traits from the num-traits crate (such as Float, Num, NumCast, etc.), in addition to more targeted traits like Real and Nan provided by Decorum.

Constraint violations cause panics in numeric operations. For example, NotNan is useful for avoiding or tracing sources of NaNs in computation, while Total provides useful features without introducing any panics at all, because it allows any IEEE-754 floating-point values.

Proxy types should work as a drop-in replacement for primitive types in most applications with the most common exception being initialization (because it requires a conversion). Serialization is optionally supported with serde and approximate comparisons are optionally supported with approx via the serialize-serde and approx features, respectively.

Traits

Traits are essential for generic programming, but the constraints used by some proxy types prevent them from implementing the Float trait, because it implies the presence of -INF, +INF, and NaN (and their corresponding trait implementations).

Decorum provides more granular traits that separate these APIs: Real, Infinite, Nan, and Encoding. Primitive floating-point types implement all of these traits and proxy types implement traits that are consistent with their constraints.

For example, code that wishes to be generic over floating-point types representing real numbers and infinities can use a bound on the Infinite and Real traits:

use decorum::{Infinite, Real};

fn f<T>(x: T, y: T) -> T
where
    T: Infinite + Real,
{
    let z = x / y;
    if z.is_infinite() {
        y
    }
    else {
        z
    }
}

Both Decorum and num-traits provide Real and Float traits. These traits are somewhat different and are not always interchangeable. Traits from both crates are implemented by Decorum where possible. For example, Total implements Float from both Decorum and num-traits.

Construction and Conversions

Proxy types are used via constructors and conversions from and into primitive floating-point types and other compatible proxy types. Unlike numeric operations, these functions do not necessarily panic if a constraint is violated.

Method Input Output Violation
new primitive proxy error
assert primitive proxy panic
into_inner proxy primitive n/a
from_subset proxy proxy n/a
into_superset proxy proxy n/a
try_from_slice primitive proxy error
from_slice primitive proxy n/a

The new constructor and into_inner conversion move primitive floating-point values into and out of proxies and are the most basic way to construct and deconstruct proxies. Note that for Total, which has no constraints, the error type is Infallible.

The assert constructor panics if the given primitive floating-point value violates the proxy's constraints. This is equivalent to unwrapping the output of new.

The into_superset and from_subset conversions provide an inexpensive way to convert between proxy types with different but compatible constraints.

Finally, the try_from_slice and Total::from_slice conversions coerce slices of primitive floating-point values into slices of proxies, which have the same representation.

use decorum::R64;

fn f(x: R64) -> R64 {
    x * 3.0
}

let y = R64::assert(3.1459);
let z = f(R64::new(2.7182).unwrap());
let w = z.into_inner();

let xs = [0.0f64, 1.0, 2.0];
let ys = R64::try_from_slice(&xs).unwrap();

All conversions also support the standard From/Into and TryFrom/TryInto traits, which can also be applied to primitives and literals. Reference coercions are supported only via these standard traits; there are no such inherent functions.

use core::convert::{TryFrom, TryInto};
use decorum::R64;

fn f(x: R64) -> R64 {
    x * 2.0
}

let y: R64 = 3.1459.try_into().unwrap();
let z = f(R64::try_from(2.7182).unwrap());
let w: f64 = z.into();
let r: &R64 = (&w).try_into().unwrap();

Hashing and Comparing Primitives

Proxy types implement Eq, Hash, and Ord, but sometimes it is not possible or ergonomic to use such a type. Traits can be used with primitive floating-point values for ordering, equivalence, and hashing instead.

Floating-Point Trait Standard Trait
FloatEq Eq
FloatHash Hash
FloatOrd Ord

These traits use the same total ordering and equivalence rules that proxy types do. They are implemented for primitive types like f64 as well as slices like [f64].

use decorum::cmp::FloatEq;

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

let xs = [1.0f64, f64::NAN, f64::INFINITY];
let ys = [1.0f64, f64::NAN, f64::INFINITY];
assert!(xs.float_eq(&ys));

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

Watchers

 avatar  avatar  avatar  avatar

decorum's Issues

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.

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.

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.

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.

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.

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.

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 ?

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.

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)

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.

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.

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?

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.

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!

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.

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.

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.

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.