Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Place left arrow syntax (place <- expr) #1228

Merged
merged 4 commits into from
Oct 9, 2015

Conversation

pnkfelix
Copy link
Member

Summary

Rather than trying to find a clever syntax for placement-new that leverages
the in keyword, instead use the syntax PLACE_EXPR <- VALUE_EXPR.

This takes advantage of the fact that <- was reserved as a token via
historical accident (that for once worked out in our favor).

rendered draft.

@pnkfelix pnkfelix changed the title Place left arrow syntax Place left arrow syntax (place <- expr) Jul 28, 2015
@nagisa
Copy link
Member

nagisa commented Jul 28, 2015

“Lets add @ and ~ back too!” was my first reaction. I don’t like the in syntax and I don’t like <- either, albeit a little less so. box x in PLACE sounded OK to me, but it didn’t stick, sadly.

Expanding: let x = HEAP <- 10 doesn’t look good at all. Perhaps even worse than let x = in HEAP 10 ± the braces.

@nrc nrc added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jul 28, 2015
@aturon
Copy link
Member

aturon commented Jul 28, 2015

I didn't follow the original syntax debate closely, but I agree with @nagisa that let x = HEAP <- 10; doesn't read terribly well. In particular, the RFC correctly points out that <- is strongly associated with assignment, but there's a follow-up point there: the use of <- in an assignment context treats the thing to the left of <- as being the thing assigned to.

@pnkfelix
Copy link
Member Author

(BTW I don't know if this is clear from this RFC, but the intention is that we will keep the box EXPR form, and fix our inference and/or coercions so that box EXPR will infer from the context whether it needs to place the result into e.g. a Box or an Rc, etc... see eddyb's hard efforts here e.g. in rust-lang/rust/pull/27292)

In other words, my intention is that people would not write let x = HEAP <- 10;. They would write let x = box 10; (and type inference would figure out that a Box<_> is needed).

The place where PLACE <- VALUE is needed is when the PLACE is a more interesting expression than HEAP. E.g. let handle = arena <- expr;

@pnkfelix
Copy link
Member Author

(but also I don't actually see what the problem is with let x = HEAP <- 10;. You're putting a 10 into the boxed::HEAP, and returning that boxed value... seems straight-forward to me...)

@pnkfelix
Copy link
Member Author

Another alternative that perhaps I should have listed in the alternatives section

let handle = in arena <- expr;

(man and I just thought this was going to be a slam dunk so there would be no need. ;)

@seanmonstar
Copy link
Contributor

<- and in both read weird to me. I'd prefer if we could just not use new syntax, such that Box::new(10) and Rc::new(5) are what we're write, and those functions would be optimized some way to allow allocating in different places (with an attribute?).

@eddyb
Copy link
Member

eddyb commented Jul 28, 2015

@seanmonstar But as @pnkfelix is trying to clarify, you would use box 10 and box 5 for those cases, not in or <-.
Maybe type ascription should be merged at the same time, to allow shorthands like (box Foo(a, b, c)): Rc<Trait> (are the parens needed? I can't remember the precedence rules).

@petrochenkov
Copy link
Contributor

Yay!

@nagisa @aturon
It's not Box, but other containers, who will benefit most from this change.

For example, emplacement into Vec:

let v = Vec::new();
// The old syntax
in v.back() { 10 };
// The new syntax. 
v.back() <- 10; // This clearly looks better to me.

or emplacement into HashSet:

let s = HashSet::new();
// The old syntax
in s.new_place() { 10 }; 
in s {10}; // If the set itself can be a "place"
// The new syntax
s.new_place() <- 10;
s <- 10; // Yep, much better

Ideally, a couple of basic containers (Vec, HashMap, HashSet) should get actually working emplacement interfaces before finalizing the emplacement syntax. Does the current state of implementation (i.e. rust-lang/rust#27215) allow implementing such interfaces? (I haven't digged into it yet.)

Edit: HashSet is not the best example because it still needs to construct the RHS before insertion for hash calculation and comparison.

@nagisa
Copy link
Member

nagisa commented Jul 28, 2015

I’ve been convinced. I can’t think up anything better; <- sounds like the best way to go here. And general uglyness of the Box/Rc<_> case actually makes you want to use box more – for the better.

👍

@aturon
Copy link
Member

aturon commented Jul 28, 2015

Thanks @petrochenkov; usage in those examples hews much closer to my intuitions about <- as assignment syntax. I withdraw my objection.

@seanmonstar
Copy link
Contributor

@eddyb yea, and I don't necessarily find that better.

let x: Rc<i32> = box 5;
// or
let x = Rc::new(5);

@Gankra
Copy link
Contributor

Gankra commented Jul 28, 2015

@seanmonstar In my experience the box would almost always be inferred -- but that also has issues, perhaps (akin to just invoking collect or default).

@liigo
Copy link
Contributor

liigo commented Jul 28, 2015

box EXPR into PLACE is always the best!

(I don't think PLACE evaluating before EXPR is a big violator to any explicit rules. Or I don't mind that.)

@nagisa
Copy link
Member

nagisa commented Jul 28, 2015

@liigo it was mentioned in previous discussion EXPR might often get quite big, e.g.

let x = box {
    a;
    b;
    c
} into HEAP;

which is quite a lot of scanning required to even notice this is not a regular box, but box-in.

That is, it is preferable to have EXPR after PLACE, IMO.

@oli-obk
Copy link
Contributor

oli-obk commented Jul 29, 2015

while let x = arena <- value; looks odd (not more than let x = in arena { value };), for existing containers as shown by @petrochenkov this looks very intuitive (Note: I wasn't around back when Rust had the <- operator).

@eddyb
Copy link
Member

eddyb commented Jul 29, 2015

I had another look at @petrochenkov's examples, and I believe the following could be made to work:

let map = HashMap::new();
map.entry(k) <- v

Maybe map[k] = v will be sugar for it, maybe not.

@nagisa
Copy link
Member

nagisa commented Jul 29, 2015

@eddyb if we’re speaking concrete examples, Vec<_> is even more concise; the only reasonable place to emplace elements is its back, therefore Vec could implement the necessary traits itself:

let v = Vec::new();
v <- element;

@pnkfelix
Copy link
Member Author

@petrochenkov asked:

Ideally, a couple of basic containers (Vec, HashMap, HashSet) should get actually working emplacement interfaces before finalizing the emplacement syntax. Does the current state of implementation (i.e. rust-lang/rust#27215) allow implementing such interfaces? (I haven't digged into it yet.)

Yes, now that rust-lang/rust#27215 has landed, the standard library should be able to experiment with adding implementations of the placement protocol to allow use of the in PLACE { EXPR } syntax.

@apasel422
Copy link
Contributor

@petrochenkov For what it's worth, I've gotten placement working with my tree map library here: https://github.com/apasel422/bst/tree/place.


```rust
let ref_1 = arena <- value_expression;
let ref_2 = arena <- value_expression;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in {}/<- should do autoref? Otherwise these nicely looking statements would consume the arena (twice!)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You generally don't keep an owned arena around, i.e. arena would most likely have the type &Arena.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You generally don't keep an owned arena around

The same can't be said about vectors. I.e. the v <- elem syntax proposed by @nagisa would have to roll back to v.back() <- elem (or &mut v <- elem). It's not especially terrible, though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative I’ve been having in mind here is to change Placer::make_place to take &mut self instead of self.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nagisa That wouldn’t really work for all cases, because, e.g., [Typed]Arena::alloc takes &self, not &mut self, meaning that you’d be forced to use mutable references when you shouldn’t have to.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might end up being moot for Arenas. Namely, We might be able to do

impl<'a> Placer for &'a Arena { ... }

Which in that particular case side steps the choice of self vs &mut self.

I am personally becoming more convinced that it should be &mut self. I spent a while last night looking through my notes to see why I had picked self but did not find a motivating example.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would &mut self work with map.entry(5) <- "foo"?

@killercup
Copy link
Member

I'm not sure how I feel about this syntax. At first it looks like another part of Haskell finally made it's way into rust, but when I see @eddyb's

let map = HashMap::new();
map.entry(k) <- v

I can't help but be reminded of Go's channel syntax (docs).

@glaebhoerl
Copy link
Contributor

Just to be clear, is it accurate to say that the only reason we want these special placement APIs and operators, instead of moving in the direction of the otherwise simpler and more general &out/&uninit type (e.g. fn back<'a, T>(self: &'a mut Vec<T>) -> &'a out T { ... }; *vec.back() = foo();), is to be able to recover from panics in the to-be-placed expression (foo()) without aborting?

@pnkfelix
Copy link
Member Author

@glaebhoerl the phrase "recover from panics" is a bit ambiguous.

We do want to be able to continue to properly unwind the stack, but I don't call that "recovery."

(My personal take on it is that the Placer protocol is a simpler change than trying to add &uninit T, which seems like it would require more linguistic infrastructure, somewhat analogous to type-state. Perhaps I am wrong here and there is no more infrastructure than is required to represent e.g. the temporary freezing of &mut when they are reborrowed... but that doen't match my gut feeling about it)

@aturon
Copy link
Member

aturon commented Oct 9, 2015

@glaebhoerl I agree with essentially your entire comment, except that I am slightly less sure about &out. In particular, it feels to me that placement should effectively be a version of &out that, instead of being tied to lifetimes, uses ownership and dynamic finalization, and thereby nicely accounts for unwinding etc. I haven't played enough with the placement protocol to say how close we are to that goal.

I wonder, for example, whether we can address the current overhead of buffer zeroing in Read::read implementations by introducing a method that takes a place instead.

@glaebhoerl
Copy link
Contributor

In particular, it feels to me that placement should effectively be a version of &out that, instead of being tied to lifetimes, uses ownership and dynamic finalization, and thereby nicely accounts for unwinding etc.

Hmm... could you perhaps elaborate further on this idea? (Are there analogous owned/dynamic counterparts to the other lifetime-constrained reference types? Can you have first-class places while accomodating both unwinding and mem::forget?)

One of the reasons for my uncertainty is indeed that while &out feels more simple, general, orthogonal, etc., it is still the case that Rust has unwinding (even if I may wish it didn't) and that &out doesn't mesh well with it, and so, at a minimum, we may want something else alongside &out to better reconcile the two. But I can't tell what that something would be, or if it would look like the placement protocol (in part because I haven't yet managed to push myself through to really comprehend it), only that it makes me somewhat uneasy that we're trying to figure the latter out before the former.

I wonder, for example, whether we can address the current overhead of buffer zeroing in Read::read implementations by introducing a method that takes a place instead.

I had assumed this would be a natural use case for &out, but didn't dive into the details. Is there a reason why one or the other would be better suited here?

@arielb1
Copy link
Contributor

arielb1 commented Oct 10, 2015

@glaebhoerl

The placement protocol is indeed a version of &out that does not kill the process on unwinding. It also "supports" mem::forget in the same way Drain does.

@nagisa
Copy link
Member

nagisa commented Oct 12, 2015

I noticed this RFC does not specify precedence nor associativity of the operator.

As far as associativity goes, it should be right-associative (i..e a <- b <- c parses as (a <- (b <- c))).

Precedence is less clear. It should bind stronger than assignment (=) for sure, but I can’t say whether it should bind more strongly than comparison/binary/math (i.e. let x = a & b <- c * d might make sense as both let x = ((a & b) <- (c * d)) and as let x = (a & ((b <- c) * d))).

@petrochenkov
Copy link
Contributor

@nagisa
Since <- is very similar to =, I'd expect it to have the same associativity and the same precedence + 1.

@pnkfelix
Copy link
Member Author

Okay this thread has gone off the rails. we need a FAQ wrt the placement-in system because people are chiming in without knowledge of what came before.

Just to advertise this at the (hopefully appropriate) audience; here is the current draft, which I plan to update as feedback comes in:

@vadimcn
Copy link
Contributor

vadimcn commented Mar 1, 2016

Hmm, am I totally off-base with this comment: https://internals.rust-lang.org/t/placement-nwbi-faq-new-box-in-left-arrow/2789/19?
Because if not, there are no downsides to using closures for delaying creation of the placed value, which would simplify the placer protocol significantly. Nor would we need the <- operator...

@eddyb
Copy link
Member

eddyb commented Mar 1, 2016

@vadimcn Hmm, this should be the first item of a FAQ: @pnkfelix tried closures. Literally the first thing he tried.
Doesn't work because of control flow,
the try! example is just an example of misbehaving code.
You don't want to use placement syntax only when it helps, you want to use it everywhere, for consistency, and closures would make that impossible in certain cases.

@vadimcn
Copy link
Contributor

vadimcn commented Mar 1, 2016

@eddyb: yes, but what I am saying is that in order to use try!, the inner code must return a Result<>, which, as far as I can see, will force creation of a temporary on the stack. And if you can't use try!, is the whole thing worth it?

@fenduru
Copy link

fenduru commented Mar 1, 2016

@vadimcn try! diverges, and returns in the error case. So if you use it inside a closure it will return from that closure instead of the outer function

@eddyb
Copy link
Member

eddyb commented Mar 1, 2016

@vadimcn Maybe a clearer example would be:

let mut v = vec![];
for x in a {
    v.push(box {
        if x == 0 { break; }
        x + 1
    });
}

Were box to be implemented using closures, break would simply not work.

The try! example is more subtle, as return works inside a closure, but you end up with a type conflict between the early return of Result<_, E> of try! and the value try! produces.
In the corner case of nested Result (e.g. || try!(Ok(Ok(0)))), compilation may succeed, but without an early return from the enclosing function in case of error.

@vadimcn
Copy link
Contributor

vadimcn commented Mar 1, 2016

Folks, I totally get what you are saying about the control flow.

But look at it from the other side: the raison d'etre of the placement operator is to eliminate temporaries in the process of moving the value into its place. And it does not work for this purpose in cases when the value creator returns anything but the value itself (such as Result<LargeValue,Error>).
AFAIK, enabling fallible value creation was the main justification for not simply using closures. Once this is out, the feature no longer carries it's weight, in my opinion.

As for the second @eddyb`s motivating example, I find it unconvincing: you can easily re-write it as:

let mut v = vec![];
for x in a {
    if x == 0 { break; }
    v.emplace(|| x+1);
}

There may still be useful control flows that work with <- and don't work with closures, but they need to be identified and documented before we go forward with this RFC.

@nikomatsakis
Copy link
Contributor

@vadimcn I would certainly love to just rely on closures. It's also plausible that this is indeed the best path. And maybe if we played enough tricks we might even be able to make foo.push() "just work" if you supply a "closure returning T" (though there is the obvious ambiguity problems), which has always been a kind of holy grail for me. :)

All that said, I think there are two points being lost here:

  1. Using closures as the desugaring for the <- operator is a non-starter, I think, because of control-flow problems. If just have no operator, and hence no desugaring, this goes away.
  2. Just because fallible things occur as part of the creation expression doesn't mean that those fallible things are the main result. For example:
vec <- Entry {
    field1: foo?,
    field2: bar?,
    field3: [0; 1024]
};

Something like that might still make sense, it seems to me.

You could also imagine things like:

vec <- if some_condition {
    return;
} else if some_other_condition {
    break;
} else {
    generate_big_result(some_fallible_thing()?)
};

(which I guess is just sort of a variation on a theme).

@vadimcn
Copy link
Contributor

vadimcn commented Mar 2, 2016

Using closures as the desugaring for the <- operator is a non-starter, I think, because of control-flow problems. If just have no operator, and hence no desugaring, this goes away.

I agree, this is what we should do.

vec <- Entry {
    field1: foo?,
    field2: bar?,
    field3: [0; 1024]
};

Since computation of foo1? and foo2? involves creation of temporaries anyways, it can be moved outside. Slightly more verbose to write, but doesn't really add any runtime overhead.

let field1 = foo?;
let field2 = bar?;
vec.emplace(|| Entry { 
    field1: field1,
    field2: field2,
    field3: [0; 1024]
});

Same applies to the last example.

@withoutboats
Copy link
Contributor

I think the major cost of syntactic support for emplacement, really the major cost for emplacement in general, is pedagogical. Everyone has to learn what emplacement is and when they need to use it. As a rule, additional syntax tends to increase the learning burden, but I think this is an exception. it seems easier to understand the notion of emplacement by dedicating syntax to it than it would be to understand why in some cases you should use a higher order function instead of the more at-hand function.

Put another way, I don't think it is easier to explain to new users why they should not use vec.push(Entry) and instead use vec.emplace(|| Entry) than it is to explain why they should use vec <- Entry. I think the syntax makes it easier to comprehend.

I think it also makes it easier to comprehend when scanning code, because higher order functions can have many different purposes.

@Centril
Copy link
Contributor

Centril commented Apr 12, 2016

If Rust decides do add do-notation for Monads (HKTs required), will this RFC have made the do-notation syntax that haskell uses impossible for Rust? Or is it still perhaps possible to give a different context based meaning to <- when started with a do "block"?

@nagisa
Copy link
Member

nagisa commented Apr 12, 2016

@Centril Rust is not a Haskell and it doesn’t face the same constraints as Haskell does/did. We might be considering HKTs for Rust, but Monads and sugar around them has little purpose even with HKTs in play.

@ticki
Copy link
Contributor

ticki commented Apr 12, 2016

@nagisa, no that's simply not true. While they have less importance in Rust (due to e.g. sideffects), they're still very useful for e.g. error handling.

@Centril
Copy link
Contributor

Centril commented Apr 12, 2016

@nagisa Having a Monad HKT trait allows you to define common operations for a whole host of data structures once. There are already a bunch of Monads in Rust, they are just not grouped together under a trait... Vec, Option, Result, BinaryHeap.

Monads are not just a way to get "side-effects", but to redefine the meaning of ; (semicolon)

Other interesting monads: Parsers, Futures, Language-based-security (http://www.cse.chalmers.se/edu/course/TDA342_Advanced_Functional_Programming/lecture9.html).

Add to this: Monad transformers, where you have a stack of monads... http://www.cse.chalmers.se/edu/course/TDA342_Advanced_Functional_Programming/lecture6.html

Also: monad comprehensions are even more powerful than list-comprehensions.

@eddyb
Copy link
Member

eddyb commented Apr 12, 2016

@Centril In a systems language, fancier functional tricks meet the real world and tend to fall apart.

As an example, Monad::bind needs to be generic over T, U and F: Fn(T) -> Self<U>.
Concerns over the choice of Fn, FnMut or FnOnce aside, this means that if you don't call F before returning, Self<*> cannot hold onto it without boxing it and turning calls to it into virtual calls.

If devirtualization doesn't occur, the slowdown compared to our current iterators or future hypothetical state-machine-based generators can be more than an order of magnitude.

Parsers and futures also tend to use implicit existentials and/or GADTs, which admittedly Rust needs, but still have the overhead of boxing and virtual dispatch.

We get it, monads are cool. But unless you can find a formalism for "do notation" which allows pervasive static dispatch and no boxing by default instead of as an optimization (and maybe actually integrate it with imperative control-flow primitives), I'm afraid there will always be more specialized solutions, not unlike the theoretical purity of linked lists contrasted with the real-world cache-efficiency of arrays.

@Centril
Copy link
Contributor

Centril commented Apr 12, 2016

@eddyb Good comment. Didn't realize that F becomes a virtual call. But just to be clear, you're not saying that Monad::bind itself becomes virtual?

Rust is a systems language, but not just. It can be used for other things =)
Anyways, if <- is reserved for placement new you can always reserve <= for do notation.
I guess we can continue discussion on HKT-related issues/RFCs.

@eddyb
Copy link
Member

eddyb commented Apr 12, 2016

@Centril Indeed, you can have Option or Result's and_then methods without any losses if you keep taking FnOnce closures (so by-value captures can be used).
Any usecase where you call the closure immediately remains static dispatch, problems appear when you try to do something like Iterator::map without having all of those explicit generics.

@killercup killercup mentioned this pull request Aug 31, 2017
10 tasks
bors added a commit to rust-lang/rust that referenced this pull request Apr 4, 2018
…sakis

Remove all unstable placement features

Closes #22181, #27779. Effectively makes the assortment of placement RFCs (rust-lang/rfcs#470, rust-lang/rfcs#809, rust-lang/rfcs#1228) 'unaccepted'. It leaves `box_syntax` and keeps the `<-` token as recognised by libsyntax.

------------------------

I don't know the correct process for unaccepting an unstable feature that was accepted as an RFC so...here's a PR.

Let me preface this by saying I'm not particularly happy about doing this (I know it'll be unpopular), but I think it's the most honest expression of how things stand today. I've been motivated by a [post on reddit](https://www.reddit.com/r/rust/comments/7wrqk2/when_will_box_and_placementin_syntax_be_stable/) which asks when these features will be stable - the features have received little RFC-style design work since the end of 2015 (~2 years ago) and leaving them in limbo confuses people who want to know where they're up to. Without additional design work that needs to happen (see the collection of unresolved questions later in this post) they can't really get stabilised, and I think that design work would be most suited to an RFC rather than (currently mostly unused) experimental features in Rust nightly.

I have my own motivations - it's very simple to 'defeat' placement in debug mode today and I don't want a placement in Rust that a) has no guarantees to work and b) has no plan for in-place serde deserialisation.

There's a quote in [1]: "Ordinarily these uncertainties might lead to the RFC being postponed. [The RFC seems like a promising direction hence we will accept since it] will thus give us immediate experience with the design and help in determining the best final solution.". I propose that there have been enough additional uncertainties raised since then that the original direction is less promising and we should be think about the problem anew.

(a historical note: the first mention of placement (under that name - uninit pointers were earlier) in an RFC AFAIK is [0] in late 2014 (pre-1.0). RFCs since then have built on this base - [1] is a comment in Feb 2015 accepting a more conservative design of the Place* traits - this is back when serde still required aster and seemed to break every other nightly! A lot has changed since then, perhaps placement should too)

------------------------

Concrete unresolved questions include:

 - making placement work in debug mode [7]
 - making placement work for serde/with fallible creation [5], [irlo2], [8]
 - trait design:
   - opting into not consuming the placer in `Placer::make_place` - [2]
   - trait proliferation - [4] (+ others in that thread)
   - fallible allocation - [3], [4] (+ others in that thread)
 - support for DSTs/unsized structs (if at all) - [1], [6]

More speculative unresolved questions include:

 - better trait design with in the context of future language features [irlo1] (Q11), [irlo3]
 - interaction between custom allocators and placement [irlo3]

[0] rust-lang/rfcs#470
[1] rust-lang/rfcs#809 (comment)
[2] rust-lang/rfcs#1286
[3] rust-lang/rfcs#1315
[4] #27779 (comment)
[5] #27779 (comment)
[6] #27779 (comment)
[7] #27779 (comment)
[8] rust-lang/rfcs#1228 (comment)
[irlo1] https://internals.rust-lang.org/t/placement-nwbi-faq-new-box-in-left-arrow/2789
[irlo2] https://internals.rust-lang.org/t/placement-nwbi-faq-new-box-in-left-arrow/2789/19
[irlo3] https://internals.rust-lang.org/t/lang-team-minutes-feature-status-report-placement-in-and-box/4646
@Centril Centril added A-placement-new Proposals relating to placement new / box expressions. A-syntax Syntax related proposals & ideas A-expressions Term language related proposals & ideas labels Nov 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-expressions Term language related proposals & ideas A-placement-new Proposals relating to placement new / box expressions. A-syntax Syntax related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.