Duck-type traits


#1

Imagine if traits could be duck-typed, like in go i.e. if you had impl T { fn foo(&self){..}} , T would automatically satisfy trait X { fn foo(&self); }. similarly if you had impl T { fn bar(&self){..}} T would automatically satisfy trait Y { fn foo(&self); fn bar(&self); } … This wouldn’t preclude explicitly saying impl X for T {...} aswell

The motivation: [1] Increase the amount of code that could be directly translatable between C++ and Rust. If you didn’t use trait namespaces, you could gather the impl’s for a type and roll a C++ class that is directly equivalent. And vica-versa, you could take a C++ class and its duck-type its’ methods into Rust traits.

[2] to reduce the amount of micromanagement and extra naming involved (my use case,implementing a vector maths library, comparing what it looks like in C++ and Rust… dealing lots of with indirection traits and accessors, and single function traits). Of course coming from C++, I find the idea of associating methods with a trait for generics (templates) slightly unnatural, because I’m used to duck typing already.

I gather when you do impl X for T { fn foo(&self)} ‘foo’ is in the namespace of ‘X’,? so you can impl Z for T { fn foo(&self)} … and you get both T.X::foo(&self), T.Z::foo(&self) ?

So that might be a problem - but you could always return to being more specific where needed. Otherwise you could encourage the community to create complimentary method names for traits that overlap in the same domains.

*If the whole signature was considered (function inputs & outputs…) would that would reduce the scope for clashes and ‘false positives’ ?

So the suggestion is that a trait looks for the function in its’ own namespace, then falls back to the namespace of the type.


#2
  • IMO any implicit thing [including duck typing] is bad.

  • You should check your text for grammer and understandability problems.

  • Perhaps you wanted to write “module” instead of “namespace”.


#3

-1. IMO any implicit thing [including duck typing] is bad.

bad in all situations ?

in game development, C++ is combined with Lua, for its dynamic, rapid iteration nature. its worth going through all the pain of interfacing for that. The same application has a need for multiple styles in different areas… the performance of C++ and the productivity of lua

Julia is designed for maths/science work, and it has ad-hoc overloading.

There’s nothing to stop you being more specific where needed, in my proposal.

Sometimes traits are helpful. Others they are uneccasery noise, distracting you from what you’re trying to focus on. ( I like to think in terms of functions, then types. ). You could have both.

People say ‘overloading is a misfeature’ because they don’t like searching the overloads… isn’t having the same named function in different traits, and having to hunt through traits just as bad?

it almost feels like managing #includes but on the level of individual functions


#4

How would we deal with Trait objects, if Traits aren’t Opt-In? I also worry about Traits that are meant to satisfy certain rules, or Traits that have no methods?


#5

