When default arguments are combined with promotions, how many times should the default argument expression be run? Just once, or once per promoted element?
For example:
proc makeit() {
writeln("IN makeit");
return 1;
}
proc foo(x:int, y=makeit()) {
return x+y;
}
var A = [1,2,3,4,5,6,7,8,9];
writeln(A);
var B = foo(A);
writeln(B);
could print out "IN makeit" only once or it could print it out 10 times.
A program like the following could work either way, since the default argument depends on a previous argument, which itself is a promoted array - meaning that the default argument is also promoted.
proc bar(x:int) {
return x + 1;
}
proc foo(x:int, y:int = bar(x)) {
assert(x+1==y);
return x+y;
}
var A = [1,2,3,4,5,6,7,8,9];
writeln(A);
var B = foo(A);
writeln(B);
A worrying pattern would be something like this:
class C { var x:int; }
proc foo(ref x:C, y:C = new C(1)) {
if x == nil then x = y;
}
var A:[1..3] C;
foo(A);
A[1].x = 100;
writeln(A); // are these all pointing to the same C instance?
While that example is contrived... it might be more plausible than programs that do output in the promoted expression. In any case it seems to me that either behavior would be OK for that example, provided the specification is clear.
Anybody have insight as to whether or not just running the default expression once in these cases is sufficient - or if it needs to be run many times. Master runs it many times right now, but I don't yet know if that was intentional or merely an artifact of the implementation strategy.
It seems to me - unless we have a good reason to run these many times - running them once would be more efficient.
I view promotion as just sugar over explicitly using a forall. If you had something like
forall (a, b) in zip(A, B) do
b = foo(a);
I would expect makeit to be executed for each iteration (as an optimization if we could prove that makeit() was "pure" I think it'd be ok to only evaluate it once, but in this case since it's not pure, I would expect to see "IN makeit" printed for each itertion)
This concisely captures my view.
On Nov 29, 2017, at 1:19 PM, Elliot Ronaghan <[email protected]notifications@github.com> wrote:
I view promotion as just sugar over explicitly using a forall. If you had something like
forall (a, b) in zip(A, B) do
b = foo(a);
I would expect makeit to be executed for each iteration (as an optimization if we could prove that makeit() was "pure" I think it'd be ok to only evaluate it once, but in this case since it's not pure, I would expect to see "IN makeit" printed for each itertion)
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHubhttps://github.com/chapel-lang/chapel/issues/7889#issuecomment-347999665, or mute the threadhttps://github.com/notifications/unsubscribe-auth/ACERpjxLKBECmI1qIaYKX5ijPFFBSveRks5s7cpugaJpZM4Qvjj8.
I view promotion as just sugar over explicitly using a forall.
I agree with this philosophy. Additionally, when the default promotion behavior is not desirable, one can redefine it with an array overload, e.g. #7180
I agree 100% with @ronawho's response as well (and he claims not to be a language design person... sheesh!)
I don't disagree with anything any of you said, but I don't really feel it answers the question. I'm happy to learn about people's intuition, but I don't feel I've clearly communicated the question.
For this comment to be reasonably complete, I'm going to repeat a variant of the example code from earlier:
proc makeit() {
writeln("IN makeit");
return 1;
}
proc foo(x:int, y=makeit()) {
return x+y;
}
var A = [1,2,3,4,5,6,7,8,9];
foo(A);
The spec says about default arguments, in section 13.4.2:
If the actual argument is omitted from the function call, the default expression is evaluated when the function call is made and the evaluated result is passed to the formal argument as if it were passed from the call site.
That might lead me to expect that foo(x) should work the same as foo(x, makeit()) (i.e. as if the user literally filled in the default argument).
Meanwhile, we have an understanding that "promotion is equivalent to explicitly using a forall" [1]. That's fine, but now we have two conceptual equivalencies, and the order in which we apply them matters.
So let's consider the call to foo(A) at the end of the above program.
Do we mentally do the "default argument" transformation before the "promotion" one? Then we get this:
chpl
foo(A)
chpl
foo(A, makeit())
chpl
var tmp=makeit();
forall a in A do
foo(a, tmp);
Or, do we mentally do the "promotion" transformation before the "default argument" one?
chpl
foo(A)
chpl
forall a in A do
foo(a);
chpl
forall a in A do
foo(a, makeit());
I just don't feel that "promotion is equivalent to a forall" indicates in any way which of these two conceptual transformation sequences is the one we should use for the language. Both of these options make use of that fact.
Is this choice completely arbitrary? Well, I don't think existing Chapel programs care about the difference - as I was able to get PR #7858 (commit def5dda) to pass standard testing with the behavior swapped from how master does it. That leads me to believe that the current behavior is an implementation consequence only (in particular - you'd think we'd have a test to "lock in" the behavior if we actually made a decision here).
footnotes:
[1] This is @ronawho's comment but I removed the term "just sugar" since I have a problem with it. Also note that this understanding is absent from spec section 26.3 - and other than by being in the "data parallel" section, that spec section doesn't even say the promoted operation will be generally parallel.
Intuitively (and my intuition could be wrong), I expect promotion to behave like a forall (or a for if a parallel iterator doesn't exist). I also expect default argument intents to behave as if you passed whatever the default is to a function.
In code, expect that:
proc foo(a, b=defaultB()) { }
foo(a);
is equivalent to:
proc foo(a, b=defaultB()) { }
foo(a, defaultB());
So I'd expect that :
proc foo(a, b=defaultB()) { }
var A = [1,2,3,4,5,6,7,8,9];
foo(A);
is equivalent (assuming there's a parallel iterator) to:
proc foo(a, b=defaultB()) { }
var A = [1,2,3,4,5,6,7,8,9];
forall a in A do
foo(a, defaultB());
That seems the right behavior to me, and if a user doesn't want that behavior, they can always opt-out of it by doing:
proc foo(a, b=defaultB()) { }
var A = [1,2,3,4,5,6,7,8,9];
var tmp = defaultB();
foo(A, tmp);
I agree with @ronawho's response again. I also tend to think that the "promotion first vs. default argument first" question that @mppf posed is too dependent on an understanding/assumption about how the compiler implements things. I.e., I'd say that default argument first approach should be as follows in the user's mind:
Do we mentally do the "default argument" transformation before the "promotion" one? Then we get this:
- original program
chpl foo(A)- after default argument transformation
chpl foo(A, makeit())- after promotion transformation
chpl forall a in A do foo(a, makeit());
and that the compiler's use of temps (or not) should fit this vision, not the other way around.
A side conversation I had with @bradcray pointed out that it might not be obvious to everyone on this issue, but if you literally write foo(A, makeit()), makeit() only runs once on master now. I think this might have been the source of some of our not understanding each other.
E.g.
proc makeit() {
writeln("IN makeit");
return 1;
}
proc foo(x:int, y:int) {
return x+y;
}
var A = [1,2,3,4,5,6,7,8,9];
foo(A, makeit());
when run only outputs
IN makeit
People may have been thinking that foo(A, makeit()) would run makeit() once per array element. I had been assuming that it running makeit() only once was a choice we carefully made at some point in the past. Maybe that isn't the right choice though... Issue #7904 asks that question, independent of default arguments.
I think @ronawho is starting to say "If it runs many times, the user can get the alternative with an explicit temporary. How would they get the alternative behavior if we made the other choice?"
Let's suppose we decided that defaultB() should run once in the below code:
proc foo(a, b=defaultB()) { }
var A = [1,2,3,4,5,6,7,8,9];
foo(A);
If the user wants the alternative where it runs once per array element, they write a forall statement or expression:
proc foo(a, b=defaultB()) { }
var A = [1,2,3,4,5,6,7,8,9];
forall a in A do
foo(a);
@ronawho already showed how to get "run it once" if we make the other language design choice. So, either way, users will be able to explicitly write the other option.
Independent of issue #7904 (which definitely contributed to my sense of "why isn't this obvious?" above), it still seems very weird to me to say that the default argument would only be evaluated once in a promoted context. Specifically, if you showed me:
proc foo(a, b=defaultB()) { }
var a: real;
forall i in 1..n do
foo(a);
and told me that defaultB() wasn't invoked n times (in an unoptimized/unoptimizable case), I'd be shocked. So similarly, to suggest that:
var A: [1..n] real;
foo(A);
where my mental model is that this is going to be rewritten by the compiler as:
forall a in A do
foo(a);
would only invoke it once seems surprising... So that suggests to me that "promote first, deal with default arguments next" is more correct / less surprising.
I would draw a parallel between "promote first, deal with default arguments next" and @cassella's comment in #7868 about default arguments vs. virtual dispatch, where we want to dispatch first, add default arguments next.
So maybe the user view of a default argument is that it creates implicitly another function? I.e.
proc foo(a, b=defaultB()) ... implicitly defines TWO functions, proc foo(a) and proc foo(a,b)? (Yes, two default arguments would define four functions, etc.)
This view implies the desired semantics w.r.t. visibility, dispatch and promotion.
I think there's enough reason here to make the default argument be computed many times in the promoted context.
No matter what we decide to do with issue #7904 (Should a call expression in a promoted call be run once or many times in a promoted context?), the many-times interpretation for default arguments is conceptually reasonable; but the only-once interpretation for default arguments wouldn't be possible as a composition of behaviors if we decided to run the call expression in #7904 many times.
Besides that, there were a variety of (strong) reactions from other developers in favor of the default argument expression running many times in a promoted context. As might have been apparent from the discussion, I had trouble understanding the justification for these, but it I think the reaction was strong enough that it doesn't matter so much (even if it comes down to intuition or taste).
So, I've adjusted my ongoing PR to preserve the behavior of master, which is "run default argument expressions many times in promoted expressions".
Thanks for the discussion!
Most helpful comment
I view promotion as just sugar over explicitly using a forall. If you had something like
I would expect
makeitto be executed for each iteration (as an optimization if we could prove that makeit() was "pure" I think it'd be ok to only evaluate it once, but in this case since it's not pure, I would expect to see "IN makeit" printed for each itertion)