Closed bitshifter closed 2 years ago
Hey @bitshifter ,
that are some good thoughts. I think that the core the library is well designed for the current stable state of rust. I like the direct access to the storage values.
As you said, the macros are one big part that could be improved. But for me improving the macros is not all about the code readability. I'm missing the generic vectors that "magically" work. I would like a u8 vector, or custom type vectors.
One way, to achieve this, would be to move and separate each vector method (e.g. abs()
, clamp()
, is_nan()
, etc.) into traits.
This could reduce the heavy macro usage by using generic implementations for known scalar types (SIMD as well).
I'm not sure about Rust best-practices, but I would group traits into definition and impl files depending on the functionality.
glam has intentionally avoiding being a generic library. Instead it has focused on the most common use case (f32
) and has expanded a bit from there due to demand to support some other primitive types such as f64
, i32
and u32
.
It's not clear from you example how this approach would work using a storage type of __m128
with a public scalar type of f32
as the traits only have one generic type T
but surely two would be required?
In short, I think it is difficult to achieve both using explicit SIMD usage in combination with a generic interface and I think it would complicate the public interface greatly so it's not a direction I've ever pursued.
Internally I do use a lot of traits to implement functionality and to get some code reuse, however it is much more complicated and you tend to run into confusing compile errors. It has always been a goal of glam to provide a simple interface which doesn't impose this kind of complexity on users, so all of this complexity is internal only for now except for the case where users want to view the source.
Another problem of implementing everything via traits is the documentation. You can no longer view the docs for Vec3
and see all the methods it implements. Documentation is on the trait, not the struct so it's much harder for users to find all the methods a struct implements. You need to find the trait that implements the method you want and then find if it's implemented for the type you are interested in. cgmath is a good example of this issue, try and find the documentation for dot
for https://docs.rs/cgmath/0.18.0/cgmath/struct.Vector3.html. You need to work out what trait has dot
(there are multiple) and then work out which trait is implemented for Vector3
.
After two weeks of experiments to implement a generic ndarray lib that has both const & dynamic structs (with the new const-generics) and views on part of the data, I must confess that generic trait implementations are the worst abominations that exists in rust (both in std and libs) and should be avoided at all costs. :goberserk: I learned the hard way.
And using macro impl for many type specifications of a generic struct can result in a docs monster. Just take a look what I found today: https://rust-lang.github.io/packed_simd/packed_simd_2/struct.Simd.html Have fun. xD
~~ Brainstorming mode ON ~~ I get your point on the complexity and documentation issues of the "mini traits" approach. Maybe having a big generic trait with most methods could also reduce the duplicated docs between each Vec struct? It could keep most common methods on a single docs page? E.g.: Expose the Vector / Matrix trait?
I think the best compromise is to write a code generator, similar to what ash does to generate bindings from vk.xml
. The resulting code may have duplication, but the source data to generate that code will not.
Out of curiosity.
What about approach similar to num_complex
crate. where we have generic struct but implementation is generic or specific to type:
pub struct Complex<T> {
/// Real portion of the complex number
pub re: T,
/// Imaginary portion of the complex number
pub im: T,
}
impl<T> Complex<T> {
}
impl<T: Clone + Num> Complex<T> {
}
impl<T: Float> Complex<T> {
}
I agree that finding the source code now is a little bit difficult. VSC jumps to the macro for example. Could this work here?
@klangner glam could not support simd with that approach as it's a different type to T
.
I took the templating approach using Tera. Merged to main in #294.
Internally glam got a lot more complex when support for other primitive types were added, e.g.
f64
,i32
&u32
.There are a couple of sources of complication:
core
module which defines traits which are implemented for different storage types, which handles the actual implementation for vectors, quaternions and matrices for different primitive types and also SIMD architectures (currently just SSE2)The purpose of both of these is to avoid a lot of code duplication between different types. Avoiding duplicating comments is almost as significant a motivator as avoiding code duplication. The old simple way that glam was written felt like it would not scale, at least not while the library is still evolving.
The macros are generally what users run into when they want to read glam source for some reason although it's likely the next hurdle will be the core crate traits.
One thought on how to make the source more readable is to use a templating language to generate the glam source instead of using Rust macros. The generated source would then be committed and that is what users would compile and view. Contributors would ideally update the template files, however PRs could be made to the generated source and then later ported back into the template generation.
The core crate feels more necessary still, I don't see an easy way to avoid that, but at least without the macros it might be easier to follow.
A few people have suggested https://github.com/dtolnay/cargo-expand for code gen. It's meant as a debugging tool but maybe it will do the job, or could be modified to support this use case.
Another suggestion was to look at rust-analyzer and
cargo xtask generate
.