I think it is a bad idea. Explicit traits also communicate a certain semantic meaning of a function. E.g. impl Add for Foo { fn add(… communicates very clearly to the reader of your source that you want to overload the operator +. That’s better then guessing whether the signature of the implemented function add matches the signature of add in the Add trait. Implicit traits would also give you the error message at the wrong place (calling site instead of at the implementation).

Even the python (AKA “the mother of all duck typing”) style guide state says: “Explicit is better than implicit.” and “Special cases aren’t special enough to break the rules.”

If you have to implement a lot of single Function traits (like for operators) this screams more for a macro than a change of the language. This would also fulfill “Beautiful is better than ugly.”.


#6

Implicit traits would also give you the error message at the wrong place (calling site instead of at the implementation).

this is only proposing optional declaration of a trait, I haven’t said anything here about trait bounds in generic code - those will still be error checked assuming the trait declaration. The caller still gets the same information - ‘can’t call this function because this function isn’t implemented for T (needed by this trait bound)’

Even the python (AKA “the mother of all duck typing”) style guide state says: “Explicit is better than implicit.”

but it is not black and white. if you are dealing with the unknown you have not yet established a successful pattern, so you need something that is easy to change. imposing the wrong structure is damaging. I’ve seen people go through many re-writes of source bases as they shuffle ideas on how classes should be arranged. This is the part of C++ I’m trying to escape. (the type of code people write before they learn that free functions are actually good)

this screams more for a macro its great that rust has macros… again, good for handling the unknown, but these can be their own form of noise, and off-putting to other users of your source base.

Something else that crosses my mind on this:- How closely could Rust and C++ interoperate. if your ‘methods’ were still based on parameter type information (even if you needed traits to access them in generics), would that open the door to easier interoperability with C++. (imagine if the name mangling matched exactly, and fn’s impls’d on Foo were the same as class Foo { …memberfunctions…}. If they’re in the trait namespace, you’ve closed that door (or added more barriers to adoption)

Apparently with the JVM , clojure and scala can be retrofitted into Java projects, because they can share many libraries. Similarly Swift stands to be hugely popular because it closely interoperates with objC . Imagine if Rust was closer to a ‘cleaned up, safer C++’, able to deal with the same interfaces.

If it forces an “all or nothing” reorganisation and re-write of your entire source base, then that is a barrier to entry.

I’m not suggesting that functions should never be impld’ form the trait - just that you should have the option.

the signature for add (or other operators) could be any (A,B)->C, because you can put information in types. e.g. adding two different fixed point shifts, or multiplying values with dimension checks. An error message could still tell you, “can’t use + because no suitable ‘add’ found. (closest candidate add(a,b,c) doesn’t match’)”


#7

This proposal would definitely destroy the distinction between the Eq and PartialEq traits, since Eq is an otherwise empty trait that just inherits PartialEq. Eq’s only difference is that implementers must guarantee stronger properties like reflexivity and transitivity, purely as an internal implementation detail. Duck typing can’t capture this; everything that impls PartialEq would be accepted as Eq.


#8

Eq and PartialEq traits, since Eq is an otherwise empty trait that just inherits PartialEq.

my proposal is that impl Eq for T { } and impl PartialEq for T { } would still be distinct T.Eq::eq and T.PartialEq.eq

its just Eq, and PartialEq would be satisfied if you independently implement `impl T { fn eq(&self,rhs)->bool

the trait first searches for a true implementation of the trait (which takes priority), then falls back to the ‘anonymous impls’ (the types’)

The anon impl of ‘eq’ would have to satisfy all traits.

(the way our use cases deal with NaN/partial/eq is that really our programs our invalid if NaN ever happens - they must never create it , by design - extraneous checks are unacceptable performance hazards, and failures due to NaN unacceptable behaviour - so basically our ‘floats’ really do satisfy Eq, Ord as well… because they are subsets of the float type. A debug build can swap in an error checking version of the float type that asserts it is never NaN.

the strict type safe way of expressing this would be to create yet another type, the ‘never NaN float’, just like pointers have the ‘never null’ assumption, even though 0000 is a bit pattern that it can hold. but the task can’t even fail if it generates a NaN - any check is an unacceptable runtime cost


#9

Eq is just one of many examples, I don’t really care about it specifically. Rust’s type system right now encourages types which make claims about implementation details. That’s the entire idea behind newtypes. Just because something is like another thing, doesn’t mean I want to accept it. Rust, in my mind, should favour correctness over convenience when necessary. We have a rich ecosystem of scripting languages for truly convenient programming. If you want Lua, just bind to Lua like everyone else.

The proposed MultiSet struct has an interface compatible with the Set interface, but I’ve argued that MultiSet should not be a Set, because given a Set I expect insert(x); insert(x); remove(x); contains(x) to return false. If duck typing allows someone to pass in a MultiSet where I expect a Set, that’s a failure of the type system, in my mind.

Similarly, I don’t want a String where I demand a SanitizedString.

I might accept ducktyping as opt-in. As in fn foo <T: Like<Set>> (set: T); but the default should definitely be strict typing, in my mind.


#10

I might accept ducktyping as opt-in.

I have proposed that duck typing only picks up anonymous impls - if you impl Set {} or impl MultiSet{} , these do not get picked up by each other. I have not suggested that type-bounds be removed.

. If you want Lua, just bind to Lua like everyone else.

but I don’t want lua.

I want something as fast and efficient as C++, with all the same low level control, but with cleaner syntax. And maybe a subset that suits being interpretted/dynamically executed aswell (just that efficiency is the priority with language choices). I want the quick and dirty coding style when experimenting and writing tests, and the ability to fully optimise anywhere. I believe this is possible. I do not believe that they are mutually exclusive ways of working.

there are cases where you write more lines of source than you’re expecting generated instructions. Sometimes all these extra annotations are just extra noise, getting in the way of the meaning … and the more you have to name, the more potential conflicts you get between libraries.

but I’ve argued that MultiSet should not be a Set, because given a Set I expect insert(x); insert(x); remove(x); contains(x) to return false.

couldn’t that just be fixed by defining a universal,unambiguous remove_all(x) remove_first(x) functions. and let the world fight it out over what the ambiguous remove() should mean.

Perhaps remove should only be supported for containers that only contain one, but clearly ‘remove_first(x)’ and ‘remove_all(x)’ could also be implemented as ‘remove(x)’ for the Set.


#11

Being able to “accidentally” implement a trait without realizing it (especially a trait from some other crate you have no knowledge of) seems like a footgun waiting to happen to me.


#12

[quote=“dobkeratops, post:6, topic:216”]Apparently with the JVM , clojure and scala can be retrofitted into Java projects, because they can share many libraries. Similarly Swift stands to be hugely popular because it closely interoperates with objC . Imagine if Rust was closer to a ‘cleaned up, safer C++’, able to deal with the same interfaces.[/quote]Decide what you want. Performance of C++ or runtime overhead of Java/ObjC. Note: You can’t interface with a class that does not exist because it was optimized away by the compiler. But this has actually has nothing to do with your proposal.

[quote]the signature for add (or other operators) could be any (A,B)->C, because you can put information in types. e.g. adding two different fixed point shifts, or multiplying values with dimension checks.[/quote]Well the signature of add is fn add<RHS, Result>(&self, rhs: &RHS) -> Result which means if you don’t pass self by reference it does not work. If it hast three parameters it does not work. This might seem trivial for add but it is not trivial to see for more complicated signatures.

[quote]An error message could still tell you, “can’t use + because no suitable ‘add’ found. (closest candidate add(a,b,c) doesn’t match’)”[/quote]Yes. But this is at the wrong place. This could happen after you compiled your library and delivered it to someone. Which means that you basically need unit tests for something that should have been caught by the compiler. I can’t imagine that this is anything anybody wants.


#13

Decide what you want. Performance of C++ or runtime overhead of Java/ObjC.

I want the performance of C++ , and removal of the problems orthogonal to that (header files, mutable default, ambiguity between method & function call syntax, closed classes). Productivity tweaks to reduce the number of cases where interfacing to Lua/C# is a win. And I’m inspired by these reference cases to see something that should be possible: The ability to closely work with language X, with modernised language Y. replace X=Java/ObjC/C++. Y=Scala/Swift/(hopefully rust…)

You can’t interface with a class that does not exist because it was optimized away by the compiler.

you can translate source to make equivalent descriptions for the inlining engines. Between Rust and C++ there is a very significant overlap where concepts have a 1:1 representation. This is where I want to be.

Do I want to isolate myself from established C++ sources ? hmm.

If it hast three parameters it does not work. This might seem trivial for add but it is not trivial to see for more complicated signatures.

With maths code the number of arguments is the easiest bit to get right. I think about what functions I want, then pick the data layout that suits those operations… everything centres on functions and data . Many representations re possible with subsets of operations making sense… overloading/polymorphism is the rule rather than the exception and traits seem to become extraneous, often just a clone of the name.

Naming is hard, but the plus side of overloading is you leverage the names you already created- and we already have IDE’s to search. Its headers that complicate it (this code will only compile if you included the right declarations first…)

Yes. But this is at the wrong place. This could happen after you compiled your library and delivered it to someone.

the best libraries are developed in an application then extracted, so you have the working application as an example of how to use it. The traits are still there to improve the error messages.

you could still re-order and compress the error message using the information in the trait. (that, basically , seems to be what traits achieve: - I’m not dealing with vtables. I do appreciate the elegance that they’re unified, sure, i do prefer rusts idea to multiply inheritance in the case where vtables are needed)

"can't call X because trait Y is not satisfied (because function Z isn't found"

"function Z not found for trait Y needed for call to X"

"trait Y not satisfied , because function Z not found, (needed for call to X,here)"

Whichever way it makes sense. The information is the same, ‘some piece of code is yet to be written’. This retrofit is going to be available in C++ - alone it’s not a big enough reason for the world to throw away billions of LOC, (or for me to reduce my productivity)

So this is where Rust creates the ambiguity for me. If I go back to C++ and wait for C++ modules & concepts I’ll get what I want… plus keep established tools and ability to work with established sources.

This would be a great shame, because Rust has a cleaner syntax and other tweaks that are really nice… (like the expression oriented syntax, immutable default, nice macro system).

The language engine seems powerful enough to handle both styles. Its just like a compulsory restriction prevents it.

Being able to “accidentally” implement a trait without realizing it (especially a trait from some other crate you have no knowledge of) seems like a footgun waiting to happen to me.

If all the type signatures have to match, i think accidental implementation would be rare enough not to worry. but we already have this hazard, and ways of dealing with it. IDE’s can trace all the overloads . People have the software they need written in C++, and they want these sources maintained and extended. Do we have to throw away all this code to get (a) immutable default (b) no headers, © expression based syntax (d) ADTs (e) safety checks for new code (f) extention methods (g) better macros?.. these are additions, not removals.

you’ve got these deep namespaces, and no overloading, and traits - it feels like it goes too far with naming sometimes


#14

Seriously, I still don’t get your point beside saving some lines of code. You’re somehow wildly mixing ideas and explaining them in a way, that is incomprehensible for me. Please stick to one thing at a time.

I barely get what your duck-type traits have to do with this Scala-swift thing. And as I already said: C++ and rust are made to optimize things away. This is not what Java and ObjC are designed to do. They have a enormous runtime overhead. Especially Swift is only possible because of that.


#15

Seriously, I still don’t get your point beside saving some lines of code.

rust ‘Float’+‘FloatMath’ used to be split into ‘Float’+‘Trigonometric’+‘Algebraic’+‘Exponential’, but then these were merged/shuffled to “simplify traits”. (a) Why did they want to ‘simplify them’ ? … because their existence imposes a mental overhead. (b) why didn’t they simply group, ‘Float=Algebraic+Exponential+FloatBase’, ‘FloatMath=FloatBase+Exponential’, … again, more mental overhead in managing the relations between traits. © What if you had wanted to divide them further instead… e.g. a trait for an Angle that supports sin/cos (and uses integer wrapround arithmetic), where the inverse trig functions don’t make sense. And a generally split for the functions that make sense for fixed point.

The same happens with vector maths. A potentially complex arrangement of traits for each subset of functions that makes sense. (e.g. if you have a vector of fixed point colour components, and all of a sudden some operations don’t make sense, or you want to abstract a set of operations that guarantees intermediates can stay in SIMD registers…)

With duck typing all these issues go away. Define unambiguous functions.

“And as I already said: C++ and rust are made to optimize things away.”

but imagine if you could directly translate some subset of C++ source to Rust source, and vica versa. Then you could use the same generics/templates.

template<typename T>
struct Vec3 {
   T x; T y; T z; T;
   T dot(const T& other) const{ return this->x*other.x +this->y*other.y + this->z*other.z; } ; ... 
} // this is ready for use in templates

struct Vec3<T> {
    x:T, y:T, z:T 
 }
 impl<T> Vec3<T> {
     fn dot(&self, other:&Self)->T { self.x*other.x + self.y*other.y + self.z*other.z } ; ...
 }   // this is not ready for use in generics as it needs a trait
     // if i reuse the name 'dot' differently in another trait, I close the door to translating back to C++

^^ imagine an automatic tool that can perform this translation - it will only make sense for a subset of the language, but imagine a few concessions here and there to increase the subset possible.

( Maybe I could implement an OptBox<T> inplace of Option<Box<T>> in Rust, as that has a more direct C++ representation, then I get the ability to safely pass potentially-null-pointers between C++ & Rust code)

Maybe we could make a tool which instantiates generic functions with certain types, and make those available to the C++ linker…

eg fn something<X:..,Y:..>(x:&X,y:&Y) {...} compile it for X=Foo,Bar Y=Baz,Qux so C++ sees… void something(Foo&,Baz&) void something(Bar&, Baz&) etc.

and vica versa.( I think they keep the door open for this by choosing to emulate gcc name mangling?)

" This is not what Java"

its compile time vs runtime, but the analogy remains. Imagine if one could seamlessly introduce rust libraries into a C++ project, or vica versa. So, by learning Rust, you do not alienate yourself from the C++ community, and can still “serve society” by contributing to projects with existing users.