Hacker News new | ask | show | jobs
by falcolas 4155 days ago
> References and lifetimes allow you to safely return pointers to stack allocated objects.

This is explicitly called out as non-idiomatic behavior in the documentation, however. The preferred action is to allocate on the caller's heap and pass a mutable reference down to the callee.

In fact, in general it's recommended not to use Box, because it complicates human reasoning about the code. And while it gets around a lot of the compiler's restrictions, its akin to writing <language of choice> in Rust, which is frowned upon in any language. Recommending its use so broadly is doing a disservice to people who want to learn Rust.

> Only at interfaces where the declaration also serves as documentation. Elsewhere, types can generally be inferred.

Except where they can't, and those locations aren't terribly consistent. The Rust designers have publicly announced their preference for explicitness over inference, and the language reflects that.

> On the other hand, in rust, you can only return one error.

This is not unique to rust, or any language really. You can only throw one exception at a time. You can only set one errno at a time. You can only return one `error` at a time.

> macros have to be explicitly imported

Except for the built in ones, which are the only ones referenced by the OP. Also, by placing the macro delimiter `!` between the name and the parenthesis, it makes the macro harder to scan for visually. I imagine that any editor will want to set up special rules to highlight these distinctly, and having special highlighting for the ones known to change the program flow would be beneficial.

> It exposes a lower-level (not simple) memory model because systems programmers need it.

Low level memory is simple: write to, read from, write to referenced, read from referenced. The OS adds one more major operation: get heap memory. Everything else is added by languages or libraries.

That said, Rust's restrictions on memory lifetimes results in more simplistic memory related code. When you have to jump through extra hoops to create a pointer which may be used beyond a single scope, and the compiler creates so much friction when you want to do anything with them in that greater scope, people will defer back to simplistic memory code.

I'm not certain if this is good or bad; it just is at this point.

> Alpha means fewer breaking changes and no "major" breaking changes not stability.

Any breaking changes affect stability, affects documentation (Rust's library documentation is behind the actual code as of a week ago), and affect 3rd party libraries. The results of this is that if you're not Mozilla, there are significant barriers to writing Rust code right now, and I would not personally recommend learning or writing Rust right now to anybody.

3 comments

> Except where they can't, and those locations aren't terribly consistent.

Why aren't they consistent? The Rust type inference is generally very good, and the places where you have to annotate are places where any typechecker would force you to annotate, because the types are simply underconstrained (e.g. the return type of Vec::collect or mem::transmute).

> The Rust designers have publicly announced their preference for explicitness over inference, and the language reflects that.

As the original author of the typechecker, I can state that the idea that we intentionally made the type inference less powerful than it could have been is totally false. It's always been as powerful as we could make it, except for interface boundaries (where type annotation is needed because of separate compilation anyway).

> Why aren't they consistent?

So, I went back to do a bit of research, and it's gotten better since this first bothered me, my apologies. My beef was with the `let x = vet::Vector::New::<i32>()` vs `let x: Vec<i32> = vec::Vec::New()`. Perhaps not the best way to word it, so consider this objection retracted. :)

> the idea that we intentionally made the type inference less powerful than it could have been is totally false

Except for function definitions, where the types could be inferred from the function bodies, but are not:

https://www.reddit.com/r/rust/comments/2bcof3/rust_type_infe...

Plus (and this is more related to the complete lack of implicit type conversions), there are types everywhere in the program. I frequently can't write a number without having to append a type, even when the type has been explicitly defined previously.

Here's one of my favorites from a recent attempt to write a ray tracer:

    let mut s: Vec3<f64> = Vec3{x: 0f64, y: 0f64, z: 0f64, w: 0f64};
> Except for function definitions, where the types could be inferred from the function bodies, but are not:

That's an interface boundary, as I mentioned. You would have to write the types in many cases anyway for separate compilation to work. In languages where you have whole-program type inference like ML and Haskell, people frequently end up writing the types for functions because of this issue.

> Plus (and this is more related to the complete lack of implicit type conversions), there are types everywhere in the program. I frequently can't write a number without having to append a type, even when the type has been explicitly defined previously.

This has nothing to do with implicit type conversions, but is rather because numeric literals have no type. It is not a type inference problem; it is just the way that numeric literals are defined.

> let mut s: Vec3<f64> = Vec3{x: 0f64, y: 0f64, z: 0f64, w: 0f64};

That much explicitness is not necessary. It could be written:

    let mut s: Vec3<f64> = Vec3 { x: 0.0, y: 0.0, z: 0.0, w: 0.0 };
Or:

    let mut s = Vec3 { x: 0f64, y: 0.0, z: 0.0, w: 0.0 };
The default types for bare literals is described in RFC #212 (https://github.com/rust-lang/rfcs/pull/212)

To summarize: bare FP literals default to f64, bare integral literals default to isize. (NOTE: isize is recently renamed from int. It is a pointer-sized integer.)

(EDIT: The default for integer literals may be superseded by a later RFC. I seem to recall that the default is actually i32 now, but I can't find a PR to back up that claim.)

So you could easily get a Vec3<f64> like so:

    let mut s = Vec3 { x: 0.0,  y: 0.0, z: 0.0, w: 0.0 };
    let mut t = Vec3 { x: 0f64, y: 0.0, z: 0.0, w: 0.0 };
The key here is that integral literals and floating point literals are distinct.

A bare literal of the form `0` is an unconstrained integral literal.

Whereas a literal of the form `0.0` or `.0` is an unconstrained float literal.

In practice it is very rare for me to annotate my numeric literals. If the variable escapes the stack frame it will be constrained by the signature of the function anyways. If not I constrain the type inline (`let x: T = ...`) and use the appropriate bare literals.

You are right that this changed, but I can't find it in the RFCs either. https://github.com/rust-lang/rust/pull/20189 implemented it. And it is what everyone agreed upon.... hmm
> The preferred action is to allocate on the caller's heap and pass a mutable reference down to the callee.

Care to link/elaborate this? I thought the recommendation was to return by value in this case -- making it easy for the caller to decide where to store the value. The move semantics would then optimize the copy away, so you end up using either the caller's stack or the heap, depending on how the call was made.

You're correct.
Actually, I got curious and tried it out. Turns out Rust didn't optimize the heap case: it used the caller's stack, and only then copied the value to the heap.

https://play.rust-lang.org/?code=%23!%5Bfeature(core)%5D%0A%...

Because it would change the semantics of the program. Where heap allocations happen is considered a side effect, and forcing a function to be inline(never) also makes it have unconstrained side effects (in some cases). Those side effects cannot be reordered.
That's because of `Box::new`, I'd think. If you use the box keyword, you should get the placement new effect.
Thank you! I thought the box keyword was simply a syntactic sugar -- I'm not that familiar with placement new, even in C++.

Note for the interested: the optimization works right now, even though the keyword is behind the box_syntax feature gate.

Updated test: https://play.rust-lang.org/?code=%23!%5Bfeature(core)%5D%0A%...

> In fact, in general it's recommended not to use Box, because it complicates human reasoning about the code.

Really? I've always understood it was because when possible that decision should be left to the caller and boxing by default just made the interface less flexible/convenient for callers. How does Box complicate reasoning about the code?

To me, because Boxed objects are a weird combination of a pointer and a stack value. Boxed values have a lifespan all their own, and you need to understand the details of a box's scope to understand how they will behave, and when they will be deallocated.