1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
//! This module shows some examples of how Rust's type system can be used to write safe yet
//! efficient code.
//!
//! There are three implementations of the problem of implementing float comparison. The problem is
//! that according to the *IEEE* standard [f64] is only [PartialOrd] and not [Ord] since some
//! floats cannot be compared (e.g. *nan*).
//!
//! However, we can restrict our comparison to *positive floats*. The memory representation of each
//! positive float in IEEE standard format can be interpreted as [u32]. Integer representations of
//! positive floats are then *monotonic* and thus have efficient [Ordering].
//!
//! The three examples presented here then address the two remaining problems:
//! 1. How to ensure that given [f64] is positive
//! 1. How to *safely* compare floats when [f64::to_int_unchecked] is `unsafe`
//!
//! Note that one would typically realize these as implementations of [PartialOrd] or [Ord] but we
//! keep it simple and implement the comparison as plain function.
//!
//! Also note that `std` actually defines a safe memory interpretation [f64::to_bits] so this
//! example is somewhat artificial.
//!
//! Last few examples describe the [top](https://en.wikipedia.org/wiki/Top_type) and
//! [bottom](https://en.wikipedia.org/wiki/Bottom_type) type realized in Rust's type system.
use std::cmp::Ordering;
/// Naive *positive* [f64] comparison function.
///
/// This implementation first checks both arguments whether they are positive and *panics*
/// otherwise. After this check it is safe to call [f64::to_int_unchecked] and do the comparison on
/// [u32]s;
///
/// # Pros
/// 1. Clients don't have to check for anything on the call side
/// 1. This call plays nicely with other APIs
///
/// # Cons
/// 1. Call to this function might crash the program! Also clients must somehow know this (e.g.
/// read the docs)
/// 1. Each call adds a branching instruction which might get significant when called frequently
/// 1. Typical protection against #1 will be adding the same check and thus duplicating the
/// code
pub fn cmp_f64(a: f64, b: f64) -> Ordering {
if !a.is_sign_positive() || !b.is_sign_positive() {
panic!("Both a and b must be positive reals");
}
unsafe {
let a: u32 = a.to_int_unchecked();
let b: u32 = b.to_int_unchecked();
a.cmp(&b)
}
}
/// Improved comparison function for *positive* [f64]s.
///
/// This implementation is slightly better and more idiomatic version of the naive approach.
/// Instead of calling [panic!] when an argument is not positive, we return an
/// [`Option<Ordering>`](Option).
///
/// # Pros
/// 1. Clients are forced to handle None even though they know it's never returned
/// 1. Each call adds a branching instruction which might get significant when called frequently
/// 1. If this is called frequently, the initial check if sign is positive will soon get costly
///
/// # Cons
/// 1. Clients must handle the [`Option<Ordering>`](Option) and it might not integrate well with
/// other standard APIs
/// 1. For performance-critical applications it's quite unfortunate that this version double checks
/// `a` and `b` with an `if` to make sure the `unsafe` block is sound
pub fn better_cmp_f64(a: f64, b: f64) -> Option<Ordering> {
if !a.is_sign_positive() || !b.is_sign_positive() {
return None;
}
unsafe {
let a: u32 = a.to_int_unchecked();
let b: u32 = b.to_int_unchecked();
Some(a.cmp(&b))
}
}
/// Opaque wrapper around [f64] which adds static semantics that the float has positive value.
///
/// The benefit of such abstraction is two fold:
/// 1. It adds static information about legal values that the compiler can't possibly know about
/// 1. while it incurs zero cost at runtime (it gets "compiled away" and behaves as an ordinary
/// [f64])
///
/// While it might seem annoying to work with [Positive] instead of plain [f64], one can `derive`
/// many traits implemented for [f64] so that instances of [Positive] can be a drop-in replacement
/// for it in many cases.
///
/// Additionally, there is an incentive in the Rust team to further mitigate the pain to work with
/// these wrappers by introducing something like a `#[newtype_derive]` macro inspired by the
/// *newtype pattern* from Haskell.
///
/// Note this could be generalized with the [num crate](https://crates.io/crates/num):
/// ```ignore
/// use num::Float;
///
/// struct Positive<F: Float>(F);
/// ```
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct Positive(f64);
impl Positive {
/// This forces clients to always check if it's ok. One cannot initialize a tuple struct which
/// contains private fields.
///
/// So the following code **does not compile** because `f64` is a private field in [Positive].
/// ```compile_fail
/// use rust_examples::typing::Positive;
///
/// let _ = Positive(-24.);
/// ```
/// The only option is then to use this factory method and therefore check the result.
/// ```
/// use rust_examples::typing::Positive;
///
/// assert_eq!(Positive::new(-24.), None)
/// ```
pub fn new(number: f64) -> Option<Self> {
if !number.is_sign_positive() {
return None;
}
Some(Self(number))
}
/// Interprets [Positive] as an [u32]
///
/// Note that this is not OOP, one can call [Positive::as_u32] as an ordinary function:
/// ```
/// use rust_examples::typing::Positive;
///
/// let pos = Positive::new(42.).expect("positive number");
/// let int = unsafe { Positive::as_u32(&pos) };
/// assert_eq!(int, 42);
/// ```
///
/// # Safety
/// The safety is guaranteed by the construction of [Positive] instances via [Positive::new].
#[inline(always)]
pub unsafe fn as_u32(&self) -> u32 {
self.0.to_int_unchecked::<u32>()
}
}
/// Safe and efficient version of comparison of two [Positive] floats.
///
/// This approach is a combination of the *make illegal states unrepresentable* and *fail early*
/// principles. Instead of having two [f64] arguments we enforce the user to pass in instances of
/// [Positive] which makes the comparison both trivial and safe.
///
/// # Pros
/// 1. Now his operation is completely safe even though it uses unsafe. It is not possible to
/// compile and run a program which calls this function with a negative number. This is a form of
/// formal validation done by the compiler and thus much stronger result than any (unit) test!
/// 1. The constructor of the wrapper type pushes the clients to check for errors early on
/// 1. The wrapper type carries certain semantics which can be taken to a benefit in the
/// implementation
///
/// # Cons
/// 1. Clients must wrap their data into the wrapper type which might get tedious and not worth it
/// for non-critical data flows (although, this might be mitigated in the future).
pub fn safe_cmp_f64(a: Positive, b: Positive) -> Ordering {
// Rust's `unsafe` keyword is basically saying: Hey, Borrow Checker, I'm now resposible for the semantics of the memory management
// in this block of code. Note that any crash in this scope results in an undefined behavior!
unsafe { a.as_u32().cmp(&b.as_u32()) }
}
/// Structure that defines single field which has the type of the
/// [*top type*](https://en.wikipedia.org/wiki/Top_type) in Rust.
///
/// Because Rust's type system lacks central hierarchy and polymorphism is realized by (not only)
/// [bounded parametric polymorphism](https://en.wikipedia.org/wiki/Parametric_polymorphism), the
/// *top type* is actually the most generic type parameter - a type parameter which can be realized
/// by any type value.
///
/// One might think that the most generic type parameter is simply `<T>` but in Rust this
/// implicitly adds a bound that `T: Sized`. Therefore the most generic version is `<T: ?Sized>`
/// which relaxes this restriction.
pub struct TopTypeExample<T: ?Sized> {
pub top_type: T,
}
/// This example presents several constructs which represent an empty set of values.
///
/// The, so far unstable, type [`!`](!) is Rust's version of the
/// [*bottom type*](https://en.wikipedia.org/wiki/Bottom_type). From categorical point of view,
/// this type is the [*initial object*](https://en.wikipedia.org/wiki/Initial_and_terminal_objects).
///
/// # Example: Never type
/// The *never_type* [`!`](!) shown below cannot be instantiated as it represents no actual value.
/// ```compile_fail
/// struct Void {
/// bottom_type: !,
/// }
///
/// /// This function can never be called since [Void] type has no values to be passed in. One can
/// /// view [absurd] as proposition `false => t` being true for arbitrary `t`, i.e. one can infer
/// /// any (absurd) fact from false assumptions.
/// fn absurd<T>(_: Void) -> T {
/// unreachable!()
/// }
/// ```
///
/// # Example: Enum with no variants
/// Another example of a type which can't be instantiated is an `enum` with no variant.
/// ```
/// enum Void {}
/// ```
///
/// # Example: Declarative macros
/// There's a reason why *macros* end with the never type [`!`](!) - macros are language constructs
/// which are *replaced* by the code snippet they define at compilation time. Therefore there is no
/// actual value they represent by themself, just like [`!`](!).
/// ```
/// println!("Rust");
/// ```
///
/// # Example: Other constructs
/// There are other language constructs which result in no value. These are typically statements
/// like single branch `if` or `return` an `break`.
pub struct BottomTypeExample;
/// Structure that defines single field which has the type of the
/// [*unit type*](https://en.wikipedia.org/wiki/Unit_type) in Rust.
///
/// The unit type has just single value an is *unique up to unique isomorphism*. In category theory
/// it is also known as the
/// [*terminal object*](https://en.wikipedia.org/wiki/Initial_and_terminal_objects) (alternatively
/// the *final object*).
///
/// There is a unique morphism (function) from any other type to it as shown below and it servers
/// as a unit for product types (tuples).
/// ```
/// fn unit<T>(_: T) -> () {
/// ()
/// }
/// ```
pub struct UnitTypeExample {
pub unit_type: (),
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::*;
#[rstest]
#[case::greater(2., 1., Ordering::Greater)]
#[case::less(1., 2., Ordering::Less)]
#[case::equal(1., 1., Ordering::Equal)]
#[should_panic]
#[case::error(2., -1., Ordering::Greater)]
#[should_panic]
#[case::error(-1., 2., Ordering::Less)]
fn safe_float_cmp(#[case] a: f64, #[case] b: f64, #[case] expected: Ordering) {
let a = Positive::new(a).expect(&format!("a shold be a positive float, got {}", a));
let b = Positive::new(b).expect(&format!("b shold be a positive float, got {}", b));
assert_eq!(safe_cmp_f64(a, b), expected);
}
}
