-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(Proposal) Concepts/Traits (enhanced generic constraints) #129
Comments
I really like the idea! |
I'd like to use a trait in a similar way I'd use an interface:
Inheritance:
Usage:
I personally won't bother about value traits and leave this kind of checks/validations to the Method contracts. |
For features like this you may want to start from outlining a possible implementation. Otherwise you may end up with a spec for a feature that's not implementable. |
@ilmax The symbol
The use of square brackets are for operator symbols, it easier to write and understand than the "true" names. The actual syntax used and grammar, at this stage it a lightweight to get the concepts across.
@mikeln I don't have the required skills or knowledge on my own, to implement it. |
Traits could also be useful in-conjunction with pattern-matching.
|
I like this very much. It allows the generics in C# to be much more flexible, allowing structural typing for methods and classes that use the traits. This is a really good basis to develop other features on top of in the future. For instance it'd be nice if you could have automatic traits, where the compiler infers the traits based on the usage within the class/function. That would eliminate a LOT of the boiler plate of types in C#, and give you essentially global type inference in opt-in scenarios. |
Range Trait T Item(int index)
{
trait { _a!; range: 0 <= index < _a,Length; }
get { _a[index]; }
set(T item) { _a[index] = item; }
} Grammar
Also thinking of a shorter version for collections.
So if it is a collection it has |
Non-Null Trait
|
Trait Options option traits // default traits are ignored at compile and runtime.
option traits -c -r // flag are -c compile-time -r runtime. These determine how the traits are treated, by default are ignored both at compile and run time. This give the ability for the developer to decide if the want them and when. |
Revised trait grammar (incomplete)
The trait options commands can now be place on a trait, to locally override a the global trait options. |
@mirhagk Traits are not just for generic constraints. Think of traits as being a separate concept to
Since traits are on more abstract level and should be considered as a part of the type/ object / method's signature. How the those traits are validated aren't specified.
Both are styles are permitted.
|
Traits also get along well with Pattern Matching (especially #191)
could be
|
BNF Functions /* BNF functions */
Seq<T,S> ::= T (S T)*
Sur<T,S0,S1> ::= S0 T S1
Comma<T> ::= Seq<T,','>
Par<T> ::= Sur<T,'(',')'>
Braced<T> ::= Sur<T,'{','}'>
PComma<T> ::= Par<Comma<T>>
BComma<T> ::= Braced<Comma<T>> Eg
Trait Grammar /* Trait */
trait ::= trait_key trait_options
trait_key ::= "trait"
trait_options ::= (compile runtime?) | (runtime? | compile)
compile ::= "-C"
runtime ::= "-R"
trait_body ::= '{' types_of_trait '}'
trait_types ::= named_trait | unnamed_trait
named_trait ::= identifier trait_body
unnamed_trait ::= trait_body
type_of_trait :: trait_inherits? ( requires_traits
| pattern_trait
| bound_trait
| shape_trait )+ Trait Inheritance /* Trait Inheritance */
trait_inherits ::= trait_key '<:' ( named_trait | BComma< named_trait >) ';' Requires Trait /* Requires Trait */
requires_trait ::= "requires:"? requires_spec ';' Trait Pattern Match /* Pattern Match */
pattern_trait ::= '|' clause into? when?
clause ::= simple_clause
simple_clause ::= PComma< clause_types >
clause_types ::= simple | typed
simple ::= constant | variable | wildcard
wildcard ::= '_' Parameter Type /* Parameter Type */
typed ::= specific_type
specific_type :: simple ':' type_identifier
inherits_type :: simple "<:" type_identifer Into Paramete /* Into Parameter */
into ::= "into" into_params
into_params ::= BComma< identifier > When Parameter /* When Parameter */
when ::= "when" when_predicate
when_predicate ::= simple_predicate | complex_predicate Bounded Trait /* Bounded Trait **/
bound_trait ::= "bound:" bound_value bound_op bound_value bound_op ';'
bound_value ::= constant | variable
bound_op ::= '<' | '<=' Shape Trait /* Shape Trait */
shape_trait ::=
shape_types ::= operator_shape | method_shape
scope ::= static? public | private
public ::= "public"
private ::= "private"
static ::= "static" Type Signature /* Type Signature */
generic_params ::= '<' Comma< generic_param > '>'
generic_param ::=
type_signature ::= generic_params? PComma< type_identifer > type_returns return_type ';'
type_returns ::= "->"
return_type :: void | type_identifier
void ::= "void" | "()" Operator Shape /* Operator Shape */
operator_shape ::= scope '[' operator ']' type_signature
operator ::= math_operator | comparision_operator
math_operator ::= minus |
positive |
add |
multiply |
divide
minus ::= '-'
positive ::= '+'
add ::= '+'
subtract ::= '-'
divide ::= '\'
multiply ::= '*'
comparision_operator ::= lt | le | eq | ne | gt | ge
lt ::= '<'
le ::= "<="
eq ::= "=="
ne ::= "!="
gt ::= '>'
ge ::= ">=" Method Shape /* Method Shape */
method_shape ::= scope method_identifer type_signature ';' This grammar probably requires more tweaking, but should be sufficient to gain an basic understanding. |
Its an interesting idea, and I would like to see this working with extension methods over any class that have specific traits, but one thing concerns me: At first glance this trait idea to me it looks like an interface, except:
I think it could be more interesting to add this "power" to interfaces, like allowing you to add static methods / properties to interfaces, operators support, etc. Sample interface ISample {
static readonly ISample Zero { get; }
static ISample operator +(ISample c1, ISample c2);
}
public static bool IsZero<T>(this T value) where T match ISample {
return T.Zero == value;
}
interface IOldStyle {
bool Test();
}
// Extension method that affects all classes that matches IOldStyle
public static void DumpTest(match IOldStyle obj) {
Console.WriteLine("Test: \{obj.Test()}");
} Any comments? |
Interface are a contract on the type (that implements them). Change the interface an you break things.
A trait on an extension method. Foo <T> ( this T x )
where T has trait add_sub
{
} |
It's an interesting idea to leverage the existing interface mechanism. It would limit some of the more crazy traits proposed (value based traits wouldn't work for example). Now you are creating a near vanilla interface, and saying that something matches it at the call site. That's an interesting concept. I'd consider as an alternative having an Things to investigate are how well this works at the CLR level, does this cause any confusion, diamond of death type scenarios. Does it cause any backwards compatibility issues (thinking about reflection here)? Does it cause any more boxing of structs? (the nice thing about the generics approach is it can avoid boxing) |
@jvlppm In your example you would have to specify that the operator |
A useful trait to have.
The extension method
|
@AdamSpeight2008 Your example uses only |
@d-kr That's a good point! It's good to see others are thinking about trait and improve a trait. |
Maybe include the |
I've been thinking about traits recently. The big difference between a trait and an interface is that an interface has to obey the Liskov Substitution Principle (return types are covariant, argument types are contravariant), while a trait shouldn't. For example, you cannot define
You cannot define Another way out without traits is adding
So it looks like the only solution is having traits, which do not answer the question what the class is, but what it works like:
Another point where I wished for traits was computing an average of |
@zdenek-jelinek It's similar the C++ proposal of Concepts. I've look at the other proposals I think of the proposal as the essence abstraction of generics, contracts, Concepts (C++), fixes the issue think you can express with interfaces since they require instance methods, what if your need a static methods like an operator? Traits would allow them be checked and allowed to be used within that method. How do you define that a generic type argument but be Numeric? What do we mean by Numeric?
Is it a Struct? Class? Interface? or none of the above. We need a way express these "higher level" concepts and axioms, down into the language itself. Where it can verified by the compiler at comple-time that those axioms are true. The CLR itself could then enforce them at runtime. |
@AdamSpeight2008 I have thought about this and changed my mind. As you are saying, this is very close to the Concepts of C++ and it makes sense to have a separate language concept for that. I still feel there are some things that you may want to address:
This however does not fit all the needs you showed in this thread. Note that the Also, it would be nice to get some idea how this would be implemented in the type system and language itself. When you need to know whether a given type implements a given interface, you query it's inheritance hierarchy and see it there, that is quite fast. However for something like traits, the type needs to be checked whether it contains all the required functionality. These checks sometimes need to happen in runtime (instantiating generic type through reflection) and we need them to be as fast as possible. This doesn't really bode well with the extension methods... As I see it, there are multiple issues with C# that can be solved with introducing duck typing and this is one good way of doing that (the other being for example type interfaces, #2427). The only difference I see in the concepts is that type interfaces reuse already existing C# features, while this brings a whole new feature in quite an aggressive manner. It would be nice to see both the proposals mature and be able to recognize the pros and cons of both the apporoaches. However, I disagree with the idea of traits being solution for contracts etc. as mentioned above. Those are semantic checks evaluated regardless of evaluated type. |
@zdenek-jelinek |
FYI, I implemented for fun the operator generic constraint (with an old version of Roslyn). |
I like the proposal but it can get confused with the existing concept http://en.wikipedia.org/wiki/Trait_%28computer_programming%29. I would change the title to "Trait-like contracts for generic constraints" or something else that mentions contracts and/or generics constraints. |
People seem to be assuming that this is focused on generics and or contracts. There is no requirement the object be generic.
Suppose you want to see if a particular object has specific traits ( C++ Concepts ) but that object is sealed / you have no control over its design and implementation. How do I validate that object meets the specification? It may not implement an What if I need to validate it has |
@AdamSpeight2008 Sure, if you like the word criteria bettern than constraints. In either case this isn't what computer scientists call traits, and it would be less confusing if you select a different word. |
@gafter Not every programmer is a computer scientist, if fact it is a rather specialist role.
Possible Alternatives
|
@AdamSpeight2008 I think "concept" is probably the best of those. |
How are traits implemented in a type? Addable in a Vector2 type for example. |
|
I have the two following traits:
And the following struct:
Can I implement both traits in this struct, and if so, how would I do it? |
Ideally everything everything could be disjoint: public struct Vector2
{
public float X { get; }
public float Y { get; }
public Vector2(float x, float y)
{
X = x;
Y = y;
}
}
trait Addable { static [+] (T,T)->T }
public static Vector2 Add(Vector2 a, Vector 2 b)
{
return new Vector2(a.X + b.X, a.Y + b.Y);
}
attest Vector2 has Addable
{
Add is [+];
}
/*These declarations could be split across four different assemblies for all I care*/ The compiler checks your |
That type static Vector2 operator +( Vector2 a, Vector2 b) {}
static Vector 2 Plusable( Vector2 a, Vector2 b) {} You could also state that you implement the trait, like you implement an interface. public struct Vector2 :
trait [+], Plusable
{
public float X { get; }
public float Y { get; }
public Vector2(float x, float y)
{
X = x;
Y = y;
}
static Vector2 +( Vector2 a, Vector2 b)
{
throw NotImplementedException();
}
static Vector2 Plusable( Vector2 a, Vector2 b)
{
throw NotImplementedException();
}
} Think of the case where you don't control the type. (eg 3rd Party implementer), but you're a consumer of it. |
I would call them |
@AdamSpeight2008 Isn't a central aspect of traits that they are always explicit?
Both are different interperations of how the multiplication operator should work for a vector type. Xna for example uses dot product whereas every single other implementation uses scale (that I know of). Could this be expressed? edit: DotProduct should return a scalar type. |
@GeirGrusom I don't think the signatures of your traits are correct. It should be: trait DotProduct
{
static [*] (T, T) -> Scalar;
}
trait Scale
{
static [*] (Scalar, T) -> T;
static [*] (T, Scalar) -> T;
} The question is still valid, since if we allow implicit trait implementation, Double or Int32 will suddenly have these traits. If trait implementation is explicit, no one will stop the programmer when they write Actually, there's a more interesting case here, generic traits: trait DotProduct<U>
where U : Scalar
{
static [*] (T, T) -> U;
}
trait Scale<U>
{
static [*] (U, T) -> T;
static [*] (T, U) -> T;
} Now we can have types like |
@orthoxerox The DotProduct signature was wrong (since dot product returns a scalar), but the other was correct.
If I write
Is the result edit: for Vector3 you could have cross product as well, which would result in 0. This relates to explicitness of traits. Should traits be implied? |
Oh, you meant scale as in elementwise multiplication? Like I've said, the traits should be explicitly assigned to the types, either during declaration of the type or via a separate statement. They should be able to implicitly recognize the members of the type they require, but two traits that do not inherit from each other shoudn't be allowed to recognize the same member unless the programmer tells them to do so. E.g.: trait DotProduct
{
static [*] (T, T) -> Scalar;
}
trait Scale
{
static [*] (T, T) -> T;
}
attest int has DotProduct; //ok
attest int has Scale; //compiler error: "operator * is already assigned to trait DotProduct!"
/* the correct way:
attest int has DotProduct {* is *}
attest int has Scale {* is *} //won't anyone fire this useless programmer?
*/ For Vector3 a different situation would happen, since Vector3 doesn't have a public struct Vector3 :
trait DotProduct {* is DP}, Scale {* is S}
{
public float X { get; }
public float Y { get; }
public float Z { get; }
public Vector3(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
static float DP( Vector3 a, Vector3 b)
{
throw NotImplementedException();
}
static Vector3 S( Vector3 a, Vector3 b)
{
throw NotImplementedException();
}
} Now I could pass |
@orthoxerox Would really like to see traits implemented that way (non-intrusive). |
Since all examples given in this issue are as a constraint, I'm renaming this to clarify that is its purpose. |
This is a dup of #154. |
Traits
Traits are a specification a Type / Method must have to be valid. This specification is validation at both compile-time and runtime. The runtime cost maybe worth it for additional safety,
Basic Grammar
incomplete
A trait can be define in a separate construct, like a class / struct.
Examples
A trait can "inherit" other existing traits. eg.
This allows "constrained generics" over numeric types.
A trait can also be anonymously define at the point of use
Value Traits
Value Trait are checked against the value of a variable, if it possible to valid at compile-time it is, if not it is validated at runtime.
Note:
T!
could be an alias for the NonNull TraitI do require help to spec-out this idea, I think it has potential has *(as see it) that traits would also enable other current proposals / part of them.
The text was updated successfully, but these errors were encountered: