Evaluation order, Oh My! #59
Replies: 4 comments 29 replies
-
I could interpret that statement two ways that are both consistent with your example. I suspect you mean #1:
the expression |
Beta Was this translation helpful? Give feedback.
-
I don't think the answer to that question is tricky at all. The code has an obvious meaning and should obviously compile! The interesting question from my point of view is: what is the evaluation order algorithm that allows us to interpret that code (and all other code we think is obvious) in the obvious way? It is of course possible that we can't agree on obviousness or that no such algorithm exists, but we should start by trying. Aside:
Applying currying is only valid if I believe that part of the key a satisfactory algorithm is viewing the evaluation of an We can begin by rewriting it as First question: is this legal code? If Second question: what should the avaluation order be? I'll try to answer that in a follow-up. |
Beta Was this translation helpful? Give feedback.
-
Yeah, I was thinking that since currying neither mutates nor has any side-effects, maybe it's a perfectly fine transformation to make.
What distinction? I think you're describing a unification, or non-distinction. Anyway, just I want the record to show that we can distinguish bound methods from method calls syntactically, and they could be interpreted differently if necessary. But it's probably not necessary. I'll have to come back to your <aside id=2> later; it's an interesting question…
In that case I believe you contradict your own previous agreement with me, because |
Beta Was this translation helpful? Give feedback.
-
I believe @kyouko-taiga and I have resolved how evaluation order works: The callee of a function call syntax is evaluated after all the other arguments, which is equivalent to a rewrite rule that lifts a binding to each argument value out of the call and into a prior statement, in left-to-right order, and then making the call on those bindings. So
where This rule entirely syntactic and applies no matter what kind of expression the callee is. In the case of an expression like The rule for evaluating operator expressions is similarly syntactically driven: Dimi, did I miss anything? |
Beta Was this translation helpful? Give feedback.
-
An interesting question has come up recently about the evaluation order of function calls.
TL;DR; what's the evaluation order of
&x.f(x.g())
and what consequences it has on the well-typedness of this expression?In Val, like in most call-by-value languages, the evaluation order is generally thought to be leftmost-innermost. That means given an expression
e
, the next sub-expression to evaluation is the innermost one found by scanninge
from left to right. Note that this evaluation order only really matters in a world where side-effects can occur. For example:According to a leftmost-innermost strategy, this program should print "a", "b" and "c" in that specific order. I speculate that it corresponds to most people's intuition.
We could leave things there in a world where functions aren't first-class. But if they are, then comes the question of the moment when the callee is evaluated relative to the arguments in a function call. For example, what should be the trace of this program:
Here, if we look at the AST, we would conclude that
make_fun()
is at the same depth asmake_arg()
. So becausemake_fun()
is left ofmake_arf()
, the former should be evaluated first. That might be a little more surprising, but I still think most people would get on board at this point. The next example may be more divisive:Should this program compile? Well, I think the answer is tricky.
An important thing to consider the meaning of
&x.f
. A reasonable way to understand a method is to see it as sugar for a free function that accepts its receiver as a hidden parameter. That's how Val actually operates under the cover: the signature ofA.f
equivalent to(inout A, Int) -> Void
. If we curry this function, we get something of the form[inout A](Int) -> Void
(that is, a function(Int) -> Void
that captures a mutable projection ofA
). Following this observation, one way to re-writemain
would be as follow:This program is ill-typed because it tries to bind
x
to initializeg
after it's been boundinout
to initializef
.Another and perhaps more straightforward way to use the free form of the method is to just call it directly without using currying. So the call
&x.f(x.g())
should be equivalent toA.f(&x, A.g(x))
. Under that interpretation, the program should still not compile becauseA.g(x)
attempts to usex
after it's been boundinout
to form the first argument. Further, we should note that this interpretation suggests&x.f
in&x.f(x.g())
) doesn't actually evaluate to a function of type(Int) -> Void
, meaning these expressions do not have the same semantics:&x.f(x.g())
(&x.f,).0(x.g())
Alternatively, we could say that
self
should come last in the parameter list, so that we evaluatex.g()
first. However, that would go against a direct application of the leftmost-innermost strategy on the original expression, i.e.,&x.f(x.g())
.What do you think should be the best strategy? What did I miss?
Personally, I think we should go with the last approach.
Beta Was this translation helpful? Give feedback.
All reactions