Expressive Wrapper Types


#1

D’s alias this and operator overloading makes the language very expressive when implementing aggregate (struct and class) wrapper types such as a bounded numeric types and unit of measure types.

It’s operator overloading can define overloading for all unary or binary operators in one templated definition of op{Unary,Binary} or selectively for a set of operators through a single template restriction.

Has Rust something similar?

If not are there plans for adding similar expressiveness features to operator overloading?


#2

Operators can be overloaded using traits from std::ops and I think the Deref trait handles implicit conversions, but it’s rarely used, except in the case of pointer types, and other (very) generic wrappers. I don’t think people would use it for units-of-measure types.

I’m pretty sure you can’t define unary and binary operators in one definition.


#3

Deref used for the described case is generally considered an anti-pattern. Also, as Rust avoids implicit conversions, you’d still have to specifically ask for the conversion.

The example from the D documentation, ported to Rust, would end up with a lot of stars:

use std::ops::Deref;

struct S
{
    x: i32
}

impl Deref for S {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &self.x
    }
}

fn foo(i: i32) -> i32 { return i * 2; }

fn main()
{
    let s = S { x: 7 };
    assert_eq!(-7, -*s);  // i == -7
    assert_eq!(15, *s + 8);
    assert_eq!(14, *s + *s);
    assert_eq!(16, 9 + *s);
    assert_eq!(14, foo(*s));
}

Implementing Add here would make more sense.

Operator overloading works in general, note that the Fn familiy of traits cannot be implemented currently, though. (without the use of a feature on nightly)


#4

Deref used for the described case is generally considered an anti-pattern.

Yeah, that’s why I said:

[It’s generally only used] in the case of pointer types other (very) generic wrappers. I don’t think people would use it for units-of-measure types.

I’m pretty sure it’s not an antipattern to use Deref for generic wrappers that accept anything, such as pointer types, like in rust-gc.

Also, when does it look like variadic generics will land? Implementing function types sounds like a pretty important use-case to be unstable after all this time.


#5

That’s definitely the right place to use Deref.

I would say that they still take some type, but I don’t see where variadic generics are needed for Fn traits?


#6

