www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Concepts vs template constraints - the practical approach

reply Norbert Nemec <Norbert Nemec-online.de> writes:
Hi there,

back in the discussions about C++-"concepts" it was argued that 
D-template-parameter constraints allow you to achieve the same goal. 
Now, I find it fairly difficult to come up with a clean solution for 
this that actually scales up for complex libraries. My best attempt so 
far is as follows:

===================================================

template verifyMyConcept(A) {
    static assert(is(A.type));
    static assert(A.len >= 0);
    static assert(is(typeof(A.init[0]) == A.type));
}

struct MyClass(T,int R) {
    alias T type;
    enum len = R;

    T[R] value;

    T opIndex(int idx) {
       return value[idx];
    }

    mixin verifyMyConcept!(typeof(this));
}

void myfunction(A)(A arr)
    if(__traits(compiles, verifyMyConcept!(A)))
{
}

unittest {
    MyClass!(int,4) x;

    mixin verifyMyConcept!(typeof(x));
    myfunction(x);
}

===================================================

As you can see, this approach attempts to define all the requirements 
for MyConcept in one place as individual static assertions. This permits 
error messages to identify which requirement for the concept is not met.

Still the code seems fairly ugly to me and the error message is not 
quite clear enough for my taste.

Ideally, there should be a way to use "concepts" similar to interfaces:

a) A concept should be defined readably in one place, listing a set of 
requirements, possibly inheriting other concepts.

b) A struct implementing the concept should state this in a similar way 
to a class that implements an interface.

c) A template that requires a parameter to fulfil a concept should state 
this in a similar way to a function requiring a specific input type

and most importantly:

d) a user of the library should get a clear and simple error message 
when using templates with parameters that do not fulfil the required 
concept.

Has anyone achieved these goals better than my feeble attempt?

Greetings,
Norbert
Nov 20 2011
next sibling parent reply Denis Shelomovskij <verylonglogin.reg gmail.com> writes:
 void myfunction(A)(A arr)
 if(__traits(compiles, verifyMyConcept!(A)))
 {
 }

It should be --- void myfunction(A)(A arr) { verifyMyConcept!A; } --- to see the error messages.
Nov 20 2011
parent Norbert Nemec <Norbert Nemec-online.de> writes:
On 20.11.2011 19:58, Denis Shelomovskij wrote:
 void myfunction(A)(A arr)
 if(__traits(compiles, verifyMyConcept!(A)))
 {
 }

It should be --- void myfunction(A)(A arr) { verifyMyConcept!A; } --- to see the error messages.

Indeed - that way you win a meaningful error message but you loose the possibility for overloading. Guess, one really has to choose between the two... :-(
Nov 20 2011
prev sibling next sibling parent reply Denis Shelomovskij <verylonglogin.reg gmail.com> writes:
Your example, a bit improved:
---
// In std.concept, e.g.
template verify(alias concept, T) {
     //static assert(isConcept!concept, concept.stringof ~ " is not a 
concept"); //the test for particular concept's concepts can be added
     mixin concept!T;
}

template verify(alias concept) {
     mixin verify!(concept, typeof(this));
}

template satisfy(alias concept, T) {
     enum satisfy = __traits(compiles, verify!(concept, T));
}

// User code
template myConcept(A) {
     static assert(is(A.type), "Error 1: !is(A.type)");
     static assert(A.len >= 0, "Error 2: A.len < 0");
     static assert(is(typeof(A.init[0]) == A.type), "Error 3: 
typeof(A.init[0]) != A.type");
}

struct MyClass(T,int R) {
     alias T type;
     enum len = R;

     T[R] value;

     T opIndex(int idx) {
         return value[idx];
     }

     mixin verify!myConcept;
}

void myFunction(A)(A arr) {
     mixin verify!(myConcept, A);
}

void myOverlodedFunction(A)(A arr) if(satisfy!(myConcept, A)) {
     // Do something
}

void myOverlodedFunction(A)(int i, A arr) if(satisfy!(myConcept, A)) {
     // Do something
}

void myOverlodedFunction(T...)(T) {
     static assert(0, "Error: here should be some user-defined error 
message(s) based on T");
}

unittest {
     MyClass!(int,4) x;
     myFunction(x);
     //myFunction(1); //Error: static assert  "Error 1: !is(A.type)"
     myOverlodedFunction(x);
     myOverlodedFunction(3, x);
     //myOverlodedFunction(3, 2); //Error: static assert  "Error: here 
should be..."
}
---
Nov 20 2011
parent Norbert Nemec <Norbert Nemec-online.de> writes:
Nice! That really improves things! Looking forward to playing with it a 
little more...



On 20.11.2011 20:31, Denis Shelomovskij wrote:
 Your example, a bit improved:
 ---
 // In std.concept, e.g.
 template verify(alias concept, T) {
 //static assert(isConcept!concept, concept.stringof ~ " is not a
 concept"); //the test for particular concept's concepts can be added
 mixin concept!T;
 }

 template verify(alias concept) {
 mixin verify!(concept, typeof(this));
 }

 template satisfy(alias concept, T) {
 enum satisfy = __traits(compiles, verify!(concept, T));
 }

 // User code
 template myConcept(A) {
 static assert(is(A.type), "Error 1: !is(A.type)");
 static assert(A.len >= 0, "Error 2: A.len < 0");
 static assert(is(typeof(A.init[0]) == A.type), "Error 3:
 typeof(A.init[0]) != A.type");
 }

 struct MyClass(T,int R) {
 alias T type;
 enum len = R;

 T[R] value;

 T opIndex(int idx) {
 return value[idx];
 }

 mixin verify!myConcept;
 }

 void myFunction(A)(A arr) {
 mixin verify!(myConcept, A);
 }

 void myOverlodedFunction(A)(A arr) if(satisfy!(myConcept, A)) {
 // Do something
 }

 void myOverlodedFunction(A)(int i, A arr) if(satisfy!(myConcept, A)) {
 // Do something
 }

 void myOverlodedFunction(T...)(T) {
 static assert(0, "Error: here should be some user-defined error
 message(s) based on T");
 }

 unittest {
 MyClass!(int,4) x;
 myFunction(x);
 //myFunction(1); //Error: static assert "Error 1: !is(A.type)"
 myOverlodedFunction(x);
 myOverlodedFunction(3, x);
 //myOverlodedFunction(3, 2); //Error: static assert "Error: here should
 be..."
 }
 ---

Nov 20 2011
prev sibling next sibling parent reply =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 11/20/2011 08:41 AM, Norbert Nemec wrote:
 Hi there,

 back in the discussions about C++-"concepts" it was argued that
 D-template-parameter constraints allow you to achieve the same goal.

Have a look at std.range.hasLength, std.range.isInputRange, and friends.
 Now, I find it fairly difficult to come up with a clean solution for
 this that actually scales up for complex libraries. My best attempt so
 far is as follows:

 ===================================================

 template verifyMyConcept(A) {
 static assert(is(A.type));
 static assert(A.len >= 0);
 static assert(is(typeof(A.init[0]) == A.type));
 }

 struct MyClass(T,int R) {
 alias T type;
 enum len = R;

 T[R] value;

 T opIndex(int idx) {
 return value[idx];
 }

 mixin verifyMyConcept!(typeof(this));
 }

 void myfunction(A)(A arr)
 if(__traits(compiles, verifyMyConcept!(A)))
 {
 }

 unittest {
 MyClass!(int,4) x;

 mixin verifyMyConcept!(typeof(x));
 myfunction(x);
 }

 ===================================================

 As you can see, this approach attempts to define all the requirements
 for MyConcept in one place as individual static assertions. This permits
 error messages to identify which requirement for the concept is not met.

 Still the code seems fairly ugly to me and the error message is not
 quite clear enough for my taste.

 Ideally, there should be a way to use "concepts" similar to interfaces:

 a) A concept should be defined readably in one place, listing a set of
 requirements, possibly inheriting other concepts.

I have separated the parts of the concepts below and then defined matchesMyConcept to "inherit" them.
 b) A struct implementing the concept should state this in a similar way
 to a class that implements an interface.

 c) A template that requires a parameter to fulfil a concept should state
 this in a similar way to a function requiring a specific input type

 and most importantly:

 d) a user of the library should get a clear and simple error message
 when using templates with parameters that do not fulfil the required
 concept.

Although the following code works acceptably with dmd 2.056, sometimes the error messages are less than ideal. This may happen when there are overloads of a function template and none of them accept a template parameter. The compiler can only say that "there is no function template for this use".
 Has anyone achieved these goals better than my feeble attempt?

 Greetings,
 Norbert

Here is something: template hasType(T) { enum bool hasType = is(T.type); } template hasNonNegativeLength(T) { enum bool hasNonNegativeLength = T.len >= 0; } template firstElementSameType(T) { enum bool firstElementSameType = is(typeof(T.init[0]) == T.type); } template matchesMyConcept(T) { enum bool matchesMyConcept = (hasType!T && hasNonNegativeLength!T && firstElementSameType!T); } struct MyClass(T,int R) { alias T type; enum len = R; T[R] value; T opIndex(int idx) { return value[idx]; } } struct YourClass {} void myfunction(A)(A arr) if (matchesMyConcept!A) { } unittest { MyClass!(int,4) x; myfunction(x); // <-- this works fine YourClass y; myfunction(y); // <-- compilation ERROR for this one } void main() {} Ali
Nov 20 2011
parent reply Norbert Nemec <Norbert Nemec-online.de> writes:
Hi Ali,

indeed, defining individual named sub-concepts makes the thing somewhat 
more readable.

However, my approach with individual static assertions was very 
intentional:

Collecting individual requirements as an AND expression of booleans does 
not allow any helpful error message. If just one of the requirements is 
not met, there is just one failure of the global assertion.

I had also toyed with boolean wrappers for the "__traits(compiles,...)" 
construct. It does work, but still it feels way more hacky than anything 
I would want to use at the foundation of a general library.

Greetings,
Norbert



On 20.11.2011 19:47, Ali Çehreli wrote:
 On 11/20/2011 08:41 AM, Norbert Nemec wrote:
  > Hi there,
  >
  > back in the discussions about C++-"concepts" it was argued that
  > D-template-parameter constraints allow you to achieve the same goal.

 Have a look at std.range.hasLength, std.range.isInputRange, and friends.

 Here is something:

 template hasType(T)
 {
 enum bool hasType = is(T.type);
 }

 template hasNonNegativeLength(T)
 {
 enum bool hasNonNegativeLength = T.len >= 0;
 }

 template firstElementSameType(T)
 {
 enum bool firstElementSameType = is(typeof(T.init[0]) == T.type);
 }

 template matchesMyConcept(T)
 {
 enum bool matchesMyConcept = (hasType!T &&
 hasNonNegativeLength!T &&
 firstElementSameType!T);
 }

Nov 20 2011
parent reply =?UTF-8?B?QWxpIMOHZWhyZWxp?= <acehreli yahoo.com> writes:
On 11/20/2011 11:36 AM, Norbert Nemec wrote:
 Hi Ali,

 indeed, defining individual named sub-concepts makes the thing somewhat
 more readable.

 However, my approach with individual static assertions was very
 intentional:

 Collecting individual requirements as an AND expression of booleans does
 not allow any helpful error message. If just one of the requirements is
 not met, there is just one failure of the global assertion.

dmd 2.056 with the code that I've tried, does provide which individual requirement is not met: deneme.d(54697): Error: no property 'len' for type 'YourClass' deneme.d(54709): Error: template instance deneme.hasNonNegativeLength!(YourClass) error instantiating deneme.d(54727): instantiated from here: matchesMyConcept!(YourClass)
 I had also toyed with boolean wrappers for the "__traits(compiles,...)"
 construct. It does work, but still it feels way more hacky than anything
 I would want to use at the foundation of a general library.

Agreed.
 Greetings,
 Norbert

Ali
 On 20.11.2011 19:47, Ali Çehreli wrote:
 On 11/20/2011 08:41 AM, Norbert Nemec wrote:
 Hi there,

 back in the discussions about C++-"concepts" it was argued that
 D-template-parameter constraints allow you to achieve the same goal.

Have a look at std.range.hasLength, std.range.isInputRange, and friends.

 Here is something:

 template hasType(T)
 {
 enum bool hasType = is(T.type);
 }

 template hasNonNegativeLength(T)
 {
 enum bool hasNonNegativeLength = T.len >= 0;
 }

 template firstElementSameType(T)
 {
 enum bool firstElementSameType = is(typeof(T.init[0]) == T.type);
 }

 template matchesMyConcept(T)
 {
 enum bool matchesMyConcept = (hasType!T &&
 hasNonNegativeLength!T &&
 firstElementSameType!T);
 }


Nov 20 2011
parent Norbert Nemec <Norbert Nemec-online.de> writes:
On 20.11.2011 21:03, Ali Çehreli wrote:
 On 11/20/2011 11:36 AM, Norbert Nemec wrote:
 Collecting individual requirements as an AND expression of booleans does
 not allow any helpful error message. If just one of the requirements is
 not met, there is just one failure of the global assertion.

dmd 2.056 with the code that I've tried, does provide which individual requirement is not met: deneme.d(54697): Error: no property 'len' for type 'YourClass' deneme.d(54709): Error: template instance deneme.hasNonNegativeLength!(YourClass) error instantiating deneme.d(54727): instantiated from here: matchesMyConcept!(YourClass)

True, there is some additional detail in some cases, but if you look closely: dmd does not complain about requirements that compute as "false", but about subexpressions that fail to compile at all. In fact, such an error even means that the check is unusable as constraint: A template constraint should always compile without error and simply return true or false.
Nov 20 2011
prev sibling parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
On 11/20/11 10:41 AM, Norbert Nemec wrote:
[snip]

Why not follow the patter of isXxx in the standard library?

Andrei
Nov 20 2011
parent reply Norbert Nemec <Norbert Nemec-online.de> writes:
On 20.11.2011 21:07, Andrei Alexandrescu wrote:
 On 11/20/11 10:41 AM, Norbert Nemec wrote:
 [snip]

 Why not follow the patter of isXxx in the standard library?

 Andrei

Actually, that was my starting point. However, this approach does not scale up: A concept typically is a collection of requirements that have to be met individually. A huge boolean statement inside an assertion does not give any clue as to which part of it failed. The isXxx approach works fine for the template constraint itself, but to assert that a given type meets all requirements of a concept, it is not very usable at all.
Nov 20 2011
next sibling parent reply Tobias Pankrath <tobias pankrath.net> writes:
 Actually, that was my starting point. However, this approach does not
 scale up:
 
 A concept typically is a collection of requirements that have to be met
 individually. A huge boolean statement inside an assertion does not give
 any clue as to which part of it failed.
 
 The isXxx approach works fine for the template constraint itself, but to
 assert that a given type meets all requirements of a concept, it is not
 very usable at all.

Wouldn't it be preferable to improve the compiler to a degree, that it can tell, which subexpression of a constraint failed?
Nov 21 2011
parent Norbert Nemec <Norbert Nemec-online.de> writes:
On 21.11.2011 09:24, Tobias Pankrath wrote:
 Actually, that was my starting point. However, this approach does not
 scale up:

 A concept typically is a collection of requirements that have to be met
 individually. A huge boolean statement inside an assertion does not give
 any clue as to which part of it failed.

 The isXxx approach works fine for the template constraint itself, but to
 assert that a given type meets all requirements of a concept, it is not
 very usable at all.

Wouldn't it be preferable to improve the compiler to a degree, that it can tell, which subexpression of a constraint failed?

Two problems: a) failing constraints do not lead to a compiler error at all but simply lead to the template being ignored. It might be possible to improve the error message if no matching template is found by listing all the templates that were ignored due to constraints. Still, this would have to be done carefully, to avoid making the message even more confusing. b) constraints are simply general expressions that evaluate to a boolean. It is only the very special situation of chained &&-expressions that one could possibly "blame" a subexpression for the failure. I am not sure whether this situation justifies a special handling.
Nov 21 2011
prev sibling parent reply Kagamin <spam here.lot> writes:
Norbert Nemec Wrote:

 On 20.11.2011 21:07, Andrei Alexandrescu wrote:
 On 11/20/11 10:41 AM, Norbert Nemec wrote:
 [snip]

 Why not follow the patter of isXxx in the standard library?

 Andrei

Actually, that was my starting point. However, this approach does not scale up: A concept typically is a collection of requirements that have to be met individually. A huge boolean statement inside an assertion does not give any clue as to which part of it failed. The isXxx approach works fine for the template constraint itself, but to assert that a given type meets all requirements of a concept, it is not very usable at all.

If an assertion fails do you want the compiler to terminate or to silently proceed to the next overload?
Nov 21 2011
parent Norbert Nemec <Norbert Nemec-online.de> writes:
On 21.11.2011 13:16, Kagamin wrote:
 Norbert Nemec Wrote:

 On 20.11.2011 21:07, Andrei Alexandrescu wrote:
 On 11/20/11 10:41 AM, Norbert Nemec wrote:
 [snip]

 Why not follow the patter of isXxx in the standard library?

 Andrei

Actually, that was my starting point. However, this approach does not scale up: A concept typically is a collection of requirements that have to be met individually. A huge boolean statement inside an assertion does not give any clue as to which part of it failed. The isXxx approach works fine for the template constraint itself, but to assert that a given type meets all requirements of a concept, it is not very usable at all.

If an assertion fails do you want the compiler to terminate or to silently proceed to the next overload?

Not at all. Silently ignoring should only happen for constraints that evaluate to false - just like it does now. The list of assertions is meant for the point of implementing a concept, to verify that all requirements are individually met. As constraint, the list of assertions is packed up into a __traits(compiles,...) expression that simply turns a failing assertion into a "false" and makes the compiler silently skip and proceed.
Nov 21 2011