I see little to no benefits of introducing such "traits" into a language having duck-typing in templates. It may sometimes improve code readability (slightly), but with more boilerplate code for traits handling in exchange. And the result machine code will be identical to code with no traits at all (with only duck-typing).
C++ is a good example of this. It had duck-typing in templates for decades. Then some sort of trait-like mechanism named concepts has been introduced. The only benefit of them seems to be slightly better error messages if someone misuses a template class/function.
Maxatar 8 hours ago [-]
>The only benefit of them seems to be slightly better error messages if someone misuses a template class/function.
The error messages are arguably worse with concepts than without them but it's really just a trade-off. Without concepts you got long "stack-traces" of raw syntax errors that could dig deep into nested functions. With concepts you get long stack-traces of concept failures where concept A fails because concept B fails because concept C isn't satisfied and then you need to dig through this chain of concepts to figure out what's actually needed.
Traits in more principled languages like Haskell/Rust don't have this issue whatsoever because traits are semantic requirements in those languages while concepts in C++ just perform a "will it compile" kind of check.
The primary benefit of concepts in C++ is overload resolution, they allow you to write multiple functions with the same name, and same number of parameters, but where the types of the parameters are required to satisfy certain properties.
The compiler selects the first overload for contiguous ranges such as std::vector and the second for non-contiguous ranges such as std::list.
tialaramex 18 hours ago [-]
I'm not a Zig expert but it looks as though the intent is that duck-typing is not a core part of this idea. Having traits allows Carol to implement Ben's Teleport trait for Alice's Goose.
With duck-typing Carol needs to either prevail upon Alice to make you a teleporting Goose, or make her own "CarolsTeleportingGoose" wrapper type which implements the teleporting functions.
mrkeen 8 hours ago [-]
Traits/typeclasses tend to disallow your three-part scenario. It's called the orphan instance problem.
The "Goose teleports" code needs to go in Ben/Teleport.src, or Alice/Goose.src. Otherwise the compiler doesn't know which implementation to use if there are multiple.
Scala (and I think Ocaml) have mechanisms for selecting which one to use. In Haskell you'd use a newtype wrapper.
cowlevel 7 hours ago [-]
Concepts are predicates for types, that is, functions from types to booleans. Everyone was already defining templates like is_contiguous_range<T>::value so much that they decided to give it first-class status in the language.
The really cool part about concepts (unless you're writing the compiler) is actually requires expressions, which can replace a lot of SFINAE.
dnautics 10 hours ago [-]
> The only benefit of them seems to be slightly better error messages if someone misuses a template class/function.
Seems like a pretty valuable benefit?
indiosmo 19 hours ago [-]
Concepts are great for overload sets where previously you needed enable_if gymnastics. Also useful for if constexpr.
If you do template metaprogramming it makes your life a lot easier.
armchairhacker 8 hours ago [-]
The benefit to C++ is that it’s actively trying for as many features as possible, keeping out of reach full support from any compiler. Maybe GPT-8 will be the first true C++ expert, built by the first superintelligence GPT-7.
kooi 4 hours ago [-]
Coming from Rust:
Why is does implementation still have a the generic T? Should it not be concrete on i32?
/// i32 forms a sum reducer under 0 and addition
const sum_trait = impl(Reducer(i32), struct {
pub const empty = 0;
pub fn combine(x: T, y: T) T { return x + y; }
});
axelmagn 3 hours ago [-]
That was a mistake on my part. Thanks for catching. Fixed.
stock_toaster 8 hours ago [-]
While a well written article, and certainly intellectually interesting, I'm not sure I'd be excited to run across this level of indirection in a codebase if I were debugging an issue.
mrkeen 8 hours ago [-]
Complexity budget and all that. But I'd sooner throw out inheritance than traits.
After reading the page I was left with the question of isn't that dynamic inheritance? The seems like dynamic runtime.
s_tec 9 hours ago [-]
In Zig's case, it's compile time. An expression like `foo.bar(1, 2)` can only compile if `foo` has a method named `bar` with the right number of arguments. In a duck-typed system, `foo` can be any type of value you like, as long as it "fits" the shape.
In contrast, many languages require you to describe the shape up-front, and then constrain `foo` to implement that shape. For instance, in Rust we would have to write something like `trait Bar { fn bar(a: u32, b: 32); }`, and then say something like `foo: T where T: Bar`, before we're allowed to call the `bar` method.
Both are compile-time safe, but one requires more ceremony (but is more explicit), while the other is simpler (but you might have to guess the requirements).
Jtsummers 9 hours ago [-]
> isn't that dynamic inheritance? The seems like dynamic runtime.
Duck-typing can be implemented with static checks, and doesn't require inheritance to be involved at all.
zahlman 8 hours ago [-]
It's ad-hoc inheritance (but really subtyping more generally).
The dynamically typed version is dynamic inheritance, or really just dynamic attribute lookup.
The statically typed version is "generic programming" with templates. See for example the way that C++ standard library sort works.
There is actually a proposal to make Rust traits desugar behind the scenes to basically this: https://goals.rust-lang.org/2026/dictionary-passing-style-ex...
Rust's creator originally wanted an OCaml-style module system instead of traits (https://graydon2.dreamwidth.org/307291.html), and interest persists today (https://www.boxyuwu.blog/posts/an-incoherent-rust/).
C++ is a good example of this. It had duck-typing in templates for decades. Then some sort of trait-like mechanism named concepts has been introduced. The only benefit of them seems to be slightly better error messages if someone misuses a template class/function.
The error messages are arguably worse with concepts than without them but it's really just a trade-off. Without concepts you got long "stack-traces" of raw syntax errors that could dig deep into nested functions. With concepts you get long stack-traces of concept failures where concept A fails because concept B fails because concept C isn't satisfied and then you need to dig through this chain of concepts to figure out what's actually needed.
Traits in more principled languages like Haskell/Rust don't have this issue whatsoever because traits are semantic requirements in those languages while concepts in C++ just perform a "will it compile" kind of check.
The primary benefit of concepts in C++ is overload resolution, they allow you to write multiple functions with the same name, and same number of parameters, but where the types of the parameters are required to satisfy certain properties.
For example:
The compiler selects the first overload for contiguous ranges such as std::vector and the second for non-contiguous ranges such as std::list.With duck-typing Carol needs to either prevail upon Alice to make you a teleporting Goose, or make her own "CarolsTeleportingGoose" wrapper type which implements the teleporting functions.
The "Goose teleports" code needs to go in Ben/Teleport.src, or Alice/Goose.src. Otherwise the compiler doesn't know which implementation to use if there are multiple.
Scala (and I think Ocaml) have mechanisms for selecting which one to use. In Haskell you'd use a newtype wrapper.
The really cool part about concepts (unless you're writing the compiler) is actually requires expressions, which can replace a lot of SFINAE.
Seems like a pretty valuable benefit?
If you do template metaprogramming it makes your life a lot easier.
Why is does implementation still have a the generic T? Should it not be concrete on i32?
/// i32 forms a sum reducer under 0 and addition const sum_trait = impl(Reducer(i32), struct { pub const empty = 0; pub fn combine(x: T, y: T) T { return x + y; } });
https://en.wikipedia.org/wiki/Duck_typing
After reading the page I was left with the question of isn't that dynamic inheritance? The seems like dynamic runtime.
In contrast, many languages require you to describe the shape up-front, and then constrain `foo` to implement that shape. For instance, in Rust we would have to write something like `trait Bar { fn bar(a: u32, b: 32); }`, and then say something like `foo: T where T: Bar`, before we're allowed to call the `bar` method.
Both are compile-time safe, but one requires more ceremony (but is more explicit), while the other is simpler (but you might have to guess the requirements).
Duck-typing can be implemented with static checks, and doesn't require inheritance to be involved at all.
The dynamically typed version is dynamic inheritance, or really just dynamic attribute lookup.
The statically typed version is "generic programming" with templates. See for example the way that C++ standard library sort works.