Well, I’m glad to see that even with my writer’s block, people still seem to be reading the blog! Although there is definitely a diversity of opinion, the majority of people seem to prefer the “Function” syntax of the choices I laid out, which is not exactly what we expected. (We were wagering people would go for the more cryptic, compact syntax. Shows what we know…) That’s the syntax you should expect to see in the beta, and if public opinion shifts over time in the beta, we’ll deal with that feedback if and when we come to it. After all, that’s what a beta is for…
The other major topic to talk about with lambda expressions is closures. I don’t have a particular question this time, just an update on a topic we discussed about nine months ago. When I talked about closures back March, I raised the question of exactly how VB should treat local variables when lifting them into closures. I won’t rehash the entire discussion here–you can just go back and read the original entry itself–but the problem boiled down to something like this:
Module Module1 Delegate Function AddDelegate(ByVal x As Integer) As Integer Sub Main() Dim lambdas(9) As AddDelegate For i As Integer = 0 To 9 Dim y As Integer = i lambdas(i) = Function(x) x + y Next For Each l As AddDelegate In lambdas Console.WriteLine(l(0)) Next Console.ReadLine() End Sub End Module
This is the same example as the previous entry on closures, except that instead of using queries, I’m using lambda expressions directly. What I’m doing here is filling an array with lambda expressions that are supposed to add a particular value to the parameter. So lambdas(0) will add 0 to the parameter, lambdas(1) will add 1 to the parameter, etc. At least, that’s the intent. But now we run into the closure question that I asked originally–should each iteration get it’s own copy of the local variable y, or should they all share the same copy of y? If the former, I get the intended semantics. If the latter, then every lambda adds 9 to the parameter (because they share the same y and the final value of y is 9).
Just to make the problem clear, let’s look at an equally valid way (in VB) of writing the same code:
Module Module1 Delegate Function AddDelegate(ByVal x As Integer) As Integer Sub Main() Dim lambdas(9) As AddDelegate For i As Integer = 0 To 9 Dim y As Integer lambdas(i) = Function(x) x + y y += 1 Next For Each l As AddDelegate In lambdas Console.WriteLine(l(0)) Next Console.ReadLine() End Sub End Module
Now instead of initializing y at the beginning of each iteration of the loop, I’m just incrementing it and letting the value carry over from one iteration of the loop to another. Now maybe we start to see the problem–if each iteration of the loop gets its own copy of y, then that seems to conflict with the idea that the value of the variable carries over from one iteration of the loop to another. We’re trying to eat our cake and have it too.
What we ended up deciding was to split the difference. It’s a little more complex that what you get in C#, for example, but it should give everyone the semantics they expect and not break any existing code (always a plus, in my book). Basically, we are going to treat the lifetime of local variables to be the scope that they are declared in. Thus, when you create a closure in a loop, each iteration of the loop does, indeed, get its own copy of the local variable. But to preserve the existing semantics, we’re going to add a caveat: when creating a new local variable, if a previous version of that local variable exists, we copy the value from that previous version into the newly created local variable. So the value of a local variable declared in a loop carries over from one iteration to the next. So lambdas work the way most people expect, and existing code continues to run as expected.
As with previous entries on closures, kudos to those who’ve bothered to read this far. It’s kind of arcane and, we believe, most people won’t ever have to think at all about the special semantics–things will just work.
I’m confused about exactly where the "y" in "Function(x) x + y" gets evaluated. Let’s consider a simpler example:
Dim lambda1 As AddDelegate
Dim y As Integer
y = 0
lambda1 = Function(x) x + y
y = 1
Would the output of this code be 0 or 1? I’m assuming from the discussion about closures and variable lifetimes that the output would be 1, by design. Is there any particular reason why the design is to evaluate "y" during lambda _execution_ rather than lamdba _construction_? If "y" were evaluated during lamdba construction, then doesn’t the whole closure lifetime problem go away?
OK, on further thought, the design makes sense, especially if the lambda expression modifies "y" by passing it ByRef to some other function:
Function Foo(ByRef a As Integer) As Integer
a += 1
y = 0
lambda1 = Function(x) x + Foo(y)
y = 1
This should print:
Capturing the value of "y" in a closure during lamdba construction probably defeats the whole purpose of lambda expressions. 🙂
So, when are closures created? On each Dim or on each entry to a new scope? What would the following do:
Dim lambdas(1) As AddDelegate
Dim i As Integer
lambdas(i) = Function(x) x + i
i += 1
If i <= 1 Then Goto label
For Each l As AddDelegate In lambdas
What would the output be?:
Delegate Function AddDelegate(ByVal x As Integer) As Integer
Dim lambdas(9) As AddDelegate
Dim lambdas2(9) As AddDelegate
Dim z As Integer
For i As Integer = 0 To 9
Dim y As Integer
z += 2
lambdas(i) = Function(x) x + y + z
y += 1
z += 2
lambdas2(i) = Function(x) z += x + y
y += 1
For i As Integer = 0 to 9
Dim func As AddDelegate = lambdas(i)
func = lambdas2(i)
* Why change the behavior of y in the loop above? I don’t see why it can’t behave
exactly as VB8, and retain its value. This is different than other languages, but not
any less "correct". If anything, it’s weird that other languages create a new instance
of y just because the loop is starting over. (esp if your mental model for a loop is
a goto at the end.) It’s as if each iteration of the loop in C-family languages is
really a function call with its own stack.
* However, I *would* expect each lambda expression to get its own copy of y, because
each really *is* a new function call with its own stack.
* It doesn’t even seem relevant to a variable within a loop, as z above should also get
its own copy for each lambda expression.
* I still prefer "lambdas(i) = ByVal(0) + y" syntax to what you’ve settled on. How do
you handle ByVal and ByRef parameters? What about typed parameters?
I would say
"lambdas(i) = ByRef(0 as Integer) + y + z")
Or maybe a more verbose
"lambdas(i) = Param(0) + y + z"
"lambdas(i) = Param(ByRef 0 As Integer) + y + z"?
* Is it legal for me to assign to z, as I do in the second lambda? It seems like it should
be, but it would require you to essentially have 2 copies of z available in the lambda.
So, I would expect lambdas(0)(0) = 0 + 0 + 2 = 2,
and lambdas2(0)(0) = 4 + 0 + 1 = 5 with z = 5 as a result.
In other words code that doesn’t assign to z would use a copy of the value of z when
the expression was created, but assigning to z would still update the variable from
the containing scope.
* After reading Cameron’s comments, I’m even more confused about what should
happen. I was going under the assumption that a closure should occur whenever
the "Function(x) …" line is executed, giving the new function a snapshot of the
variables in scope at that time, with the added complications of dealing with updates
to those variables. So, in his 1st example, I would expect the "lambda1(0)" call to
use the y=0 value, and then assign y = 0 + 1, which overwrites the existing value
of y (which was already 1 so it’s a confusing example.). So the output would be:
But more importantly changing the line "y = 1" in the code to "y = 42" would have
no effect on the output, because y is overwritten by Foo().
Does this really defeat the purpose of lambdas and/or closures? I don’t think so, but
I’m no expert on either.
For his second example, I would expect the output to be :
The first lambda expression is a function that adds 0, and the second lambda is a
function that adds 1.
* I can see the value in some of these simple lambda expressions, but I’m afraid it’s a
slippery slope to abuse of the ‘:’ and ‘_’ operators.
lambdas(i) = Function(x) If z > 10 Then : z = x + y 2 : Else If z > 5 Then : _
z = x + y : Else If z > 0 then : z = 1 : Else z = 0 : End If
I think the value of VB has always been the simplicity and clarity of its syntax,
helped in no small part by the line-oriented nature. Of course, not all VB syntax is
simple and clear, but that’s a different topic.
Cameron: Closures are created upon entry into a block. So your example would print 2 2, because you never left the block (so we never would create another copy of y).
Justin: What we’re proposing to do is exactly what you suggest in terms of the behavior of closures and loops. The plan for more complex parameters is just to have the lambda syntax work like a nameless function:
x = Function(x As Integer, y As Integer) x + y
Note that for now lambda expressions can ONLY have expressions in them. So you can’t do assignment in the context of a lambda (at least, not directly). This is a limitation and we expect in the future to support lambdas with statements, but we won’t do this for Orcas.
These lambda expressions remind me the good old DefFn from MSX-Basic…! =))
I suppose that the "correct" behavior would be to *have a reference* to y. On the other side, making it possible to have a *copy* of the value would be usefull to. Why not allow ByRef and ByVal in such expressions? You could even state that ByVal would be the default (just as in parameter declaration). This would follow the Rule of Least Surprise ™, I guess =)))
Delegate Function AddExpr(X As Integer) As Integer
Dim Lambda(0 To 3) As AddEpr
For Y As Integer = 0 to 3
Lambda(Y) = Function(X) X + ByRef(Y) + Y
‘Or, if you wanted to be explicit:
‘Lambda(Y) = Function(X) X + ByRef(Y) + ByVal(Y)
For Each F As AddExpr In Lambda
Since, at invocation time, Y is 4 (because it went out of the loop with this value), the snippet would print 4, 5, 6 and 7.
I think "Function" keyword is a little bit long for this.
Dim f As Func(Of Integer, Integer) = Function(x, y) x + y
Could you consider a symbol or short keyword?
Dim f As Func(Of Integer, Integer) = (x, y) x + y
Dim f As Func(Of Integer, Integer) = |x, y| x + y
Dim f As Func(Of Integer, Integer) = (x, y): x + y
Can you clarify "for now lambda expressions can ONLY have expressions in them. So you can’t do assignment in the context of a lambda (at least, not directly). This is a limitation and we expect in the future to support lambdas with statements, but we won’t do this for Orcas"?
Does it mean that a lambda expression can not have multiline expressions?
Looking past Orcas, would it mean that lambda expressions with multiple statements would include ‘Return’ statements and ‘End Function’ keywords?
Pingback: Panopticon Central