The feature is apparently unstable because stabilization implies extended support, and according to nagisa (https://github.com/rust-lang/rust/issues/29625#issuecomment-240585954),

uncertainty about Args being the right thing given the possibility of variadic generics coming along around 2030 is the most major reason this is unstable.


#7

Sounds interesting.

Are you saying that the batch-operator overloading I referenced will be possible in Rust soon?

What nightly feature are you referring to?


#8

No, batch overloading isn’t part of that. I just implied that you cannot implement the function call operator () currently.


#9

No, there’s no build in solution for batch overloading of operators. I’d argue that you can get similar effects by crafting a custom macro. In fact, Rust standard library generates a lot of impls for primitive types via macros (an example).

What surprises me is that there is no crate with such macro on crates.io… I would very much liked to create primitive wrappers with run-time (debug) contracts (think NaN-free f64).

Another thing to keep in mind is that there are a host of inherent methods defined on primitive types, so if you really want your type to look like a primitive one, you might want to provide those impls (which are of course macro generated in stdlib).


#10

Do you have any sample code of how this would look like in D ?

I’m currently writing a unit of measure crate in Rust: simple_units and I’m curious to see how other programming languages implement this.


#11

The lord of macros DanielKeep has a newtype_derive crate which provides a lot of these, but they’re all for individual traits, so mass derivation is still a bit noisy.

// newtype_derive's docs say it can be used on stable without
// custom_derive, but then usage is even more verbose
#![feature(custom_derive)]

#[macro_use]
extern crate custom_derive;
#[macro_use]
extern crate newtype_derive;

// Note: If you want the binop impls on borrowed types as well
// like the primitives have, use NewtypeXyz(*) instead of NewtypeXyz
custom_derive!{
	#[derive(NewtypeAdd,NewtypeSub,NewtypeMul,NewtypeRem,NewtypeNeg)]
	#[derive(NewtypeAddAssign,NewtypeSubAssign,NewtypeMulAssign,NewtypeRemAssign)]
	#[derive(Copy,Clone,Debug,Eq,PartialEq)]
	struct Int(i32);
}

fn main() {
	let i = Int(4);
	println!("{:?}", i + Int(5));
//	println!("{:?}", i + 5i32);  // error
}

https://crates.io/crates/ordered-float

extern crate ordered_float;
use ordered_float::NotNaN; // also OrderedFloat, which sorts NaN last
use std::collections::BTreeSet;

fn main() {
	// implements Ord
	let set1: BTreeSet<_> =
		vec![1f64, 2f64, 3f64].into_iter()
		.map(|x| NotNaN::new(x).unwrap())
		.collect();

	// implements math binops
	let set2 = set1.clone().into_iter()
		.map(|x| x + NotNaN::new(1.).unwrap())
		.collect();

	println!("{:?}", (&set1) & (&set2));  // => {NotNaN(2), NotNaN(3)}
}

A brief look through the implementation however and I already see a couple of glaringly obvious bugs, which reflects the lack of active community interest:

fn main() {
	let zero = NotNaN::new(0.).unwrap();
	let infinity = NotNaN::new(1./0.).unwrap();
	let nan = zero * infinity;
	println!("{:?}", nan);  // => NotNaN(NaN)
}

There is also ord_subset (formerly under the much cooler name almost_ord) which tries to generalize the idea of “types with a few bad eggs”. Personally, however, I prefer the strategy of just panicking on partial_cmp failure; this strategy supports types with multiple totally ordered subsets. (such as the partial order on mathematical sets obtained by defining <= to be “subset of” )

I think the rust community in general is still trying to work out the best way to address these common needs.


#12

So I took my own stab at a derivation macro which tries to compress all or most of the derivations somebody may need into one.

As it turns out, there are problems. (surprise!)


1. There are many types of impls you may want to derive.

pub struct Foo(i32);

// the plainest choice...
impl Add<Foo> for Foo { ... }

// but you might want these as well...
impl<'a> Add<&'a Foo> for Foo { ... }
impl<'b> Add<Foo> for &'b Foo { ... }
impl<'a,'b> Add<&'a Foo> for &'b Foo { ... }

// In some cases, maybe you'll want these, too. I won't judge:
impl Add<i32> for Foo { ... }
impl<'a> Add<&'a i32> for Foo { ... }
impl<'b> Add<i32> for &'b Foo { ... }
impl<'a,'b> Add<&'a i32> for &'b Foo { ... }

// Aw man, don't forget the assign traits!
impl AddAssign<Foo> for Foo { ... }
impl AddAssign<&Foo> for Foo { ... } // NOTE: see point 2
impl AddAssign<i32> for Foo { ... }
impl AddAssign<&i32> for Foo { ... } // NOTE: see point 2

You see how the number of options explodes with each thing we add. Generally, speaking, I think what we want is often some sort of direct product of possibilities, and we would want a derivation macro to be able to provide all of these dimensions.

2. We must avoid generating invalid impls.

Two of the impls above have been specially marked with a note. These impls don’t actually exist on primitive numeric types. Phooey, huh? (in general, an impl will require Clone)

In general, invalid impls are a big problem because we have to avoid generating them. And we can’t just tell a macro to generate “whatever works;” there’s no way for it to know what does! Unfortunately, where clauses cannot provide us with an easy way out, because bounds on fully monomorphized types are checked at compile time:

pub struct Bar(i32);
pub struct Foo(Bar);
use ::std::ops::Add;

// compiler says "not a chance."
// error[E0277]: the trait bound `Bar: std::ops::Add` is not satisfied
impl Add for Foo where Bar: Add<Bar,Output=Bar> {  }

3. The Rust macro system is… the Rust macro system.

You know that feeling when nothing at all in the final product looks anything at all like your original design? macro_rules! is the pure distilled essence of that feeling 24/7, no refunds.

So yeah. I wrote a macro which does “x”… but look at it even slightly funny and the facade shatters completely. Also, as an implementation detail, it tickle tortures kittens on each compile. That said, it seems to work alright…


Anyways, it’s called newtype-ops and it’s the first crate I’ve actually bothered publishing (not because I’m at all confident in its implementation, but rather to take advantage of docs.rs). I’ve already switched one small project over to using it, which now implements all of its operators in a single line:

newtype_ops!{ {[Frac][Cart]} arithmetic {:=} {^&}Self {^&}Self }

Aside from the many documented flaws, I’d like to know what use cases people have for which the macro doesn’t suffice. Understanding the problem space is a necessary part of creating a better solution, and I feel as though I significantly underestimated the scope of the problem initially…