www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - New syntax proposal for template type parameter contraints

reply "Phil Lavoie" <maidenphil hotmail.com> writes:
Hi, I'd like to share an idea I had. It was motivated by the 
feeling I get when writing template functions and templates 
altogether. The current template constraint mechanism is awesome 
cuz it gives power and flexibility. However, I think it could be 
improved, readibilty wise. Here is my suggestion:

//A new kind of type definition: constraint. Or metatype or 
whatever.
//Can only be used where template type parameters are declared. 
It CANNOT be used as a type declaration.
constraint InputRange {

   /**
     Not really executed, just to implement the constraints. Could 
also be "invariant" or even a new type of block altogether.
   */
   static this() {
     InputRange r;             //Can use the "type" directly, or 
typeof(this).
     auto f = r.front();
     auto isEmpty = r.empty();
     r.popFront();
   }


}

//Can be used directly like an interface, but implies a template 
underneath.
void myTemplateFunction( InputRange r ) {
   foreach( elt; r ) {
     //Do something clever.
   }
}


Semantically equivalent to:

template __superManglingInputRange(T) {
   static if(
               is(
                   typeof(
                     () {
                       //Put what's in "static this" here.
                       T r;
                       auto f = r.front();
                       auto isEmpty = r.empty();
                       r.popFront();
                     }
                   )
                 )
             ) {
     enum __superManglingInputRange(T) = true
   } else {
     enum __superManglingInputRange(T) = false;
   }
}

void myTemplateFunction(T)( T r) if( __superManglingInputRange!T 
) {
   //The code.
}

The idea is to eventually be able to do something like this:

constraint InputRange(Elt) {
   Elt front();
   void popFront();
   bool empty();
}

void myTemplateFunction( InputRange!int r ) {
   foreach( elt; r ) { ... }
}

What do you think? Does this feel right to you?

Phil
May 16 2014
next sibling parent "Idan Arye" <GenericNPC gmail.com> writes:
On Friday, 16 May 2014 at 20:31:40 UTC, Phil Lavoie wrote:
 The idea is to eventually be able to do something like this:

 constraint InputRange(Elt) {
   Elt front();
   void popFront();
   bool empty();
 }

 void myTemplateFunction( InputRange!int r ) {
   foreach( elt; r ) { ... }
 }

 What do you think? Does this feel right to you?

 Phil

The main problem is that `myTemplateFunction`'s signature makes it look like it's a concrete function, when in fact it's a template. Another problem is that we now have to have an argument if we want to pass a template parameter, which is a serious limitation. How about if instead these constraint could be used in `is` expressions like type specializations? void myTemplateFunction(T)(T r) if(is(T : InputRange!int)) { foreach(elt; r) { ... } } True, the syntax is less elegant, but it's more flexible, you can easily tell that it's a template, and you can use the same syntax in static branching. Also it could be nice if a template can implement a constraint like classes implement interfaces. Unlike classes&interfaces, it shouldn't be a requirement that a template implements a constraint in order for it to be used as parameter where the constraint is expected. Rather, the compiler should check that the template fulfills the constraint even if the template is never instantiated(is this possible?). This could be a nice mechanism for verifying templates. BTW - I'm don't think we need a new keyword for this - we can use `interface template` instead.
May 16 2014
prev sibling next sibling parent "Phil Lavoie" <maidenphil hotmail.com> writes:
On Friday, 16 May 2014 at 23:14:13 UTC, Idan Arye wrote:
 On Friday, 16 May 2014 at 20:31:40 UTC, Phil Lavoie wrote:
 The idea is to eventually be able to do something like this:

 constraint InputRange(Elt) {
  Elt front();
  void popFront();
  bool empty();
 }

 void myTemplateFunction( InputRange!int r ) {
  foreach( elt; r ) { ... }
 }

 What do you think? Does this feel right to you?

 Phil

The main problem is that `myTemplateFunction`'s signature makes it look like it's a concrete function, when in fact it's a template.

True. To me that is not a big issue though. It can lead to some surprises I see your point but I would be ok with that.
 Another problem is that we now have to have an argument if we 
 want to pass a template parameter, which is a serious 
 limitation.

Not sure I get what you mean. I didn't mean for the old syntax to be abandonned if that makes sense, just to add some sugar to the existing one.
 How about if instead these constraint could be used in `is` 
 expressions like type specializations?

     void myTemplateFunction(T)(T r) if(is(T : InputRange!int)) {
       foreach(elt; r) { ... }
     }

 True, the syntax is less elegant, but it's more flexible, you 
 can easily tell that it's a template, and you can use the same 
 syntax in static branching.

It's interesting. But would it warrant a change from the usual syntax, which would probably be: void myTemplateFunction(T)(T r) if( isInputRange!(T, int)) { foreach(elt; r) { ... } } ?
 Also it could be nice if a template can implement a constraint 
 like classes implement interfaces. Unlike classes&interfaces, 
 it shouldn't be a requirement that a template implements a 
 constraint in order for it to be used as parameter where the 
 constraint is expected. Rather, the compiler should check that 
 the template fulfills the constraint even if the template is 
 never instantiated(is this possible?). This could be a nice 
 mechanism for verifying templates.

Hmm do you mean like a struct "implementing" an interface? struct MagicRange: InputRange { ... } Where it's not an actual interface but the compiler would check it against the constraint(s). Which would be nice to make sure that a type follows a given interface, although, as soon as it is used as an InputRange you would know.
 BTW - I'm don't think we need a new keyword for this - we can 
 use `interface template` instead.

Love it! Would "template interface" make more sense?
May 16 2014
prev sibling next sibling parent "Idan Arye" <GenericNPC gmail.com> writes:
On Saturday, 17 May 2014 at 00:42:35 UTC, Phil Lavoie wrote:
 On Friday, 16 May 2014 at 23:14:13 UTC, Idan Arye wrote:
 On Friday, 16 May 2014 at 20:31:40 UTC, Phil Lavoie wrote:
 The main problem is that `myTemplateFunction`'s signature 
 makes it look like it's a concrete function, when in fact it's 
 a template.

True. To me that is not a big issue though. It can lead to some surprises I see your point but I would be ok with that.

When I see such a signature as `void myTemplateFunction( InputRange!int r ) {` I'm automatically assuming I can do things like `void function(InputRange!int) foo = &myTemplateFunction;`, but in our case I can't since it's a template. Also, since it's a template, certain instantiations might cause it to crash. We have come to expect such things from templates, but here we don't expect a template. Also, I'm not very familiar with the internals of the parser, but I'm pretty sure that not being able to tell if a declaration is a template or not based on it's signature will cause a serious headache...
 Another problem is that we now have to have an argument if we 
 want to pass a template parameter, which is a serious 
 limitation.

Not sure I get what you mean. I didn't mean for the old syntax to be abandonned if that makes sense, just to add some sugar to the existing one.

void myTemplateFunction(){ InputRange!int r; r.doSomething(); } When you call `myTemplateFunction`, how do you tell it which concrete type that implements `InputRange` to use? Also: void myTemplateFunction(InputRange!int a, InputRange!int b){... Must `a` and `b` share the same type, or can they have two different types as long as the two types fulfill `InputRange!int`?
 How about if instead these constraint could be used in `is` 
 expressions like type specializations?

    void myTemplateFunction(T)(T r) if(is(T : InputRange!int)) {
      foreach(elt; r) { ... }
    }

 True, the syntax is less elegant, but it's more flexible, you 
 can easily tell that it's a template, and you can use the same 
 syntax in static branching.

It's interesting. But would it warrant a change from the usual syntax, which would probably be: void myTemplateFunction(T)(T r) if( isInputRange!(T, int)) { foreach(elt; r) { ... } }

Not as nearly as different from your proposal! At any rate, this syntax will not be used THAT much. We can create a wrapper template in Phobos that encapsulates any type that follows a constraint to create a concrete type that delegates ONLY the constraints' methods to the real struct/object. That way we can create concrete functions: void myTemplateFunction(InterfaceTemplateWrapper!(InputRange!int) r) {... That way we can make sure `myTemplateFunction` doesn't use any other methods of `r` that don't appear in `InputRange!int` but happen to be in the all concrete types we have send to the function.
 Hmm do you mean like a struct "implementing" an interface?

 struct MagicRange: InputRange {
 ...
 }

 Where it's not an actual interface but the compiler would check 
 it against the constraint(s). Which would be nice to make sure 
 that a type follows a given interface, although, as soon as it 
 is used as an InputRange you would know.

Assuming it is used as an InputRange at some template that gets instantiated. If you are writing a library there is a chance that a lot of your code is templates that only get instantiated when the library is used. Yes, you should have unit tests, but having the type system check things is better.
 BTW - I'm don't think we need a new keyword for this - we can 
 use `interface template` instead.

Love it! Would "template interface" make more sense?

In `mixin template` the word "template" comes second. Consistency is always good, and I see no benefit from breaking it here.
May 16 2014
prev sibling next sibling parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
On Fri, 16 May 2014 20:42:34 -0400, Phil Lavoie <maidenphil hotmail.com>  
wrote:

 On Friday, 16 May 2014 at 23:14:13 UTC, Idan Arye wrote:

 How about if instead these constraint could be used in `is` expressions  
 like type specializations?

     void myTemplateFunction(T)(T r) if(is(T : InputRange!int)) {
       foreach(elt; r) { ... }
     }

 True, the syntax is less elegant, but it's more flexible, you can  
 easily tell that it's a template, and you can use the same syntax in  
 static branching.

It's interesting. But would it warrant a change from the usual syntax, which would probably be: void myTemplateFunction(T)(T r) if( isInputRange!(T, int)) { foreach(elt; r) { ... } }

I just want to say, these two do not look very different. There are a couple things I want to say about these ideas without saying I agree or disagree with the proposals. These are the problems with the current system: 1. The template constraint is all or nothing. If you have a complex if-statement, and one bit fails, it's often difficult to determine what happened. 2. The template constraint is decoupled from the parameters themselves. If you have 5 parameters, you have to match the parameters to how they are constrained, and that isn't always straightforward. I agree that the original proposal does not look template-ish enough. I think we can do a lot with the existing trait templates to solve these 2 major problems. A possible solution: template interface isInputRange(T, E) { .. No change in implementation .. } void myTemplateFunction(T : isInputRange!int)(T t) { } would basically change this into the equivalent of today's constraints. however, given that the template parameter is coupled with the constraint more directly, a better error message could be created, e.g. "Type MyNonRangeType does not satisfy template interface isInputRange!int." I'm not sure the above doesn't conflict with current syntax, but I like it a hell of a lot better than the decoupled constraint afterward. -Steve
May 16 2014
prev sibling next sibling parent "Idan Arye" <GenericNPC gmail.com> writes:
On Saturday, 17 May 2014 at 01:53:46 UTC, Steven Schveighoffer 
wrote:
 On Fri, 16 May 2014 20:42:34 -0400, Phil Lavoie 
 <maidenphil hotmail.com> wrote:

 On Friday, 16 May 2014 at 23:14:13 UTC, Idan Arye wrote:

 How about if instead these constraint could be used in `is` 
 expressions like type specializations?

    void myTemplateFunction(T)(T r) if(is(T : InputRange!int)) 
 {
      foreach(elt; r) { ... }
    }

 True, the syntax is less elegant, but it's more flexible, you 
 can easily tell that it's a template, and you can use the 
 same syntax in static branching.

It's interesting. But would it warrant a change from the usual syntax, which would probably be: void myTemplateFunction(T)(T r) if( isInputRange!(T, int)) { foreach(elt; r) { ... } }

I just want to say, these two do not look very different. There are a couple things I want to say about these ideas without saying I agree or disagree with the proposals. These are the problems with the current system: 1. The template constraint is all or nothing. If you have a complex if-statement, and one bit fails, it's often difficult to determine what happened. 2. The template constraint is decoupled from the parameters themselves. If you have 5 parameters, you have to match the parameters to how they are constrained, and that isn't always straightforward. I agree that the original proposal does not look template-ish enough. I think we can do a lot with the existing trait templates to solve these 2 major problems. A possible solution: template interface isInputRange(T, E) { .. No change in implementation .. } void myTemplateFunction(T : isInputRange!int)(T t) { } would basically change this into the equivalent of today's constraints. however, given that the template parameter is coupled with the constraint more directly, a better error message could be created, e.g. "Type MyNonRangeType does not satisfy template interface isInputRange!int." I'm not sure the above doesn't conflict with current syntax, but I like it a hell of a lot better than the decoupled constraint afterward. -Steve

Good point! This is also the way you use regular interfaces(or classes) for constraints, so it shouldn't conflict with current syntax.
May 16 2014
prev sibling next sibling parent "Mason McGill" <mmcgill caltech.edu> writes:
On Saturday, 17 May 2014 at 01:53:46 UTC, Steven Schveighoffer 
wrote:
 A possible solution:

 template interface isInputRange(T, E) { .. No change in 
 implementation .. }

 void myTemplateFunction(T : isInputRange!int)(T t)
 {
 }

 would basically change this into the equivalent of today's 
 constraints. however, given that the template parameter is 
 coupled with the constraint more directly, a better error 
 message could be created, e.g. "Type MyNonRangeType does not 
 satisfy template interface isInputRange!int."

I like this, but may I suggest a slightly different approach: to parallel structures, classes, interfaces, etc., concepts could be defined as nouns. This avoids the need to define dummy type parameters ("T", above) and construct lvalues from them. It also sidesteps the awkward "is-typeof-lambda" idiom: static interface InputRange(E) { // This code must compile for any InputRange!E. bool empty = this.empty; E front = this.front; this.popFront(); } void f1(T : InputRange!int)(T) { } void f2(E, T : InputRange!E)(T) { } This parallels the existing (declarative) notion of template specialization. With this scheme, one could also overload concepts: static interface InputRange { static assert(is(typeof(this) < InputRange!E, E)); } // For the common case in which we don't need to name // the element type: void f3(T : InputRange)(T) { }
May 17 2014
prev sibling next sibling parent "Kagamin" <spam here.lot> writes:
On Friday, 16 May 2014 at 23:14:13 UTC, Idan Arye wrote:
 Another problem is that we now have to have an argument if we 
 want to pass a template parameter, which is a serious 
 limitation.

I'd say, it's not different from selection from an overload set. On Saturday, 17 May 2014 at 01:28:50 UTC, Idan Arye wrote:
 When I see such a signature as `void myTemplateFunction( 
 InputRange!int r ) {` I'm automatically assuming I can do 
 things like `void function(InputRange!int) foo = 
 &myTemplateFunction;`, but in our case I can't since it's a 
 template.

One of possible applications of this feature would be specification of constraints for variable types to improve readability: range!int r = ... So it should work like a normal type.
May 21 2014
prev sibling next sibling parent "Kagamin" <spam here.lot> writes:
On Friday, 16 May 2014 at 20:31:40 UTC, Phil Lavoie wrote:
 The idea is to eventually be able to do something like this:

 constraint InputRange(Elt) {
   Elt front();
   void popFront();
   bool empty();
 }

I have a similar idea, but I think of it as a type inference mechanism: inference template InputRange(E=Any, S=void) { static if (is(S.front)) static if (is(E==Any)||is(E==ElementType!S)) alias InputRange = S; else alias InputRange = Mismatch!"element type mismatch"; else alias InputRange = Mismatch!"need `front` member"; } Here E is optional parameter, which can be used to constrain range element type, or may be omitted to accept any element type. S is type of right hand expression. As a bonus, this template can substitute type.
May 21 2014
prev sibling parent "w0rp" <devw0rp gmail.com> writes:
On Friday, 16 May 2014 at 23:14:13 UTC, Idan Arye wrote:
 On Friday, 16 May 2014 at 20:31:40 UTC, Phil Lavoie wrote:
 void myTemplateFunction( InputRange!int r ) {
  foreach( elt; r ) { ... }
 }

 What do you think? Does this feel right to you?

 Phil

The main problem is that `myTemplateFunction`'s signature makes it look like it's a concrete function, when in fact it's a template. Another problem is that we now have to have an argument if we want to pass a template parameter, which is a serious limitation.

I have though about this kind of problem before when I was thinking of similar syntax. My solution was to require empty template parameters at least. void foo()(InputRange r); In my proposed syntax however, InputRange was not a type.
May 21 2014