Skip to content
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

Closed
AdamSpeight2008 opened this issue Jan 28, 2015 · 46 comments
Closed

(Proposal) Concepts/Traits (enhanced generic constraints) #129

AdamSpeight2008 opened this issue Jan 28, 2015 · 46 comments

Comments

@AdamSpeight2008
Copy link
Contributor

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

        trait ::= "Trait" identifier specification
specification ::= '{' spec+ '}'
         spec ::= 

incomplete

A trait can be define in a separate construct, like a class / struct.

Examples

trait Zero { static readonly Zero ()->T }
trait Unit { static readonly Unit ()->T }
trait [+] ( static [+] (T,T)->T } // operator + (addition)
trait [-] { static [-] (T,T)->T } // operator - (subtraction)

A trait can "inherit" other existing traits. eg.

trait SimpleCalc <: { Zero, Unit, [+], [-] }

This allows "constrained generics" over numeric types.

Summation< T > ( IEnumerable<T> nums )
  T has SimpleCalc
{
  T total = T.Zero();
  foreach( T num in nums)
    total = total + num; // += can't be used as the specification didn't specify it.
  return total;
}

A trait can also be anonymously define at the point of use

foo <T> ()
  T has Trait { ... }
{
 // method code 
}

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.

 trait NonNull <T : class> { != null }


int CharCount ( string s )
  s is NonNull 
{
  return s.Length;
}

Note: T! could be an alias for the NonNull Trait


I 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.

@ilmax
Copy link

ilmax commented Jan 28, 2015

I really like the idea!
I would love to better understand this proposal, so starting from the grammar i have few questions:
does a specification allows multiple spec? (the + make me aware of) or should we keep it fixed to one and use "trait inheritance" to allow a trait to be a composite trait?
Does we allow multiple traits to be specified as constraints?
Does we really want to always pay the runtime validation even when all clients of our method are known at compile-time?
Why there are some syntactic changes to how some concepts are represented, e.g <: instead of : for inheritance and a java like arrow type (->) instead of the familiar =>? Are those typos?

@fubar-coder
Copy link

I'd like to use a trait in a similar way I'd use an interface:

public trait Zero<T> {
    static T Zero { get; }
}
public trait Add<T> {
    static T operator +(T,T);
}
public trait Subtract<T> { 
    static T operator -(T,T);
}

Inheritance:

public trait SimpleCalc<T> : Zero<T>, Add<T>, Subtract<T> {
}

Usage:

T Summation<T>(IEnumerable<T> nums)
    where T has SimpleCalc<T>
{
  var total = T.Zero;
  foreach( T num in nums)
    total = total + num; // += can't be used as the specification didn't specify it.
  return total;
}

I personally won't bother about value traits and leave this kind of checks/validations to the Method contracts.

@mikedn
Copy link

mikedn commented Jan 29, 2015

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.

@AdamSpeight2008
Copy link
Contributor Author

@ilmax The symbol + means one or more, so a specification requires at least one spec .
Those aren't typos. It is F# syntax (T,T)->T

