www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Best Way to Pass Template Typed Alias Parameters for Functions?

reply Vijay Nayar <madric gmail.com> writes:
I have a few cases where I would like to pass in a function as a 
value to a template, but I want to ensure that the function takes 
certain kinds of parameters, is a const function, or matches any 
other set of conditions.

What is the best way to do this? Just to avoid any potential 
confusion, I've included a few examples below. I'm using explicit 
functions rather than lambdas just avoid any possibility of types 
being incorrectly inferred.

size_t myHashFunction(int a) { return cast(size_t) a; }

void main() {
     class A(KeyT, HashF) { }
     auto a = new A!(int, myHashFunction);
     // ^ onlineapp.d: Error: template instance `A!(int, 
myHashFunction)`
     // does not match template declaration `A(KeyT, HashF)`

     // Alias parameter: 
https://dlang.org/spec/template.html#aliasparameters
     class B(KeyT, alias HashF) { }
     auto b = new B!(int, myHashFunction);
     // ^ Works, but we cannot enforce the kind of function passed 
in.

     // Typed alias parameter: 
https://dlang.org/spec/template.html#typed_alias_op
     class C(KeyT, alias size_t function(KeyT) HashF) { }
     auto c = new C!(int, myHashFunction);
     // ^ onlineapp.d: Error: template instance `C!(int, 
myHashFunction)`
     // does not match template declaration `C(KeyT, alias size_t 
function(KeyT) HashF)`

     // Specialization: 
https://dlang.org/spec/template.html#alias_parameter_specialization
     class D(KeyT, alias HashF : size_t function(KeyT)) { }
     auto d = new D!(int, myHashFunction
     // ^ onlineapp.d: Error: template instance `D!(int, 
myHashFunction)`
     // does not match template declaration `D(KeyT, alias HashF : 
size_t function(KeyT))`
}

Looking at some of the code in std.algorithm, it seem that static 
asserts are sometimes used for this purpose. Does anyone have a 
solution to this problem of instantiating template classes whose 
parameters are functions?  I have used std.function "unaryFun" 
before, but this has the problem of not being able to specify 
function attributes, like const.
Dec 23 2018
parent reply Alex <sascha.orlov gmail.com> writes:
On Sunday, 23 December 2018 at 17:13:49 UTC, Vijay Nayar wrote:
 I have a few cases where I would like to pass in a function as 
 a value to a template, but I want to ensure that the function 
 takes certain kinds of parameters, is a const function, or 
 matches any other set of conditions.

 What is the best way to do this? Just to avoid any potential 
 confusion, I've included a few examples below. I'm using 
 explicit functions rather than lambdas just avoid any 
 possibility of types being incorrectly inferred.

 size_t myHashFunction(int a) { return cast(size_t) a; }

 void main() {
     class A(KeyT, HashF) { }
     auto a = new A!(int, myHashFunction);
     // ^ onlineapp.d: Error: template instance `A!(int, 
 myHashFunction)`
     // does not match template declaration `A(KeyT, HashF)`

     // Alias parameter: 
 https://dlang.org/spec/template.html#aliasparameters
     class B(KeyT, alias HashF) { }
     auto b = new B!(int, myHashFunction);
     // ^ Works, but we cannot enforce the kind of function 
 passed in.

     // Typed alias parameter: 
 https://dlang.org/spec/template.html#typed_alias_op
     class C(KeyT, alias size_t function(KeyT) HashF) { }
     auto c = new C!(int, myHashFunction);
     // ^ onlineapp.d: Error: template instance `C!(int, 
 myHashFunction)`
     // does not match template declaration `C(KeyT, alias 
 size_t function(KeyT) HashF)`

     // Specialization: 
 https://dlang.org/spec/template.html#alias_parameter_specialization
     class D(KeyT, alias HashF : size_t function(KeyT)) { }
     auto d = new D!(int, myHashFunction
     // ^ onlineapp.d: Error: template instance `D!(int, 
 myHashFunction)`
     // does not match template declaration `D(KeyT, alias HashF 
 : size_t function(KeyT))`
 }

 Looking at some of the code in std.algorithm, it seem that 
 static asserts are sometimes used for this purpose. Does anyone 
 have a solution to this problem of instantiating template 
 classes whose parameters are functions?  I have used 
 std.function "unaryFun" before, but this has the problem of not 
 being able to specify function attributes, like const.
I assume you are looking for constraints... https://dlang.org/concepts.html Then, case B is the way to go, see https://run.dlang.io/is/jWU3tr Case D also looks promising. Not sure, how to formulate the specialization right now... There are a lot of traits you can use for the constraints. https://dlang.org/phobos/std_traits.html and https://dlang.org/spec/traits.html e.g., for constness there is getFunctionAttributes, which could be useful.
Dec 23 2018
parent reply Vijay Nayar <madric gmail.com> writes:
On Sunday, 23 December 2018 at 18:04:32 UTC, Alex wrote:
 On Sunday, 23 December 2018 at 17:13:49 UTC, Vijay Nayar wrote:
 I have a few cases where I would like to pass in a function as 
 a value to a template, but I want to ensure that the function 
 takes certain kinds of parameters, is a const function, or 
 matches any other set of conditions.
I assume you are looking for constraints... https://dlang.org/concepts.html Then, case B is the way to go, see https://run.dlang.io/is/jWU3tr Case D also looks promising. Not sure, how to formulate the specialization right now... There are a lot of traits you can use for the constraints. https://dlang.org/phobos/std_traits.html and https://dlang.org/spec/traits.html e.g., for constness there is getFunctionAttributes, which could be useful.
I've been playing with the idea of specifying the constraints or using "static assert" in the constructor. They are good options, but there's a few cases where they fall a bit short. For example, imagine you have a container class that needs a comparator function to be able to compare the items in the container. While you can use std.traits to assure the right kind of function is passed in, that information does not make its way into the type system. For example, if you have a const function in your container like "T find() const", and this function needs to use that comparator, then you're out of luck because the compiler doesn't know that the comparator function is const and will not modify the objects being compared. Last time I ran into this problem, my solution was simply to give up on const. But now I'm running into it again and trying to think through it again before giving up again.
Dec 23 2018
parent reply Alex <sascha.orlov gmail.com> writes:
On Sunday, 23 December 2018 at 18:13:25 UTC, Vijay Nayar wrote:
 I've been playing with the idea of specifying the constraints 
 or using "static assert" in the constructor. They are good 
 options, but there's a few cases where they fall a bit short.  
 For example, imagine you have a container class that needs a 
 comparator function to be able to compare the items in the 
 container.  While you can use std.traits to assure the right 
 kind of function is passed in, that information does not make 
 its way into the type system.

 For example, if you have a const function in your container 
 like "T find() const", and this function needs to use that 
 comparator, then you're out of luck because the compiler 
 doesn't know that the comparator function is const and will not 
 modify the objects being compared.

 Last time I ran into this problem, my solution was simply to 
 give up on const. But now I'm running into it again and trying 
 to think through it again before giving up again.
Hm... still not sure... ;) This would compile and run: ´´´ import std.experimental.all; size_t myHashFunction(int a) { return cast(size_t) a; } void main() { auto b = new B!(int, myHashFunction); b.arr = 42.iota.array; assert(b.find == 1); } class B(T, alias HashF) { T[] arr; T find() const { foreach(el; arr) { if(HashF(el)) { return el; } } assert(0); } } ´´´
Dec 23 2018
parent reply Vijay Nayar <madric gmail.com> writes:
On Sunday, 23 December 2018 at 18:31:24 UTC, Alex wrote:
 On Sunday, 23 December 2018 at 18:13:25 UTC, Vijay Nayar wrote:
 For example, if you have a const function in your container 
 like "T find() const", and this function needs to use that 
 comparator, then you're out of luck because the compiler 
 doesn't know that the comparator function is const and will 
 not modify the objects being compared.

 Last time I ran into this problem, my solution was simply to 
 give up on const. But now I'm running into it again and trying 
 to think through it again before giving up again.
Hm... still not sure... ;) This would compile and run: ´´´ import std.experimental.all; size_t myHashFunction(int a) { return cast(size_t) a; } void main() { auto b = new B!(int, myHashFunction); b.arr = 42.iota.array; assert(b.find == 1); } class B(T, alias HashF) { T[] arr; T find() const { foreach(el; arr) { if(HashF(el)) { return el; } } assert(0); } } ´´´
You're right, it does compile. I'm a bit surprised. I wonder if this is a relatively recent improvement in the language, because last time I ran into this I had no such luck. But after seeing that your example did work, I figured one could try to get the best of both worlds by using a strongly-typed wrapper function in one's class. So far it seems to work: import std.traits; class A(KeyT, alias HashF) { // Strongly-typed wrapper around template value parameter 'HashF'. static size_t hash(in KeyT key) { return HashF(key); } static this() { static assert(isCallable!HashF, "Hash function is not callable!"); static assert(Parameters!(HashF).length == 1, "Hash function must take 1 argument."); static assert(is(Parameters!(HashF)[0] : const(KeyT)), "Hash parameter must be const."); static assert(is(typeof(HashF(KeyT.init)) : size_t), "Hash function must return size_t type."); } KeyT data; size_t getHash() const { return hash(data); } } void main() { auto a = new A!(int, (int a) => cast(size_t) a); a.data = 5; a.getHash(); }
Dec 23 2018
parent reply Alex <sascha.orlov gmail.com> writes:
On Sunday, 23 December 2018 at 18:53:15 UTC, Vijay Nayar wrote:
 You're right, it does compile. I'm a bit surprised. I wonder if 
 this is a relatively recent improvement in the language, 
 because last time I ran into this I had no such luck. But after 
 seeing that your example did work, I figured one could try to 
 get the best of both worlds by using a strongly-typed wrapper 
 function in one's class.  So far it seems to work:

 import std.traits;

 class A(KeyT, alias HashF) {
   // Strongly-typed wrapper around template value parameter 
 'HashF'.
   static size_t hash(in KeyT key) {
     return HashF(key);
   }
   static this() {
     static assert(isCallable!HashF, "Hash function is not 
 callable!");
     static assert(Parameters!(HashF).length == 1, "Hash 
 function must take 1 argument.");
     static assert(is(Parameters!(HashF)[0] : const(KeyT)),
         "Hash parameter must be const.");
     static assert(is(typeof(HashF(KeyT.init)) : size_t),
         "Hash function must return size_t type.");
   }

   KeyT data;
   size_t getHash() const {
     return hash(data);
   }
 }

 void main() {
     auto a = new A!(int, (int a) => cast(size_t) a);
     a.data = 5;
     a.getHash();
 }
I'm not sure, whether you need the static this() part at all, as all of the asserts the compiler should do even when they are absent... by isCallable you restrict the HashF to not use IFTI by calling HashF(key) you ensure implicitely, that Parameters!(HashF).length == 1 by having hash(in KeyT key) defined with an "in" you ensured, that HashF does not mutate the argument and by defining size_t getHash() you ensured the return type of HashF...
Dec 23 2018
parent Vijay Nayar <madric gmail.com> writes:
On Sunday, 23 December 2018 at 19:10:06 UTC, Alex wrote:
 On Sunday, 23 December 2018 at 18:53:15 UTC, Vijay Nayar wrote:
 You're right, it does compile. I'm a bit surprised. I wonder 
 if this is a relatively recent improvement in the language, 
 because last time I ran into this I had no such luck. But 
 after seeing that your example did work, I figured one could 
 try to get the best of both worlds by using a strongly-typed 
 wrapper function in one's class.  So far it seems to work:

 import std.traits;

 class A(KeyT, alias HashF) {
   // Strongly-typed wrapper around template value parameter 
 'HashF'.
   static size_t hash(in KeyT key) {
     return HashF(key);
   }
   static this() {
     static assert(isCallable!HashF, "Hash function is not 
 callable!");
     static assert(Parameters!(HashF).length == 1, "Hash 
 function must take 1 argument.");
     static assert(is(Parameters!(HashF)[0] : const(KeyT)),
         "Hash parameter must be const.");
     static assert(is(typeof(HashF(KeyT.init)) : size_t),
         "Hash function must return size_t type.");
   }

   KeyT data;
   size_t getHash() const {
     return hash(data);
   }
 }

 void main() {
     auto a = new A!(int, (int a) => cast(size_t) a);
     a.data = 5;
     a.getHash();
 }
I'm not sure, whether you need the static this() part at all, as all of the asserts the compiler should do even when they are absent... by isCallable you restrict the HashF to not use IFTI by calling HashF(key) you ensure implicitely, that Parameters!(HashF).length == 1 by having hash(in KeyT key) defined with an "in" you ensured, that HashF does not mutate the argument and by defining size_t getHash() you ensured the return type of HashF...
You are correct again! Playing around with using classes and functions returning the wrong type or not having a const argument, it seems that the compiler will assure that all the conditions needed by the wrapper function are satisfied and give a clear error. I don't know when this happened, but this definitely makes the language a lot easier to use for these circumstances, and all the std.traits stuff in "static this()" can be thrown out.
Dec 23 2018