Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Agreed. If I could retroactively alter Rust's design, I'd remove non-marker traits with methods, and instead have functions and data structures take method or vtable pointers. In case of static dispatch, the method pointers would be passed as compile-time const generic parameters. This allows picking different ordering operators for different trees or sorts defined on the same type, instead of requiring wrapper types defining custom comparison operators, or having individual types or functions add comparator parameters in an ad-hoc fashion (https://doc.rust-lang.org/std/primitive.slice.html#method.so...).


> Agreed. If I could retroactively alter Rust's design, I'd remove non-marker traits with methods, and instead have functions and data structures take method or vtable pointers.

Do you mean single-method traits? Because it already sounds like a pain in the ass for Hash or Ord, but it sounds absolutely horrendous for Iterator.

> This allows picking different ordering operators for different trees or sorts defined on the same type, instead of requiring wrapper types defining custom comparison operators

Sounds like an aggressive worsening of the default, and a solution in search of a problem really: if that’s a really common need you could have a generic wrapper rather than a specific one (like Reverse).

Your complaint about ad-hoc comparator seems odd as your method would essentially require always doing that, unless I’m misunderstanding what you mean.


> Do you mean single-method traits?

No, the pointer to vtable can point to a struct value containing multiple function pointers.

> if that’s a really common need you could have a generic wrapper rather than a specific one (like Reverse).

The way I see generic wrappers, it's not the integer that's reversed, but the data structure defined over it, and requiring wrapping/unwrapping reversed integers/etc. is ceremony in the wrong location. With specific wrappers, you could argue it often has semantic significance representing how your specific type isn't just a regular integer, but a Uid or such.

> Your complaint about ad-hoc comparator seems odd as your method would essentially require always doing that, unless I’m misunderstanding what you mean.

My change to Rust would make all trait-bounded generic functions consistently allow supplying a trait implementation separate from the underlying type, effectively transforming this pattern from an ad-hoc change to a language construct more flexible than globally coherent traits. From there on, you could layer on optional ergonomic improvements, like implicit trait implementations or optional default traits (while keeping the first-class ability to supply any trait implementation desired).

Is the idea practical? Maybe. Currently structs can't supply default field values, even though traits can supply default methods. But the hard part is that default trait methods are effectively templates, not functions:

  trait A {
      fn f() -> i32;
      fn g() -> i32 {
          Self::f()
      }
  }

  struct T1;
  impl A for T1 {
      fn f() -> i32 {
          1
      }
  }

  struct T2;
  impl A for T2 {
      fn f() -> i32 {
          2
      }
  }
To preserve the current semantics (a debatable goal), instead of building method table structs out of free functions (the most minimal and elegant implementation, though less ergonomic) like `static T1_A = A<T1>{...}`, you'd still have to keep a compromise syntax of some sort, closer to current `impl` but allowing you to optionally specify a name for the impl.

Another issue to be addressed is that global coherence can serve as a lint against accidentally implementing the same trait twice for a type (as opposed to deliberately providing two different implementations with different names the user can select.)


The ergonomics of this are really bad without some kind of implicit parameters and trait resolution. Without trait methods, ad-hoc overloading becomes impossible. You would have to explicitly specify what + operator and what == operator you use everywhere. After all, there could be multiple different additions or comparisons defined for a type, so which one do you mean by `+`?

Another problem is that a data structure's invariant can depend on the trait implementation. For example a tree is balanced with a comparison. This means that the comparison has to be stored in the struct, with the corresponding runtime overhead. With traits you know that there is only a single implementation for any particular type, so if the trait v-table is an argument to every method call there is no risk of different implementations being used at different times.


> This means that the comparison has to be stored in the struct, with the corresponding runtime overhead.

The runtime overhead is solvable in principle by making the comparison a const-generic parameter of the data structure type. But to do this properly requires dependent types, because the type of that const-generic parameter is taken from an earlier parameter in the same definition. It's a dependent sum type, sometimes called a dependent record.


From my understanding, compile-time dependent types (which is all that's needed to mimic statically dispatched generics) are easy, and C++ templates support them (I haven't tried Rust const generics but I hear they're quite limited). Runtime dependent types are much less common, and (given my limited understanding of dependent types) I could describe trait objects (&dyn Trait) as a dependent pair (ptr, Trait-shaped vtable containing *fn taking ptr's type) (though rustc hard-codes support for trait objects, and its job is easier since it never lets ptr change runtime type while its target is used for a &dyn Trait).


Rust's const generics today are limited to the built-in integral types (so, char, bool, and the various sizes of signed or unsigned integer) and the constant must actually be an obvious constant, so e.g. a literal, or something you could assign to a const in Rust. So yes that's much less flexible than C++.

Some of the flexibility in C++ here is desirable, much of it is either wanking or outright dangerous. C++ will allow a float generic for example, which is obviously a footgun because good luck explaining why Thing<NaN> and Thing<NaN> are different types...

Rust expects to sooner or later ship Const generics for user-defined types, but to exclude the nonsense of "float generic" the likely rule goes like this: Your const parameter type must derive PartialEq and Eq. It won't be enough to implement them yourself as your implementation might be nonsense (e.g. misfortunate::Maxwell implements Eq but no Maxwell is equal to anything, including itself) you'll need to use the derive macro which promises the compiler these things actually have working equivalence so that Thing<A> and Thing<B> are the same type if A and B are equivalent.


>Some of the flexibility in C++ here is desirable, much of it is either wanking or outright dangerous. C++ will allow a float generic for example, which is obviously a footgun because good luck explaining why Thing<NaN> and Thing<NaN> are different types...

It's not nonsense, there are legitimate use-cases for floating point template params. A classic example is something like generateInlinedVersionOfFunction<someInt, someFloat>() that generates a class or function with some particular values inlined for efficiency. Using nan as a template parameter is stupid, but it's not especially more dangerous than using nan in general, since any issues specific to its compile-time usage will manifest at compile time, not runtime.

It does nobody any favours to just dismiss advanced features Rust doesn't support as dangerous and unnecessary just because most Rust programmers haven't come across a use-case.


> It does nobody any favours to just dismiss advanced features Rust doesn't support as dangerous and unnecessary just because most Rust programmers haven't come across a use-case.

I think requiring decidable equality for const generics is a fine approach for a limited MVP like what Rust is going with at present. Sure there are more general settings but they would need dependent types and the user would have to write proofs of type equality/apartness anytime the issue came up in a compile. Seems like a bit of a non-starter for now, even though it's effectively what enables non-trivial logical features like homotopy types.


> A classic example is something like generateInlinedVersionOfFunction<someInt, someFloat>() that generates a class or function with some particular values inlined for efficiency.

This smells very bad. Seems like a better choice is a macro, and I understand that in C++ the macros are awful and worth avoiding, but Rust has decent declarative macros for this type of work.

Do you have some real world examples?


Can't you do that in Rust with a macro?


You can always just convert your float to a bit-equivalent int and back for the type param if you really want it. I have never seen floats used for template parameters in the wild, other than party tricks like wrapping a float in a struct with an "epsilon" parameter used for the operator<. Which is all kinds of wrong.


AIUI, practical dependent types are always evaluated at compile time, but the difference with something like C++ generics is that the types can include references to program bindings that relate to runtime values. This means that dependent types can express proofs evaluated at compile time about the runtime properties of a program.


Why? If it’s a one-off occurence you can do it inside the closure, if it happens more often then surely a zero-overhead wrapper type is the ergonomic choice.




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: