A follow-up to my previous email: there's one possible unsoundness depending on
how our typestate works (betraying my ignorance, sorry). If it's possible to
invalidate a typestate, then dealing with closures would have to be tightened
so that whatever constraints a lambda requires of an upvar have to be upheld
forever once the closure is created. Otherwise you could have, e.g.:
let @mutable int x = 17;
check prime(x);
auto f = lambda() : prime(x) { frobPrimeNumber(x); };
x = 12;
f(); // crash
On a separate note, I agree that "down-lambdas" and "heap-lambdas" are two
potentially different kinds of constructs with pretty different uses. There are
lots of useful things about both. Down-lambdas are good for registering
clean-up code for a scope, as we mentioned before, but they're also prevalent
in a lot of functional patterns: map/forEach/fold, for example, all take
down-lambda arguments. And a local recursive algorithm that is immediately
applied is a very common functional pattern.
(But I do think arbitrary-lifetime lambdas are good for usability, since they
come up so much in event-based programming. If they can be made to work, of
course.)
Dave
On Aug 30, 2010, at 2: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 RAII has a lot of good things going for it, and
> try/finally is weak tea. But I don't think lambda is just about RAII. 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.
>
> 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. Primarily, I see it as a lightweight
> notational convenience for a common pattern, which you can already express
> using helpers.
>
> But all that said, nobody's thought through Rust's control flow as deeply as
> Graydon, and I wouldn't swear to it that there isn't some deal-breaker I
> haven't thought of. Here are a few thoughts about what seem like some of the
> tricky issues:
>
> - Exteriors:
>
> I wouldn't think we'd want to allow lambda-functions to close over
> stack-allocated locals. Stack locals are not meant to outlive their frame,
> and lambda-functions are. IIRC, Apple's GCD wantonly lets you do so, with C's
> usual "it's undefined" as the semantics of referring to a dead upvar. That's
> obviously not an option. Java achieves safety by forcing you to either
> const-declare variables that have upvar references, so they can be copied, or
> else effectively box/heap-allocate them by placing them in objects or
> one-element arrays.
>
> We can do better than Java, though, since we have a really lightweight and
> natural way of saying a local variable could outlive the stack frame -- "@".
> :)
>
> Long story short, I'm suggesting we could restrict lambdas to only be allowed
> to refer to @-typed variables.
>
> - Propagating typestate constraints to lambdas
>
> When you use an explicitly declared helper function, you can propagate
> typestate constraints to make a program type check:
>
> fn helper(@mutable int x) : prime(x) { ... }
> ...
> let @mutable int x = 17;
> check prime(x);
> ... helper(x) ...
> frobPrimeNumber(x);
>
> If we turn that helper into a lambda with an upvar, we don't have a type
> constraint to propagate the predicate:
>
> let @mutable int x = 17;
> check prime(x);
> ... lambda() { ... x = 12; ... } ...
> frobPrimeNumber(x); // the lambda may have ruined prime-ness!
>
> One approach would be to restrict upvars to be read-only. I *think* this
> would be sound, since the helper wouldn't be able to violate the typestate of
> a variable. And you could still get the mutable version using |bind|:
>
> let @mutable int x = 17;
> check prime(x);
> ... bind (lambda (@mutable int x) : prime(x) {
> ...
> x = 12;
> check prime(x);
> ...
> })(x) ...
> frobPrimeNumber(x);
>
> In order to make that type-check, the programmer is forced to put the
> explicit constraint on the parameter x.
>
> But you could also take this a step further and just allow programmers to
> specify constraints on a lambda's upvars, alleviating the need for the manual
> closure conversion:
>
> let @mutable int x = 17;
> check prime(x);
> ... (lambda () : prime(x) {
> ...
> x = 12;
> check prime(x);
> ...
> }) ...
> frobPrimeNumber(x);
>
> 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.)
>
> That's just my current thinking, anyway. Graydon, does any of this sound
> plausible?
>
> Dave
>
> On Aug 30, 2010, at 11:06 AM, Graydon Hoare wrote:
>
>> On 10-08-28 02:33 PM, Sebastian Sylvan wrote:
>>
>>> Let me first say that the only reason I write this is because I really like
>>> Rust. It seems to get a lot of things really right, and I'd like it to
>>> become successful! With that said, here's one issue. I have more, and I may
>>> write more about that later, but I figured I start with this one because it
>>> actually ends with a suggestion!
>>
>> Hi,
>>
>> First, I'd like to remind you of the conduct guidelines for this community.
>> While not quite venturing into the "abusive" or "flaming" category, for an
>> introductory post/proposal your tone is needlessly provocative and filled
>> with loaded terminology ("hijacking", "kludge", "severely crippled"). We aim
>> for decorum here, please try to respect that. I had to rewrite my response a
>> few times to ensure that I was not escalating the tone, and I don't enjoy
>> spending work hours on such exercises.
>>
>> With respect to the technical feasibility of your proposal: it may be
>> possible -- it's similar to how we currently translate foreach blocks, for
>> example -- and I'll add a note about this proposal to the existing bug
>> (issue #6) that's a work-item for a "lambda" short-form of anonymous
>> function immediate. If anyone wants to take the time to work out how your
>> proposal interacts with Rust semantics in more detail and ensure it'd be
>> safe, I'd be happy to entertain that conversation. Such alias-based, scoped
>> capture would have the pleasant additional benefit of being faster than
>> function bindings (no heap allocation), so I'm sure nobody would object to
>> trying to fit them in.
>>
>> Finally, I should clarify some misconceptions in your post. When I speak in
>> the collective voice "we" here, I am referring to myself mainly as well as
>> some opinions I believe to be shared with others who have worked on the Rust
>> design. Please, others, speak up if you disagree:
>>
>> - Comprehensibility is an important design goal in Rust, so we're not
>> terribly interested in appeals to "many" kinds of additional control
>> flow abstraction. Control flow is a notorious comprehension hazard
>> in code at the best of times. We may be interested in handling some
>> well-motivated cases we currently do poorly (at the moment, for
>> example, parallel iteration can't be done easily) as well as general
>> abstractions that support >1 of those. Hypothetical use-cases, much
>> less so.
>>
>> - Rust has very few nonlocal control mechanisms already. Catchable
>> exceptions as outlined in your example are not something we consider
>> particularly plausible in Rust, due to custom typestate predicates
>> (there's a FAQ entry). Currently there is no such feature. Aside
>> from termination-semantics failure and loop-break / loop-continue,
>> there are no other nonlocal jumps, and (at the moment) not much
>> interest expressed in adding any.
>>
>> - Destructors and RAII are not a "kludge", and try/finally blocks are
>> not a particularly good replacement for them. Destructors and RAII
>> encapsulate the initialization state of a resource and its
>> sub-resources, such that only those resources acquired wind up being
>> released. To mimic this in try/finally blocks is verbose and
>> error-prone: you need a boolean "tracking variable" for each
>> acquired resource that you set immediately after the resource is
>> acquired, as well as logic at the finally-side to conditionally
>> release only those resources acquired (and such logic is
>> order-sensitive, and needs its own try/finally blocks in order to be
>> as robust as a good destructor system). This is particularly
>> error-prone since you only notice coding errors in such paths in the
>> (rare) cases of exceptions actually being thrown; more likely, only
>> non-throwing case is exercised and the errors are shipped
>> un-noticed.
>>
>> In addition, destructors and RAII generalize to handle resources
>> that outlive a control frame, without any change to the underlying
>> resource-manager object. Our feeling is therefore that destructors
>> and RAII are, on balance, a more-desirable feature than try/finally
>> blocks would be (if we had them).
>>
>> If you have further concrete suggestions about changes to Rust, we'd be
>> happy to hear some of them as well, but please try to keep the tone
>> respectful and constructive. The (relatively interesting, reasonable)
>> proposal in your post was almost lost amid the paragraphs of additional
>> critique.
>>
>> Thanks,
>>
>> -Graydon
>> _______________________________________________
>> Rust-dev mailing list
>> [email protected]
>> https://mail.mozilla.org/listinfo/rust-dev
>
> _______________________________________________
> Rust-dev mailing list
> [email protected]
> https://mail.mozilla.org/listinfo/rust-dev
_______________________________________________
Rust-dev mailing list
[email protected]
https://mail.mozilla.org/listinfo/rust-dev