digitalmars.D - Concepts vs template constraints - the practical approach
- Norbert Nemec (50/50) Nov 20 2011 Hi there,
- Denis Shelomovskij (8/12) Nov 20 2011 It should be
- Norbert Nemec (4/16) Nov 20 2011 Indeed - that way you win a meaningful error message but you loose the
- Denis Shelomovskij (53/53) Nov 20 2011 Your example, a bit improved:
- Norbert Nemec (3/56) Nov 20 2011 Nice! That really improves things! Looking forward to playing with it a
- =?UTF-8?B?QWxpIMOHZWhyZWxp?= (51/101) Nov 20 2011 I have separated the parts of the concepts below and then defined
- Norbert Nemec (15/40) Nov 20 2011 Hi Ali,
- =?UTF-8?B?QWxpIMOHZWhyZWxp?= (9/55) Nov 20 2011 dmd 2.056 with the code that I've tried, does provide which individual
- Norbert Nemec (7/17) Nov 20 2011 True, there is some additional detail in some cases, but if you look
- Andrei Alexandrescu (4/4) Nov 20 2011 On 11/20/11 10:41 AM, Norbert Nemec wrote:
- Norbert Nemec (9/13) Nov 20 2011 Actually, that was my starting point. However, this approach does not
- Tobias Pankrath (2/12) Nov 21 2011 Wouldn't it be preferable to improve the compiler to a degree, that it c...
- Norbert Nemec (11/23) Nov 21 2011 Two problems:
- Kagamin (2/20) Nov 21 2011 If an assertion fails do you want the compiler to terminate or to silent...
- Norbert Nemec (8/28) Nov 21 2011 Not at all. Silently ignoring should only happen for constraints that
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
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
On 20.11.2011 19:58, Denis Shelomovskij wrote: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... :-(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
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
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
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, NorbertHere 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
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
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, NorbertAliOn 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
On 20.11.2011 21:03, Ali Çehreli wrote:On 11/20/2011 11:36 AM, Norbert Nemec wrote: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.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)
Nov 20 2011
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
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? AndreiActually, 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
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
On 21.11.2011 09:24, Tobias Pankrath wrote: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.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
Norbert Nemec Wrote:On 20.11.2011 21:07, Andrei Alexandrescu wrote:If an assertion fails do you want the compiler to terminate or to silently proceed to the next overload?On 11/20/11 10:41 AM, Norbert Nemec wrote: [snip] Why not follow the patter of isXxx in the standard library? AndreiActually, 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 21 2011
On 21.11.2011 13:16, Kagamin wrote:Norbert Nemec Wrote: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.On 20.11.2011 21:07, Andrei Alexandrescu wrote:If an assertion fails do you want the compiler to terminate or to silently proceed to the next overload?On 11/20/11 10:41 AM, Norbert Nemec wrote: [snip] Why not follow the patter of isXxx in the standard library? AndreiActually, 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 21 2011