How to describe a tuple or list of pointers to procedures

I think I have just: a) failed to remember; and/or b) failed to find in the docs (which is almost certainly my fault) how to express the type of a procedure so as to make a tuple, array, or list of pointers to functions. Can someone point me at an example?

Hi Russel, what do you “pointers to functions” look like? Are they C functions, Chapel FCFs, … ?

In other words, is the challenge to express the type of a pointer, or bundle them up into a tuple, or …?

As a reference, a function returning a tuple might look like this:
proc makeTup(): 2*int return (1,2);

Vass

If I have:

proc some_function(x:int): int { return 0; }

then I can do:

const functions = (some_function,);

and the compiler determines the type of functions for me. But what if I want to explicitly specify the type of functions?

Hi @Russel —

func() is currently used to describe a function type in Chapel, so the type of some_function would be func(int, int) indicating that it takes and returns an int (the final type argument being the return value). You can read more about this feature here: https://chapel-lang.org/docs/technotes/firstClassFns.html#specifying-the-type-of-a-first-class-function

For your specific example, you could therefore write:

const functions: ((func(int, int),) = (some_function,);

or alternatively:

const functions: 1*func(int, int) = (some_function,);

And now my standard caveat: Use of first-class functions like this is a feature that has not received much attention in Chapel’s development in recent years. Users seem to be able to do quite a lot with it in spite of the inattention, but you may run into the limits of what works (or works well) today in this area of the language (and you should feel free to give a shout if/when you do).

-Brad

1 Like

@bradcray, given the overloaded definitions:

proc iterative(n: bigint): bigint {
  assert(n >= 0);
  var total = new bigint(1);
  for i in one..n do total *= i;
  return total;
}

proc iterative(n: int): bigint {
  assert(n >= 0);
  var total = new bigint(1);
  for i in 1..n do total *= i;
  return total;
}

I tried:

const fs_tuple: 1 * func(int, int) = (
  iterative,
);

but this leads to the error message:

test/Factorial_Chapel_Test.chpl:22: error: iterative: can not capture overloaded functions as values

just as if I had not specified a type for fs_tuple. Also using tuples with explicit types means having to replicate the information of the tuple entry count. So I tried using an array:

const fs_array: [func(int, int)] = [
  iterative,
];

but that leads to the error message:

test/Factorial_Chapel_Test.chpl:25: syntax error: near '='

Removing the explicit type makes then leads to the error:

test/Factorial_Chapel_Test.chpl:26: error: iterative: can not capture overloaded functions as values

just as with the tuple case.

Unless you are doing some form of “compare and contrast” or old-style (aka 1980s) generics I can see that you can probably end up never using pointers to functions. So I can see most Chapel users never needing them. In my case here it is about testing lots of implementations of the same function, not the average use of Chapel per se.

Ah, looks like the array version should have been:

const fs_array: [1] func(int, int) = [
  iterative,
];

which has the same problem of having to replicate data about the number of items. Also it results in the error:

test/Factorial_Chapel_Test.chpl:26: error: iterative: can not capture overloaded functions as values

which is bringing on the conclusion that the type system cannot be used to “condition” the selection from a set of overloaded functions.

Sad that it seems that:

const fs_array: [] func(int, int) = [
  iterative,
];

is not legal code.

test/Factorial_Chapel_Test.chpl:25: syntax error: near ']'

Hi Russel —

This is behaving as currently intended. Specifically, Chapel evaluates expressions like iterative or (iterative,) independently of the larger context in which they appear. So the evaluation of the declared type on fs_tuple would only come afterwards to make sure that the initialization expression was compatible with the declared type. It doesn't influence the evaluation of the initialization expression itself. Equivalently, if I wrote var x: int = foo(); and had two 0-argument overloads of foo(), one that returned int and the other of which returned real, the compiler would throw up its hands rather than disambiguating based on the context of what was going to happen to foo() after it was called.

So when the compiler sees iterative, it tries to determine which function you're referring to, finds multiple overloads, and doesn't know where to go from there, so issues the error.

I think it might be interesting to consider expanding Chapel's notion of first-class functions to refer not just to a single function as it currently does, but to a family of function overloads, such that any call through fs(0)(myActualArg) would essentially result in a dynamic dispatch to determine which overload was used. If that were of interest, I think it'd be worth a GitHub issue to request the feature. But currently, the behavior in this respect is closer to that of a C function pointer where if there isn't a single function to refer to, we throw up our hands.

That said, you seem more interested in referring to a specific overload than in paying the cost of a dynamic dispatch, which would suggest needing some way of specifying which copy of iterative you're referring to. But I think to be true to the language, that would need to occur via further decorations on the RHS reference to iterative to disambiguate at the time it's being evaluated, and don't have any great ideas about how to do that offhand. Are you familiar with other languages that support function overloads and taking a pointer/reference to a specific overload that would serve as inspiration here?

Given your motivation to test lots of implementations of the same function, I wonder whether it would be more straightforward to do something more like the following:

proc runTest(arg) {
  iterative(arg);
  iterative(arg+1);
 ...whatever...
}

runTest(42);
runTest(new bigint(42));
etc.

That is, rather than trying to find a way to pass a specific function into the test jig, write the test jig itself in a very generic way and rely on the existing language support for generics and overload resolution to run each version as you pass in different argument types?

Of course, you could also wrap each overload with a unique function name, like:

proc bigintIterative(n: bigint): bigint {
  return iterative(n);
}

proc intIterative(n: int): bigint {
  return iterative(n);
}

and then set up your tuple in terms of the specific case you want:

const fs_tuple: 1 * func(int, int) = (
  intIterative,
);

but that's obviously a lot more typing...

Not deeply entangled with this discussion, but the problem with your array case:

const fs_array: [func(int, int)] = [
  iterative,
];

is that in Chapel the array element type comes after the square brackets. So writing this as:

const fs_array: [1..1] func(int, int) = [
  iterative,
];

ought to work.

1 Like

Regarding this note:

I think we agree with that. See Support inferred-size arrays via partially bounded ranges · Issue #10596 · chapel-lang/chapel · GitHub (which is slightly different in that it requires you to specify whether you want your array to be 1-based or 0-based or something else... But we could also support completely inferred size arrays, which would probably default to 0-based indexing like other features.

1 Like

I already went the route of creating procedures in the test code to enforce call site type evaluation as per your intIterative and bigintIterative :slight_smile: . [But I have no actual proof of that since I have not been committing the steps to date!] It works fine, all the tests pass – using my SCons build anyway, I await Chapel 1.23.0 release to see if Mason can compile and run the tests.

Having the wrapper procedures isn’t really an overhead since this is test code, but it gives the appearance of an overhead. Having to manually disambiguate overloads is not unusual in languages supporting overloading, so Chapel is definitely not an outlier here. My feeling is that if application code isn’t requiring the need to select overloads except by call site type checking, then test code shouldn’t be a driver for the feature, given it is easy to create the wrapper procedure.

1 Like

In case anyone is interested the code driving things here is at https://github.com/russel/Factorial/tree/master/Chapel It is a work in progress. The next issue is how to make things work for more different integral types.

1 Like