Working around the orphan rule

I'm using uom to encode physical units into Rust's type system, replacing Rust's fundamental numeric types in my code with uom types which contain information about the physical quantities that those values represent, in order to eliminate physical-type errors at compile time. It's great ... in isolation from the rest of the Rust universe.

The problem is that there is a bazillion traits out there in which the trait's author provides implementations for the fundamental numeric types. Replacing fundamental types in my code with uom-wrapped ones, makes me lose those trait implementations, and the orphan rule prevents me from supplying them, which causes the introduction of uom into my code, to stop my code working with most third-party crates.

I could wrap the uom types in my own, to implement the trait, but there are soooo many uom types (admittedly, there's quite a lot of generics in there) and they implement soooo many methods, that this seems like an intractable task, and I also worry that my newtypes will make it more difficult to work within uom itself.

What techniques are there for mitigating these sorts of problems?

I think the biggest technique for dealing with this sort of stuff is realising you might not need every trait implementation to work for uom types.

Often by narrowing the scope you realise it's not as much of an issue or you've simplified things enough that a simple newtype is sufficient.

No, I don't need every trait that has been implemented for f32 etc. but I do need the ones that are used in the crates that I use in my code.

My approach has been to write converters between the uomless types in my code and uomful types with which I want to replace them. Then I take some component of my code and replace the uomless types in it with uomful ones, convert between uomful/uomless at the boundary, and ensure that all my tests pass. Commit. Rinse. Repeat.

In this way uom is slowly spreading through my code, and as it does so, I keep hitting parts where I can't add uom, because it turns out to depend on some trait that is implemented for the plain numbers but not for uom. So I'm ending up with uom-free islands in my code (where conversions have to be done at the boundary), which perhaps isn't as bad as it seems to me at first blush, but it's certainly less than ideal.

In the end, doing nontrivial physical simulations using bare floats and ints is like programming in a typeless language. So it bothers me that there are all these regions in my code where I have to live without a type system.

One general technique is to provide the trait implementations in uom behind a feature gate. But I assume you've already considered this option since uom uses it for serde's traits.

Could you give a few examples of traits where you're running into this difficulty?

... which is an option available only to the authors of uom.

1 Like

…somehow I got it into my head that you were the author of uom. Sorry!

For traits that are widely used, the actual author might be amenable to adding such feature-gated impls (as they already have for serde's traits). I'd expect std::iter::Sum and nalgebra::ComplexField to meet that bar, at least.

1 Like

uom implements Sum already, for some things anyway.

An alternative to new types is a new trait or a few, but that may or may not reduce how much glue you have to write depending on how easy it is to blanket-implement the traits you're looking at.

An alternative to uom implementing foreign traits is for the owner of the foreign trait making their implementations generic (using some shared framework like num_traits) where possible. (Or implementing for well-known foreign types directly.)

1 Like

This isn't a great answer, but you could use make_units! from dimensioned, which is a crate that serves the same role as uom, which would define the unit types in your own crate, where you could implement any traits you want.

As I said, not a great option, but it is an option.

2 Likes

Indeed. I already offered to submit a PR for implementing std::iter::Sum in uom, but it turns out that ...

It seems that I failed to spot a lone & in the deluge of noise in an error message, and, in the context of repeatedly bumping into unimplemented traits, jumped to the wrong conclusion too quickly.

Indeed. After posting, it had occurred to me that I could use this approach in uom which also has macros for creating new systems of units, although the ones in dimensioned seem much simpler to use.

I forget why I settled on uom rather than dimensioned. I think I discovered it first and just stuck with it. Your comment prompted me to take another look at dimensioned. It tickles me to see that the three most recent open issues relate to three of the four traits I listed above.

Perhaps dimensioned is better suited to my needs than uom. In particular, there doesn't seem to be a good solution for creating derived units from existing ones in uom (while dimensioned has derived in dimensioned - Rust) and I can see this shortcoming becoming very irksome.

I'd even expect Sum and other traits from standard library to be available unconditionally (or whenever the std is available, if the crate is no_std), if they make sense for the type. The main reason for feature-gating in this case is AFAIK to not include the unnecessary dependencies, and std is, well, often necessary.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.