On 10-08-30 02:07 PM, David Herman wrote:
In principle, I'd prefer to have a lambda form that can implicitly bind upvars,
although I think I'd look at this from a slightly different direction than
Sebastian.
I think we may have a divergence of topic here. Not an unhealthy one --
we should discuss both at a little length, given the importance attached
to these matters -- but let's be clear on what Sebastian proposed and
what you're talking about. There are two kinds of relatively different
lambdas here:
- Down-Lambdas (I'll call them this for now) which can't outlive their
current scope, and thereby alias the environment they're in by
frame pointer, directly and at minimal cost, about the same as
the current foreach blocks do.
- Heap-Lambdas (again, hope this isn't offputting) which you're
describing. These are an expression form of our local fn items
that can be placed in a bind expression. Or perhaps bypass a bind
expression altogether.
Sebastian is proposing down-lambdas as an addition so that we can
support a few pass-custom-logic-in idioms (presumably parallel-iter,
also the obvious like map and filter). We need to get aliasing rules
right here, which are complex on params and possibly fatal on the
closure value itself, but it seems *plausible*.
In contrast, you're proposing we try to come up with rules by which
heap-lambdas can be made to safely capture their environment, merely
lowering the syntax barriers to using the current bind / local-fn combo.
I'm sympathetic to both proposals and interested in working details of
both or either out (ideally combining them tastefully, or doing them
separately if not). Though I'd caution that there *are* serious hazards
involved; I'm an old lisper too, in terms of personal history, but this
is a language aiming for a slightly different sweet spot.
But I don't think lambda is just about RAII.
Indeed not. Didn't mean to imply that.
In particular, I think a very common use case for lambda is for event-driven
programming, a style I'd expect to come up often in Rust programs. Requiring
programmers to name their functions and place them out-of-band breaks up the
flow of the programming and just makes it a little harder to read. Or from the
other direction: being able to pass an event handler directly inline as an
anonymous lambda makes it immediately clear to the reader that the relevance of
the function doesn't extend beyond this one place.
Fair points.
Graydon, Re: control flow complexity, I'm not sure whether lambda adds too much
control-flow complexity, given that we already have higher-order constructs
with |bind| and objects.
Nope, I don't think it adds any control-flow complexity. I was
mentioning control-flow complexity wrt. Sebastian's example, that used
catchable-exceptions and an implied nonlocal control transfer (which we
don't have).
> Primarily, I see it as a lightweight notational convenience for
common pattern, which you can already express using helpers.
Yup. Issue 6 is open, right? I'm not going to reject a patch that
implements a fn-expression form. I just want to be careful with what it
*means*, particularly as far as any implied capture :)
I wouldn't think we'd want to allow lambda-functions to close over
stack-allocated locals.
Probably not if the closure escapes to heap. Not unless making one
implies a copy at the point of capture. That's a possibility. Not an
appealing one to me, or not by absolute-most-common default, but a
possibility.
Long story short, I'm suggesting we could restrict lambdas to only be allowed
to refer to @-typed variables.
Sneaky but possible. Let's consider this further...
One approach would be to restrict upvars to be read-only.
Yeah. It's a bit random-feeling though. Users will certainly complain.
You can just view this as syntactic sugar for the one above. (BTW, I'm not
trying to propose exact concrete syntaxes, just looking for an existence proof
that it's possible to propagate typestate constraints into lambdas.)
Yeah. I think that proof has been achieved. Let's consider concrete
proposals.
That's just my current thinking, anyway. Graydon, does any of this sound
plausible?
Yeah, it does.
Let me toss a few design considerations into the stew here. We're
obviously getting into brainstorming mode for a few emails. Just try to
keep it focused on the existing semantic categories and runtime
vocabulary. Considering these points:
- The hard part: if we're going to capture by aliasing-fp, we have
to come up with some way of prohibiting the formation of a copy
of the down-fn. It has to have a non-copyable type. Otherwise
you can copy to the heap, and then everything explodes. This
is currently design problem #1 for down-lambdas. If we can't solve
this, the remainder of down-lambdas is doomed. In foreach loops, at
present, there's no such problem because the inner fn isn't named.
- I don't see a strong reason to add "lambda" to the language, as a
syntactic keyword, when we've got the shorter "fn" already. So for
sketching sake, let's restrict to that. If we are really aiming to
shave syntax we can even play the smalltalk game and move the params
inside the block: "{(x, y) foo(x+y); }"
- If you're going to start providing methods for environment capture,
it is worth considering whether to keep "bind" at all. It gives you
something like currying, but the argument about redundancy cuts both
ways: "bind f(10, _)" can be written "fn(int x) { f(10, x); }". It
depends a lot on the relative frequency of currying vs. capture. Now
that we're down to just two slot modes, it might make sense to call
the "bind" game off. It might not be paying for itself.
- I'm still somewhat concerned with allowing the programmer to specify
clearly which variables are being captured, when it makes sense.
I'll admit that there are enough cases in which it is an annoyance,
but if you have a large body of logic, it's a comprehension hazard
to have a closure copying something to the heap and/or retaining a
reference without a reader noticing. It's good to be able to be
explicit when you want to be. Rust's design has tried to keep in the
foreground the fact that when working on a large codebase, the
programmer wants to fasten seatbelts because they don't trust
*themselves* to get things right, and want double-checks to occur.
- You still need something like an argument-list to indicate which
arguments you want the resulting function to accept. What its type
is. So you need to indicate stuff about the captures *and* the
residual arguments. This is why it gets chatty.
- I don't want to get into inferring function types or type-param
capture. Too much effort, those subsystems are already overloaded.
So I want to keep a result-type in there too.
- C++ capture clauses have two forms, one in which they explicitly
list the variables captured and one in which they capture
"everything" by mentioned in the body. I wonder if we can follow
their lead here.
Suppose we do this:
- use the "fn" keyword.
- permit fn-expressions, only for monomorphic functions.
- Permit an optional capture clause between "fn" and its params in
expression context.
- The capture clause can be @ or & followed by an optional list of
captured vars (just their names), or omitted to indicate "inspect
the function to figure out the captures". Assuming we figure out
a way to prohibit copying down-lambdas. If not, just @.
- Remove "bind". Having shipped in Sather is not exactly a huge
sales pitch, and the haskell/ML people are the only ones who will
even think to use currying. Everyone else won't notice its absence.
This is essentially the C++-0x-lambdas approach, just adapted to our
current syntax and semantics a touch. So we'd get:
- "fn &(a,b) (int x) -> int { ... }" -- alias a, b; fn takes one int.
- "fn @(a,b) (int x) -> int { ... }" -- box a,b; fn takes one int.
- "fn & (int x) -> int { ... }" -- alias everything mentioned in fn.
- "fn @ (int x) -> int { ... }" -- box everything mentioned in fn.
- "fn (int x) -> int { ... }" -- no capture at all.
To C++'s credit here, their scheme gives programmers the option to
choose to be lazy and capture stuff implicitly, if they feel safe doing
so, or the ability to be more-precise and capture only what they mention.
Anyway, as I say above, I think there's at least one hard semantic
problem here (how to prohibit escape of a down-lambda) along with a few
syntax considerations. Thoughts welcome on how to overcome the former.
Proceeding with the heap-capture variant is possible, if everyone's able
to accept losing currying and by-name "bind" expressions.
-Graydon
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev