Closed nical closed 7 years ago
Thank you for sparking this discussion, @nical !
I feel that traits are on the entirely opposite side of the problem. Traits describe behavior, where the original idea behind mint
was to standardize the content.
Suppose you use a library that exposes something satisfying mint::Vector
trait. Now you want to process it with nalgebra
. What do you do?
The solution I'd prefer is to have mint
describing types only (plus a few From
/Into
conversions). Then we may have another crate (mint_traits
?) with standard traits like Transform3
that are implemented for the standard types as well as (potentially) native types of other libraries.
The idea would be that euclid or naglebra would implement ways to make conversion to mint traits easy (like From
).
An interface that wants to be "more standard" could be generic over the mint trait or some trait that describes an easy conversion between the mint trait and nalgebra's structure.
Something inspired from this: https://deterministic.space/elegant-apis-in-rust.html#custom-traits-for-input-parameters
For example if you look at the lyon_bezier crate, all of the members are publicly exposed so it can't expose an API with standard types unless all members are themselves of that standard type and then I go from having a library that is very nice to use with euclid to one that is a bit awkward to use with every library (not very appealing from my point of view as a user and maintainer of the crate) and more annoying to maintain.
However, the bezier curve segments could maybe be generic over a Point2<F32>
trait that provides the interface I need to do the bezier math. It would make the crate a bit more tedious to write (generics all over the place), but it would seamlessly integrate with the other lyon crates that use euclid or any other math crate that implements the mint traits.
If you expose mint
types in that (wonderful!) bezier crate and have a separate library with standard traits that happen to be implemented for mint
types, then it's internals are going to be pretty and easy to maintain.
Kinda, but having mint declare data structures and logic (the traits and implementation of the traits), is equivalent to creating a new full featured math library. At this point mint isn't lighter wait than euclid or the others.
Plus, for lyon_bezier to be nice to use with the rest of the lyon crates which heavily use euclid, it needs to store euclid objects (be it explicitly or generically by storing a trait that euclid points implement). If a bezier segment stores its points generically using traits (not types), I can seemlessly integrate with the rest of my code, whereas with mint types I have to jungle between two ways to represent the same thing.
is equivalent to creating a new full featured math library
I was just about to put it into README ;) But yes, that is true. However, the point can be somewhat softened by:
mint_traits
. Edit: technically, traits and their implementations would deserve separate (dependent) features.or lyon_bezier to be nice to use with the rest of the lyon crates which heavily use euclid
It would be as nice to use if the rest of lyon crates was using mint
;)
One trouble with traits is that the set of potentially useful operations is much larger, and has a much larger set of design choices to be made, than the set of potentially useful interface types. Mint's current direction allows it to be lightweight and minimal, and imposes no significant design constraints on other libraries. lyon_bezier, for example, would play adequately well with mint by merit of suitable From
/Into
implementations existing for euclid types, rather than having to be invasively refactored into generic, less-predictable, slower-to-compile code.
The traits don't need to expose the entire set of potentially useful operations. In fact it could be as small as the set of things you can do out of a mint type, so x()
, y()
, z()
and new(x, y, z)
are enough to provide the same service as mint types if you want to keep it small.
If as a user of the trait you want to add missing operations, you can simply do it, even in a separate crate. For example, injecting a square_length
method to implementors of a trait Vector2<T>
that only exposes x and y:
trait SquareLength<T> {
fn square_length(&self) -> T;
}
impl<V, T> SquareLength<T> for V
where
V: Vector2<T>,
T: Add<Output=T> + Mul<Output=T>
{
fn square_length(&self) -> T {
self.x() * self.x() + self.y() + self.y()
}
}
With mint types, lyon_bezier would have to either copy out every member into an euclid type before doing anything and then back after operations are performed (the methods are on average 5-6 lines long so it is already quite invasive), or have from/into called every time a member is used which is not worth the pain. I agree with the problem that generics worsen compile times, but I don't see what's less predictable about accessing x through a trait method rather than indexing an array.
Another advantage of traits: Euclid has a great feature that all vectors and transforms are tagged with units (or spaces depending on how you choose to think of it) in a very ergonomic way. This lets us avoid adding a vector in screen space to a vector in world space, CSS stacking-context space (or one of the dozen other coordinate systems we have to jungle with). This has allowed us to find a lot of bugs (enough bugs to want to maintain a dedicated vector library), and has prevented tons more. mint types basically lose all of the benefits of this feature, because you need to convert into a unit-less vector type at the API boundary where you are exposing your standard types, so you are dropping all of this important type information. traits can easily let us support this because vectors with different units are different types and so different implementations of the same traits. The trait system naturally won't let you assign a vector in a space to a vector in another space, even if they are expressed generically.
It would be as nice to use if the rest of lyon crates was using mint ;)
you don't need to create a new crate if the goal is that everyone switches to it and only uses it (realistically people won't). If that's what we are after, we'd be much better off saying that cgmath is the standard (I'd say euclid but I guess cgmath already has more users), and trying to convince other math libraries to make it easy to interoperate with cgmath.
There's been discussions about providing standard data structures for common math types, but the interop question can also be addressed through traits. What would be the pros and cons of mint defining traits instead of data structures?
the kind of traits I am thinking about could be like:
Or could look very different, I don't know. I just want to get the discussion started. I have a feeling that I would prefer standard traits over standard data structures, probably because adding new data structures next to the ones I am already happily using feels redundant and cumbersome, but I'd need to explore this more to have an idea of how useful that would be.