()->()  // Method takes no parameters, returns no parameters (void)
()->T   // Method takes no parameters, returns a T
 T->()  // Method takes one parameter (a T) and returns no parameters (void)
 T->T   // Method takes one parameter (a T) and return a T.

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.
@fubar-coder Yes, can use them like interfaces. The usage location of a trait is similar to that of interfaces and constrained generic eg where T is IComparable<T>, because of it's location it could have visibility of the method's parameters. Hence a possible way for Method Contracts (#119)
So the definition of 'spec' would change to 'spec ::= trait_spec | contract`

T! would be an implied value trait for non-null.

@mikeln I don't have the required skills or knowledge on my own, to implement it.

@AdamSpeight2008
Copy link
Contributor Author

Another example trait

trait ICMP_Ops : IComparable<T>
{
 [==] (T,T)->bool
 [!=] (T,T)->bool
 [<]  (T,T)->bool
 [<=] (T,T)->bool
 [>]  (T,T)->bool
 [>=] (T,T)->bool
}

Traits could also be useful in-conjunction with pattern-matching.
borrowing Nemerle's match construct.

match ( foo ) with
{ | __ has foo_trait -> ... ;
  | __ has bar_trait -> ... ;
  | __ has baz_trait -> ... ;
  | __ has trait { ... } -> ...;
  | __ -> ; // default match
}

@mirhagk
Copy link

mirhagk commented Jan 29, 2015

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.

@AdamSpeight2008
Copy link
Contributor Author

Range Trait

T Item(int index)
{
 trait {  _a!; range: 0 <= index < _a,Length; }
  get { _a[index]; }
  set(T item) { _a[index] = item; }
}

Grammar

      range ::= "range:" range_value op range_value op range_upper ";"
range_value ::= Expr<T : IComparable<T>>
range_upper ::= Expr<T> | IList<T>
   range_op ::= "<" | "<="

Also thinking of a shorter version for collections.

range: _a;

So if it is a collection it has .Length so an alias for _a!; range: 0 <= index < _a.Length;

@AdamSpeight2008
Copy link
Contributor Author

Non-Null Trait

 named_type ::= 
         op ::= '!'
    nonnull ::= named_type op ';'

@AdamSpeight2008
Copy link
Contributor Author

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.
The ide would give some indication that traits are being ignored if the source contains any.
-c Enforce at compile-time.
-r Enforce at run-time

This give the ability for the developer to decide if the want them and when.

@AdamSpeight2008
Copy link
Contributor Author

Revised trait grammar (incomplete)

        trait ::= "trait" identifier? commands? '{' spec+ '}'

     commands ::= (compile runtime?) | (runtime compile?) 
      compile ::= "-c" | "-C"
      runtime ::= "-r" | "-R"

         spec ::= range_trait | has_trait | requires_trait | ensures_trait | shape_trait

  range_trait ::= "range:" expr range_op expr range_op expr ';' 
  range_op    ::= "<" | "<="
    has_trait ::= "has:" '(' predicate ')';
ensures_trait ::= "ensures:" preicate ';' 
  shape_trait ::=

The trait options commands can now be place on a trait, to locally override a the global trait options.
Shape Trait is for check the shape of something, does it have this method, does it have this interface. etc

@AdamSpeight2008
Copy link
Contributor Author

@mirhagk Traits are not just for generic constraints.

Think of traits as being a separate concept to

  • Interface
  • Class
  • Struct
  • Generics
    I consider them as being a further generalisation abstracts of Generic Constraints, Code Contracts, Method Contracts. As these approaches are in general about checks and validation, does the object / type met some form of specification.

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.

  • Inherited Traits
    Similar to Interface inheritance.
  • Traits Libraries
    The definition of the them can be in external libraries and referenced into the project.
  • Parameter Value
    Do the parameter met the requires. akin to Guards Statements.
    Is the value null? isnot null or obj !
    Is it within certain bounds. bound:
    If not trait type is specified (eg requires: or bound: it defaults to mean requires: has this trait type is most likely to be used more often than the other, which permits shorter definitions in those use case.
    trait { x < y; y > x;}
    vs
trait { requires: x < y;
        requires: y > x;
      }

Both are styles are permitted.

  • "Shape"
    Shape is about what methods the object must have (both static and instance)
    see the operator examples.

@AdamSpeight2008
Copy link
Contributor Author

Traits also get along well with Pattern Matching (especially #191)

trait xy  { | (_,_) into {x,y} when (x < y) && (y > x); }
public void M(int x, int y) requires x < y, requires y > x {}

could be

public void M(int x, int y) 
trait XY
{}

@AdamSpeight2008
Copy link
Contributor Author

BNF Functions
Use the arguments passed in to replace the arguments use in the rule definition.

/* 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 PComma< ... > produces '(' ... ( ',' ... )* ')'

/* */
            constant ::=
            variable ::=
     type_identifier ::=

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
This is using a lightweight syntax.

  /* 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.

@jvlppm
Copy link

jvlppm commented Feb 4, 2015

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:

  • It is more powerful
  • Don't require inheritance

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.
Along with support for duck typing.

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?

@AdamSpeight2008
Copy link
Contributor Author

Interface are a contract on the type (that implements them). Change the interface an you break things.
A trait doesn't require any interface, it can be used against sealed types, either struct or class.
As long as the trait is satisfied it can be used.
The inheritance is use on traits to allow you specify more complex traits without having to write all it.

trait op_add { public static [+](T,T)->T ; }
trait op_sub { public static [-](T,T)->T ; }
trait add_sub { <: { op_add, op_sub }; }

A trait on an extension method.

 Foo <T> ( this T x )
  where T has trait add_sub
{
}

@mirhagk
Copy link

mirhagk commented Feb 4, 2015

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 implict interface so that rather than matching at the call site you simply use the interface as is.

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)

@AdamSpeight2008
Copy link
Contributor Author

@jvlppm In your example you would have to specify that the operator == is also declared on the type.

@AdamSpeight2008
Copy link
Contributor Author

A useful trait to have.

trait num 
{
  type: 
  {
    | Integer  ;
    | Int16    ;
    | Int32    ;
    | Int64    ; 
    | UInteger ; 
    | UInt16   ;
    | UInt32   ;
    | UInt64   ;
    | Byte     ;
    | UByte    ;
    | Single   ;
    | Double   ;
    | Decimal  ;
  }
}

The extension method Sum would be a generic implementation.

T Sum<T>(this IEnumerable<T> xs ) where T has trait: num
{ T res = T.Zero;
  foreach(T x in xs)
  { res = res + x
  }
 return res;
}

@d-kr
Copy link

d-kr commented Feb 5, 2015

@AdamSpeight2008 Your example uses only Zero and += from num. If you require just those two members Sum could support complex numbers, vectors, matrices, polynoms ... too

@AdamSpeight2008
Copy link
Contributor Author

@d-kr That's a good point! It's good to see others are thinking about trait and improve a trait.

@AdamSpeight2008
Copy link
Contributor Author

Maybe include the (int foo #0,string bar #1) syntax of this Codeplex Topic

@orthoxerox
Copy link
Contributor

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 IMonad<T> in C#. Non-generic IMonad can be defined using CRTP as IMonad<M> where M : IMonad<M>, but this of course depends on the faithfulness of the implementor of the interface. Let's say you want to implement only a Bind/SelectMany method (since we cannot define static methods in an interface yet):

interface IMonad<M> where M : IMonad<M>
{
    M Bind(Func<object, M> func);
}

class Maybe : IMonad<Maybe>
{
    Maybe Bind(Func<object, Maybe> func)
    {
        //implementation goes there
    }
}

You cannot define IMonad<M, T> and implement it in Maybe<T>, but you are welcome to try.

Another way out without traits is adding as this type identifier (like Current in Eiffel). This of course violates LSP and is a thorn in Eiffel programmers' side.

interface IMonad<T>
{
    as this<T> Bind(Func<T, as this<T>> func);
}

class Maybe<T> : IMonad<T>
{
    Maybe<T> Bind(Func<T, Maybe<T>> func)
    {
        //implementation goes there
    }
}

IMonad<int> m = new Maybe<int>(1);
m.Bind(x => yield x); //BOOM!

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:

trait TMonad<T>
{
    static as this<T> Unit(T value);
    as this<T> Bind(Func<T, as this<T>> func);
}

class Maybe<T> : TMonad<T> //not necessary, but allows the compiler to check that the implementation is complete
{
    static Maybe<T> Unit(T value)
    {
        return new Some<T>(value);
    }

    Maybe<T> Bind(Func<T, Maybe<T>> func)
    {
        //implementation goes there
    }
}

Another point where I wished for traits was computing an average of Points using LINQ. I worked around by implementing a Point-specific extension method, but a universal extension method could have been implemented with a trait.

@AdamSpeight2008
Copy link
Contributor Author

@zdenek-jelinek It's similar the C++ proposal of Concepts.
It a lot more than a language feature.

I've look at the other proposals
#119
#2427
etc
and seeing if there is a underlying unifying "concept" that is applicable.
Traits I believe are good solution to that problem.

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?
Operator are overload able so strictly speaking there meaning is user defined
How can we be certain that the operator `<' actually means LessThan? or that '+'' is to mean Addition. I

Interface <: Trait  /* which only contain instance method traits /*
Contract  <: Trait

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.
It would also be possible release just the traits as a nuget package.

@zdenek-jelinek
Copy link

@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:

  1. The value traits could be a separate thing since they do not relate to type system that much.
  2. Traits composed of list of types don't feel very flexible as they disallow more types to fulfill such trait and seem even more limited than current generic constraints.
  3. Taking (1.) into account, the syntax can be amended to fit into C#:
public trait Number<T>
{
    static T MinValue { get; }
    static T MaxValue { get; }
    static T Zero { get; }
}
public trait Addable<T> : Number<T>
{
    static T operator+(T a, T b);
}

public static class SumProvider
{
    T Sum<T>(IEnumerable<T> values) where T : Addable<T>
    {
        T result = T.Zero;
        foreach (var value in values)
        {
            result += value; // uses operator+
        }

        return result;
    }
}

This however does not fit all the needs you showed in this thread.

Note that the Zero property is not currently present on basic numeric types. Thus we need a mechanism to add it there. We could probably use extension methods or extension properties (theoretical) to achieve such a thing.

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.

@AdamSpeight2008
Copy link
Contributor Author

@zdenek-jelinek
I think in some case it would be possible to get reflection based information at compile-time.

@MatthieuMEZIL
Copy link

FYI, I implemented for fun the operator generic constraint (with an old version of Roslyn).
https://improvecscompiler.codeplex.com/documentation
Don't know if it's the way you want it but I +1 the idea.

@dsaf
Copy link

dsaf commented Aug 26, 2015

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.

@AdamSpeight2008
Copy link
Contributor Author

People seem to be assuming that this is focused on generics and or contracts. There is no requirement the object be generic.

Think of traits as being a separate concept to
•Interface
•Class
•Struct
•Generics
I consider them as being a further generalisation abstracts of Generic Constraints, Code Contracts, Method Contracts. As these approaches are in general about checks and validation, does the object / type met some form of specification.

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.

As see it and proposed that these are aspects of the same problem.

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.
Eg you can not add an interface / contract to it.
Extension methods don't help here.

How do I validate that object meets the specification? It may not implement an Interface
For example: Is it a numeric type?

What if I need to validate it has static traits?
For example: A Static constructor that takes an Integer?*

@gafter
Copy link
Member

gafter commented Aug 27, 2015

@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.

@AdamSpeight2008
Copy link
Contributor Author

@gafter Not every programmer is a computer scientist, if fact it is a rather specialist role.
I'm thinking more towards the computer programmer.

noun: trait; plural noun: traits
a distinguishing quality or characteristic, typically one belonging to a person.

synonyms: characteristic, attribute, feature, quality, property;
habit, custom, mannerism, idiosyncrasy, peculiarity, quirk, oddity, foible

  • characteristic
    too long, easily mis-associated with character.
  • attribute
  • Already has a meaning within C# and VB.net.*
    *feature
  • doesn't work, as it as an existing association to aspects of system (typical UI).
  • quality
    Association with how good / bad something is.
  • property
    *Already has meaning within C# and VB.net *
  • criteria
    More associated with filtering data, or the conditional used when you know the measurement domain it being use.
    eg height. > 6foot
    Don't like the word *criteria
    as is has negative connotations, ie critic, criticism*

Possible Alternatives

  • Axiom
  • Concepts
  • Quirks

@gafter
Copy link
Member

gafter commented Aug 27, 2015

@AdamSpeight2008 I think "concept" is probably the best of those.

@GeirGrusom
Copy link

How are traits implemented in a type? Addable in a Vector2 type for example.

@AdamSpeight2008
Copy link
Contributor Author

@ilmax

How are traits implemented in a type? Addable in a Vector2 type for example.
What do you mean? Addable?

@GeirGrusom
Copy link

I have the two following traits:

trait [+] { static [+] (T,T)->T }
trait Plusable { static [+] (T, T) -> T }

And the following struct:

public struct Vector2
{
    public float X { get; }
    public float Y { get; }
    public Vector2(float x, float y)
    {
        X = x;
        Y = y;
    }
}

Can I implement both traits in this struct, and if so, how would I do it?

@orthoxerox
Copy link
Contributor

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 attest statement and allows Vector2 to be passed to any function that accepts Addables.

@AdamSpeight2008
Copy link
Contributor Author

public struct Vector2
{
    public float X { get; }
    public float Y { get; }
    public Vector2(float x, float y)
    {
        X = x;
        Y = y;
    }
}

That type Vector2 does match either of traits because it does not have

  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.

@dsaf
Copy link

dsaf commented Aug 27, 2015

@AdamSpeight2008

I consider them as being a further generalisation abstracts of Generic Constraints, Code Contracts, Method Contracts. As these approaches are in general about checks and validation, does the object / type met some form of specification.

I would call them clause which means stipulation: a condition or requirement that is specified or demanded as part of an agreement.

@GeirGrusom
Copy link

@AdamSpeight2008 Isn't a central aspect of traits that they are always explicit?
A better example:

trait DotProduct { static [*] (T, T) -> Scalar }
trait Scale { static [*] (T, T) -> T }

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.

@orthoxerox
Copy link
Contributor

@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 attest int has DotProduct, but it's their own problem now.

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 Vector3<int> that must produce integer dot products and can only be scaled using integers. But is this enough to allow writing var b = new Vector3<int>(1, 2, 3) * 1.5; and getting the compiler to understand it can safely convert Vector3<int> to Vector3<double>?

@GeirGrusom
Copy link

@orthoxerox The DotProduct signature was wrong (since dot product returns a scalar), but the other was correct.

trait DotProduct
{
    static [*] (T, T) -> Scalar;
}    

trait Scale
{
    static [*] (T, T) -> T;
}

If I write

new Vector3<int>(1, 2, 3) * new Vector3<int>(1, 2, 3)

Is the result [1, 4, 9] or 14?

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?

@orthoxerox
Copy link
Contributor

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 Scalar trait and you can't define both operators in a way that both traits can pick them up implicitly (since only their return value would be different). For example, you could write:

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 Vector3 to a function that accepts DotProduct parameters and use * there to call DP, pass it to a function that accepts Scale parameters and use * to call S. But I wouldn't be able to use * on two Vectors3 directly, or use DotProduct&Scale as the function parameter type.

@xen2
Copy link

xen2 commented Aug 27, 2015

@orthoxerox Would really like to see traits implemented that way (non-intrusive).
Lot of code reuse possible and lot of possibilities for new optimizations (force devirtualization by using traits instantiations instead of interfaces, without having to duplicate code).

@gafter gafter added this to the Unknown milestone Sep 10, 2015
@gafter
Copy link
Member

gafter commented Sep 13, 2015

Since all examples given in this issue are as a constraint, I'm renaming this to clarify that is its purpose.

@gafter gafter changed the title (Proposal) Traits (Proposal) Concepts/Traits (enhanced generic constraints) Sep 13, 2015
@gafter
Copy link
Member

gafter commented Nov 20, 2015

This is a dup of #154.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests