Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Some notes on Rust (lambda-the-ultimate.org)
204 points by wkornewald on Feb 1, 2015 | hide | past | favorite | 106 comments


> Nobody seems be saying much about Rust, or if they are, the LtU search can't find it. So I'm starting a Rust topic.

The reason I personally am silent about it is because there is an ongoing overhaul of standard io libraries. Honestly, my need to be adventurous dried up when I was left with few broken libs. Writing about Rust still has a risk of becoming obsolete and misleading quickly. Waiting for the real 1.0.


> There's a macro called "try!(e)", which, if e returns a None value, returns from the enclosing function via a return you can't see in the source code. Such hidden returns are troubling.

Strikes me as simply a very appropriate use of macros. Get tired of writing the same syntactic fragment again and again? Write a macro. Want to see what some macro is "hiding"? Look it up or expand it.


The issue is it makes it difficult to notice that a function might return when scanning through a function. Especially as it's in places that are looking for a value (e.g. assignment).

At the moment it's just return and try!, but people look to the standard library for what is acceptable. When the standard library contains a macro that can return, people will write their own macros that return. It could potentially be half a dozen different macros you need to keep in your head.

Personally, I go back and forth on it. Hopefully it will turn out fine.


> The issue is it makes it difficult to notice that a function might return when scanning through a function.

What other solutions are there? The only other approach to error handling I've seen is exceptions (e.g., C++, Java, C#, JS…), and if you don't like `try!` because it is a "hidden return", you certainly won't like exceptions. At least with Rust's macros, I know that in the absence of one, there is no return; in the presence, there might be. Exceptions in most languages make no guarantee.


It's a trade off. For the trouble of exceptions I get nice benefits like stack traces (though there are proposals to add stack traces to Rusts error handling). There are times when I really like exceptions, and times (such as trying to trace through an execution path) that I'm irritated by them.

What other solutions are there? Well, you could make the return explicit:

    let z = try!(x / y, onerror = return)
There are obvious downsides, such as added verbosity, and the need to figure out keyword arguments in macros, and do you allow access to the error value etc. but at least I can grep for/highlight "return". It also makes the meaning of the slightly confusingly named "try" more obvious (again, I'm vaguely aware of proposals that would change the name).

I think ultimately try! is a good thing, but I don't think it's trivially "a very appropriate use of macros". It's a considered use given some difficult trade-offs.


Kotlin has a notion of function inlining with the return statement being inlined too. This means you can do, e.g.:

list.forEach { if (it == something) return; }

and you aren't returning from the code block, but the enclosing function. Yet behind the scenes forEach is being expanded by the compiler into a regular imperative for loop, in a macro-like way.


I think people get used to it, especially if it's a part of the standard libs. C++ <iostream> overloads the bitshift operator and nobody seems to mind.


Why aren't exceptions used here?

Is this because the behavior is sort of "exceptional" but not so exceptional that the (supposedly inefficient) exception-mechanism is warranted?

In that case, I think the compiler should handle this case still. Using profiling, it could determine which exceptions are really exceptions and which ones are not.


Rust doesn't have exceptions.


Interesting. This is certainly new for me :)


Rust's error handling looks like the Maybe monad. That seems pretty reasonable in Haskell. I'm a little surprised by the criticism in the article — is the author saying there isn't enough syntactic sugar?


It's similar. We don't have HKT, so we can't get fully generic monads, but you can implement specific instances, like we have with Option/Result.


Steve, what's the reason that Rust doesn't have HKTs (higher-kinded types). Is there a technical barrier, e.g. to do with life-time inference, or is it a philosophical choice not to have them?


It's one of our most requested features (and would be really good for collections) but it should be backwards compatible and therefore was postponed until after 1.0.

Nobody has put in the work to actually make a formal RFC yet either, which is required.


Is anybody actively working on this? I suspect that doing this well is non-trivial. Neither Tofte/Talpin nor Cyclone, both of which heavily inspired Rust's lifetimes, have HKTs as far as I'm aware.


Everything is focused on shipping a good 1.0, so no.


I understand that 1.0 was / is a priority. But I wonder what the state of discussions about HKTs in Rust is: is this addition believed to be an easy problem in the sense that it may be a lot of work, but no major roadblocks are expected? Or are there open questions that require substantial research?


The state is, "this should be backwards compatible, therefore, we don't need to think about it more until after 1.0." That's pretty much it. There isn't an active discussion, because we're actively discussing the things needed to ship 1.0.


My experience mirrors the author's: it results in a lot of use of macros and case statements. These create a fair bit of cognitive overhead to discern what the program flow will end up being, and special syntax for unpacking values.

The broad use of case statements leads to one more odd problem - knowing when, and when not, to use a `;`. Explicit returns are frowned upon, they prefer the "results from the last expression" form of returns. The `;` results in an expression returning a different value and a different type. The type system will usually catch these errors and print a helpful "perhaps you should remove the ';' from this line" message, but it's an extra bit of cognitive overhead induced by case statements.

Ultimately, I think it's less about missing syntactic sugar, and more about the type system acting like an electric fence instead of a hedge in its efforts to guide the user to their destination.


> Explicit returns are frowned upon

That's not true in any of the code I write. I prefer explicit returns in all of my Rust code. The only time I use the "result from last expression" return is when it's the last statement in the function.

The recommended way to deal with errors in Rust is "try!". Using "try!" essentially gives you the ergonomics of exceptions. You should prefer that to match or .and_then(), which are verbose.


Explicit returns are not frowned upon. I don't know where you got that idea. The only thing I can think is that this:

    fn foo() -> bool {
        true
    }
is preferred over

    fn foo() -> bool {
        return true
    }
But that's more of a style issue.

As for `;`, it's just like Standard ML. `;` is for sequencing expressions. I love it.


> Explicit returns are not frowned upon. I don't know where you got that idea

From the docs:

http://doc.rust-lang.org/book/functions.html

> Using a `return` as the last line of a function works, but is considered poor style


Right -- explicit returns are frowned upon in the last line of a function, because they're absolutely not necessary, but everywhere else they're kosher.


Yes. Which is what I said. "Using a `return` as the last line of a function works" is not the same as "don't use explicit returns."


So I should use `return` except when I shouldn't? This is the cognitive overhead problem I'm talking about.

The original context of my concern is the instance where the match statement makes up the last statement in the function (frequently the only statement in the function's immediate scope). Since the individual cases are not terminating the function early, to get a value out of a match statement you simply leave the last expression bare.

i.e.

    fn something... {
      match input {
      Ok(input_val) => {/* several lines of semicolon terminated code*/
                        output}
      Err(errval) => {/* several more lines of semicolon terminated code */
              output}
    } 
is correct, but

    fn something... {
      match input {
      Ok(input_val) => {/* several lines of semicolon terminated code*/
                        return output;}
      Err(errval) => {/* several more lines of semicolon terminated code */
              return output;}
    } 
is poor style, and

    fn something... {
      match input {
      Ok(input_val) => {/* several lines of semicolon terminated code*/
                        output;}
      Err(errval => {/* several more lines of semicolon terminated code */
              output;}
    } 
is an error.


> So I should use `return` except when I shouldn't? This is the cognitive overhead problem I'm talking about.

No. Use `return` only when you must. If you want an early return in a function, then you need to use `return`. If you don't need an early return, then don't use `return` at all.

I can't remember if this was ever a cognitive load for me personally. I don't think it was.


> I can't remember if this was ever a cognitive load for me personally. I don't think it was.

Well, to be fair, it sounds like you're used to semicolons having special meaning (aside from separating expressions) from a previous language.

> As for `;`, it's just like Standard ML. `;` is for sequencing expressions. I love it.

It's worth remembering that most users of C, C++, Ruby, Java, Python, or whathaveyou are not used to the semicolon having special meaning. Since Rust appears to be primarily aimed at replacing C++, this is going to be a significant change which will likely trip people up for awhile.


I'm a C/Python person, and it did take a question on irc for me to get my head around how Rust does it, but now it seems so natural that it's a little irksome to look at some of my Python code :)


Ruby also does this, so if you want that natural feel in your dynamically typed language... ;)


> This is the cognitive overhead problem I'm talking about.

The cognitive overhead of having the value of a function be the last expression in it is incredibly minor; lots of languages have this feature. Even JavaScript and C# have this with their arrow functions.

> …is an error.

The only time you get an error is if you tried to do something that you couldn't do in C++ at all (returning a value without typing return). I don't see how that would confuse C++ programmers. C++ programmers who write Rust using explicit returns everywhere will have their programs work exactly as they expect.


>The cognitive overhead of having the value of a function be the last expression in it is incredibly minor; lots of languages have this feature.

This can cause performance problems in Coffescript because for loops are also expressions.


Wow. I wrote that article on LtU last night, after going over there to see what the language theorists were saying about Rust. (And after, for the third time in three weeks, having my Rust code fail to compile because the Rust crowd changed the language again, after the "alpha release" and its claims of stability: http://blog.rust-lang.org/2014/12/12/1.0-Timeline.html) I wasn't expecting it to be picked up on Hacker News.

Rust is going to be very important. The ownership system is a major step forward in language design. It's a huge improvement over C/C++.

It's not easier to write than C++. Rust may feel clunky for people coming from Javascript, Python, Ruby, and PHP. Having to think about lifetime issues for mere strings is a new cognitive load. The big win with Rust is that most of the errors are caught at compile time. This is Rust's big advantage, but alien to scripting programmers. The Rust compiler report errors in three phases. First you get all the syntax errors, and until the syntax is perfect, that's all you get. Then you get all the type errors, and until the type issues are perfect, that's all you get. Then you get the ownership errors. Ownership is a global analysis; ownership problems involve at least two points in the program. The compiler produces good, but very wordy error messages. (Hint to Rust developers: put in a line length limit and word wrap for long compile time error messages.) If your ownership design is faulty, the result is likely to be "fighting with the borrow checker", because the problem isn't local, and just fixing the compiler-reported error will make the problem pop up elsewhere. The cleverness of the ownership system is impressive, but some programmers are going to feel like they're being hammered by it. Successful C++ programmers won't have a major problem with this. It may be tough on the Javascript crowd.

Rust requires some advance planning, which may be incompatible with "agile" development. It's also difficult to port code from other languages to Rust without rethinking the ownership and bounds logic. There is a port of Doom to Rust. It has a lot of unsafe code, because Doom's internal memory structures are not directly compatible with Rust's. Such problems will recur as big packages with delicate internals are ported over to Rust.

This is partly a documentation problem. The current tutorial (http://doc.rust-lang.org/book/hello-cargo.html) is relentlessly upbeat and glosses over too many of the hard problems. Once some third-party books have been written, that situation should improve.


Not easier than C++? I dunno, C++ looks like a clusterfuck of complicatedness. I'm mostly coming from F#, with some C. Rust's ownership system just makes sense and seems to perfectly answer the questions I have when using C APIs. Rust, for me, looks a lot like what I've wanted when writing performance F#. I'd commit atrocities to have optional ownership in F#.

Every time I've fought with the borrow checker, it's because I've had a serious design or conceptual flaw. (Well, apart from syntax/compiler questions). Since you can derive most of the rules just by thinking about it, I find it grows on you quickly. I'll be very saddened if the borrow checker actually ends up being hurtful for adoption overall. Though I agree if you cannot handle pointers or think about memory, Rust will be difficult. So yeah, scripting only devs will have trouble. But! It's better than them writing the code in C.

Rust overall seems like that. There's less random stuff and things work mostly by thinking about safe, zero overhead abstractions, and what falls out from those mandates. Mostly.

Now, maybe if I was a modern C++ programmer, I'd find the effort about the same. OTOH, C++ systems don't end up as safe as Rust ones, so I'm not sure there's a perfect comparison to be had. Maybe that'll change as C++ has started catching up feature wise, but someone I doubt it.


Overall, I think Rust is easier to use than C++ but not easier to write than C++. Writability is a specific facet of usability, which is hurt by strict upfront checks.

Writability is a worthy thing to improve: GHC's -fdefer-type-errors feature is a good example. I hope Rust improves its writability in the future.


I think just like other languages, writing Rust leads to a familiarity with what the compiler wants. Then you spend more time thinking about design and the problem at hand, and less fighting the compiler. I think Rust will ultimately be easier than C++ to write because while there are tricky concepts to master, there should be much fewer of them than C++ has.


I disagree. While you get familiar with what the compiler wants (certainly I did, after using Rust for more than 2 years now), it's just that Rust compiler wants more from you than C++ compiler does.

The number of tricky concepts largely do not affect writability, because you don't have to use them. The number of mandatory compile time checks do, because you have to pass them.

One way to improve writability is a mode to turn off some of checks while you are exploring. I already gave a good example from Haskell. When you are done exploring, you can turn on all the checks.


Hmm, so like a Rust flag like --allow-leaks or --corrupted-ownership?

It seems like editor support would totally dominate here. That is, realtime checking of lifetimes and autocomplete/inline errors should be a much bigger improvement in write times than some way to emit invalid code.


Thank you, you've managed to nicely express my feelings on Rust. For coming up with a polished end product that works, Rust is easier, but for bashing on your keyboard to get out your ideas it can be a little frustrating compared to even C++.


Just a historical note: Rust's ownership system has much historical pedigree, such as the Cyclone language and the work on ML compilers with region inference, following Tofte and Talpin's pioneering work. The Wikipedia article (https://en.wikipedia.org/wiki/Region-based_memory_management...) has a good overview.


I know, I wrote one of the early papers: Strict Mode for C++ (2001): http://animats.com/papers/languages/cppstrictpointers.html Further back, Ada has region-based memory allocation.

Around 2002, when there was concern about computer-related terrorism, I suggested that the C++ standards committee's unwillingness to deal with memory safety constituted material support of terrorism. They were angry, and terrified. That post was actually deleted from their USENET group.


Although as a long-time C++ programmer I'm the first to criticize it when it deserves it, that's a pretty assholish thing to suggest. As if we needed more decisions being made by fear instead of logic around that time.


> memory safety constituted material support of terrorism.

I love this. Not the terrorism charge per-se, but the general idea. Using unsafe code should be shunned, in general. After Rust 1.0 or so, any new C projects, especially for people not heavily invested in C, should be met with questioning.

I'm looking at this one project, mostly PHP. But they need some socket and packet handling, and they wrote it in C. Complete with lines like:

  thing = malloc(somesize); // gets memory
  ...
  free(thing); // give memory back
And network-connected string parsing galore. Or a million line project, used in many networks, compete with its own hacked up XML processor, that has never issued a security advisory. I'm looking forward to a time when the choice to use C here would be questioned and viewed with a doubtful eye by everyone.

Propaganda similar to "loose lips sink ships", aimed at memory unsafe code, might help achieve that goal.


I had no idea that Ada has regions. The Wikipedia article doesn't mention it. Maybe somebody who knows about it wants to edit the article?


I agree with many of your points actually. But I wrote the Doom renderer in rust at https://github.com/cristicbz/rust-doom and it's not a port, it's written from scratch based on specs, so maybe your argument is correct in general, but it really doesn't apply to it: it has very little unsafe code, some which really shouldn't be:

1. A tiny function which loads textures. I was fooling around with optimising load speeds. The actual speedup I got was insignificant and I really should revert it to safe code.

2. A silly getter on the transformation matrix---another misguided attempt at a speedup; no reason to use UnsafeCell instead of RefCell. Should revert.

3. Casting buffers from files as structs. This is safe for any buffer the same size as a Copy struct, std offers no such function, so I do. Unsafety is isolated in two small functions (one for T and one for Vec<T>), all its callers are safe.

4. Interacting with OpenGL. Fair enough, this happens all over the place, but it's not actually unsafe. The OpenGL bindings library didn't take much care to only mark unsafe operations as such, and all GL calls are marekd unsafe---which is why I wrap them up in a macro which peforms the operation in an unsafe block and panics on any error. I should port my code to glium and then this would go away as well.

*better formatting


I don't have an account there so I'll comment here:

> In particular, allocating a new object and returning a reference to it it from a function is common in C++ but difficult in Rust, because the function doing the allocation doesn't know the expected lifetime of what it returns.

This is what boxes are for. A Box is a unique pointer to a value on the heap and can be used without knowing compile-time lifetimes. References and lifetimes allow you to safely return pointers to stack allocated objects. In C++, you'd have to do this:

  MyType value;
  my_function(&value);
When returning references, rust uses the lifetimes instead of explicit declarations to figure out where (on the stack) `value` needs to be allocated.

> Declarations are comparable in wordiness to C++.

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

> Rust has very powerful compile-time programming; there's a regular expression compiler that runs at compile time. I'm concerned that Rust is starting out at the cruft level it took C++ 20 years to achieve. I shudder to think of what things will be like once the Boost crowd discovers Rust.

Unlike C++, 1. Macros from one crate aren't imported into another unless the user explicitly requests that they be. 2. Macro invocations are clearly macro invocations. You never have to wonder if something is a function or a macro.

> The lack of exception handing in Rust forces program design into a form where many functions return "Result" or "Some", which are generic enumeration/variant record types. These must be instantiated with the actual return type. As a result, a rather high percentage of functions in Rust seem to involve generics.

How is this a problem?

> There are some rather tortured functional programming forms used to handle errors, such as ".and_then(lambda)". Doing N things in succession, each of which can generate an error, is either verbose (match statement) or obscure ("and_then()"). You get to pick. Or you can just use ".unwrap()", which extracts the value from a Some form and makes a failure fatal.

I agree that this is less than ideal. However, IMHO, this is better than Java and C++.

Java:

Libraries tend to bubble everything. This leads to long throws clauses in function signatures with unexpected exceptions. A user of these libraries often catches and ignores these exceptions when writing the first draft of his or her programs because they don't make sense (why handle IO Errors when using a collection?). And then, because his or her program works, he or she forget about the ignored exception cases turning them into silent errors.

On the other hand, in rust, you can only return one error. When writing a function that has multiple failure modes, this forces the programmer to think about the set of failures that can happen and come up with new error type. This doesn't force the programmer to come up with a meaningful error type but it gives them the opportunity.

Additionally, like in Java, Rust programmers can ignore errors (`unwrap()`). However, unlike in Java, these ignored errors are not silent, they are fatal.

C++:

Exceptions are unchecked and everyone I've talked to avoids them like the plague. In the end, C++ exceptions end up acting like rust's `panic!()` because programmers don't check them but are used like Java's exceptions because programmers could check them.

> There's a macro called "try!(e)", which, if e returns a None value, returns from the enclosing function via a return you can't see in the source code. Such hidden returns are troubling.

I agree that hidden returns can be troubling. However, in rust, only macros can lead to hidden returns, macros use a special syntax (`macro_name!(args...)`, and macros have to be explicitly imported.

> All lambdas are closures (this may change), and closures are not plain functions. They can only be passed to functions which accept suitable generic parameters. This is because the closure lifetime has to be decided at compile time.

The first sentence is correct but the last two are just wrong:

    fn takes_a_function(f: Box<Fn()>) {
        (f)();
    }
    fn main() {
        takes_a_function(Box::new(move || { println!("hello world") }));
    }
The `Box` allocates the closure on the heap and the `move` causes the closure to capture by value. This means that this closure (`f`) can be moved freely without lifetime restrictions because it doesn't reference the stack. However, most functions that accept closures use generics and do any necessary boxing internally to make the user's life easier.

> Rust has to do a lot of things in somewhat painful ways because the underlying memory model is quite simple. This is one of those things which will confuse programmers coming from garbage-collected languages. Rust will catch their errors, and the compiler diagnostics are quite good. Rust may exceed the pain threshold of some programmers, though.

Rust is a systems language. It exposes a lower-level (not simple) memory model because systems programmers need it. If you want garbage collection, you are free to roll your own (yes, you can actually do this in rust).

> Despite the claims in the Rust pre-alpha announcement of language definition stability, the language changes enough every week or so to break existing programs.

Re-read those claims. Alpha means fewer breaking changes and no "major" breaking changes not stability.


> References and lifetimes allow you to safely return pointers to stack allocated objects. In C++, you'd have to do this: MyType value; my_function(&value); When returning references, rust uses the lifetimes instead of explicit declarations to figure out where (on the stack) `value` needs to be allocated.

OMG, thank you for including this. I spent several months reading every bit of documentation that was available for Rust, and programming in it daily. Made some good progress. But I never, never came across this explanation. Very enlightening.

Rust desperately needs documentation covering these kinds of details. How on earth is someone supposed to make serious use of the language without knowing this?

I believe the Klabnik documentation hinted at this (something like, "The Rust compiler is smarter than that" and therefore you don't need to overuse pointers), but by no means did it actually spell it out. And you only needed a few sentences to cover it.

I know the Rust community is aware that more documentation is needed and has a todo list a mile long. But I don't know if technical details such as this are high enough on the priority list.


> But I don't know if technical details such as this are high enough on the priority list.

There are actual features which still have no documentation. It's hard being a single person trying to keep up with changes from tons of other people, many full time and some community. I may be the person who is most looking forward to Rust being stable...


And I should say, for the record, that you deserve a ton of credit for being a nearly superhuman job. The doc may be lacking, but it would be far worse off without you.

Still, I wish the team/community could find a way to shore up the coverage of this kind of information. I think we've given beginners enough for now, that the focus should shift to "details you need to know about what the compiler does."


Thank you, I appreciate it. These kinds of comments are the ones I try to re-read when I'm feeling down about stuff.

Yes, I agree, I'd love more help :) I think you'll see a shift after 1.0.0-beta happens, since then, the focus will be on polish, rather than shipping every breaking change.


> I may be the person who is most looking forward to Rust being stable...

No, that would be me! :)


>Unlike C++, 1. Macros from one crate aren't imported into another unless > the user explicitly requests that they be. 2. Macro invocations are > clearly macro invocations. You never have to wonder if something is a > function or a macro.

Given the reference to Boost, the author is almost certainly talking about template metaprogramming, not C macros. TMP is obviously a lot more limited in scope than Rust macros, but it could hardly be called dangerous; I doubt anyone's ever invoked it by accident.


Boost has all kinds of preprocessor macro stuff, like a loop construct that works through recursive includes, and the foreach macro (hopefully obsoleted with C++11).


True, but since the context of the comment was about how crufty he worries Rust would get once the "Boost crowd" discovers Rust macros, I doubt that it's really the specific topic of "relative merits of language constructs given the name 'macro'" that's under discussion (the word 'macro' doesn't even appear until later in the post). Certainly C macros aren't the main source of cruft in Boost.


> 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.


> 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.


> In particular, allocating a new object and returning a reference to it it from a function is common in C++ but difficult in Rust, because the function doing the allocation doesn't know the expected lifetime of what it returns.

I'd like to see a code snippet explaining this problem.


Yeah, it's unclear what he's talking about there. Normally when a function allocates a new object, it would want to return it by move (transferring ownership), rather than by reference. That doesn't involve any lifetimes.

    fn make_a_foo() -> Box<Foo> {
        Box::new(Foo { a: 5 })
    }
If the function allocated memory and only returned a borrowed reference, who would be responsible for freeing it? Yes, Rust will make you stop and think there, as it enforces memory safety.

In cases where it does make sense to return a reference to a new object, like allocating from an arena, the lifetime ('a) of the returned reference will be the same as the lifetime of the arena.

    fn new_from_arena<'a>(arena: &'a TypedArena<Foo>) -> &'a mut Foo {
        arena.alloc(Foo { a: 5 })
    }
But Rust can infer the lifetime, so that can be shortened to:

    fn new_from_arena(arena: &TypedArena<Foo>) -> &mut Foo {
        arena.alloc(Foo { a: 5 })
    }


Any reason you would ever want to return a Box instead of just a Foo, which the caller can then put in a Box if so desired?


I can think of a case: when you want to return a type whose size is not known at runtime (a "dynamically-sized type" a.k.a. DST), then you need to stuff it behind something else whose size is known so that the compiler can statically determine how much memory to allocate before calling your function. You could use a reference for this task, but only if, as per Rust's usual rules, you have a parameter to your function whose lifetime you can tie it to. If you don't have such a parameter, then a Box is your next best bet.

Somewhat related to this, there's also currently a language deficiency where you can be forced to use Box when returning a closure from a function. This will be addressed shortly after 1.0.


I think he may not understand that Rust has move semantics by default, and so returning a Box just transfers ownership of that object.


> Despite all this, Rust is going to be a very important language, because it solves the three big problems of C/C++ that causes crashes and buffer overflows. The three big problems in C/C++ memory management are "How big is it?", "Who owns and deletes it?", and "Who locks it?". C/C++ deals with none of those problems effectively. Rust deals with all of them, without introducing garbage collection or extensive run-time processing. This is a significant advance.

What? C++11/14 solves these issues.


> What? C++11/14 solves these issues.

You're right that C++ provides a solution to the first two, but C++ locking via std::mutex isn't done in the same way as Rust: in Rust the mutex owns the data and prevents you from getting access to it unless you lock. std::mutex, however, is a separate value from the data it protects and it's up to you to coordinate access to that data.

I would also argue that Rust is a better solution to the first two issues. Modern C++ does not solve the problem of use-after-free (dangling references and invalid iterators are very possible, and common in large codebases). This is something that I don't believe C++ can solve without becoming a radically different language. Furthermore, Rust forces you to use the right patterns unless you type "unsafe": this is, again, important for security, reliability, and developer productivity, reducing the amount of time you spend in the debugger.


My simplification of matters is that while C++ can now do everything right, it still easily lets you do everything wrong.

Rust compels correctness, so for any project where you can't trust your coworkers aptitude towards correctness (and that is to say you even trust your own) Rust is an insane productivity booster. We could have avoided millions of hours of work and thousands of zero day and system destroying bugs if we had OS cores written in a language like Rust.


> so for any project where you can't trust your coworkers aptitude towards correctness

So, for any non-trivial project. People will make mistakes, no matter how skilled or experienced they are. Catching these mistakes at compile-time can be a huge gain for security and stability.

Though it remains to be seen is what kind of maintenance/development burden these constraints introduce over the longer term lifecycle of a software project.


Also, it's worth noting that "C++ provides a solution" is not "C++ solves these issues". Somewhere in any C++ codebase of significant size, someone has done it wrong and the compiler isn't going to tell you where.


Fair points

Rust’s synchronization primitives are immature — they’ve been rewritten once or twice in the past year or so — but cool from a usability perspective.

edit: oh, hello pcwalton. I suspect you knew this already. :P


[Citation needed]


?


Based on what did you concludes that?


No; modern C++ provides the tools for which disciplined use solves these issues. The problem is that one can silently subvert that discipline, and still introduce memory errors.

Rust enforces memory safety at the language level. C++ itself does not "know" about memory safety. This difference, to me, is huge. You can still opt out of memory safety in Rust through unsafe regions, but the fact that Rust provides memory safety guarantees to non-unsafe regions is, to me, a change in kind, not degree. When the only thing enforcing memory safety is disciplined use, it's still too easy to make a mistake.


> What? C++11/14 solves these issues.

Sure, it solves some of those problems if you use exclusively smart pointers and vectors, and never use the built-in language pointers and arrays. What forces you to do so, when '*' and "new" and [] are right there?

Rust pushes you towards the right solution by requiring an "unsafe" block if you use raw pointers or arrays. That doesn't prevent you from using them (such as in the implementations of higher-level constructs or FFI calls), but it does hint that they're the wrong solution for everyday programming.


Only if you are allowed to use said features.

Many C++ codebases out there are still pre C++98.

I like C++, but I don't see the opportunity where to use C++14 outside hobby projects.


The issue holding most serious projects back is MSVC's historic lack of support for C++11 (and now 14/17) features. Thankfully, it looks like this is starting to change with MSVC 15 [0].

As an anecdote, the (very large) project I work on at $DAYJOB is finally moving from C++03 to C++11 in the next few weeks.

[0] - http://blogs.msdn.com/b/vcblog/archive/2014/11/17/c-11-14-17...


There are more platforms out there than Mac OS X, GNU/Linux and Windows.

Many embedded platforms for example.

There are also enterprise customers that only allow for projects using the installed compilers, usually from the OS vendors.


May I ask you where do you work?


C++14 may be a bit new for some circumstances - but surely it's more mature than Rust (and is likely to retain that advantage).


> I like C++, but I don't see the opportunity where to use C++14 outside hobby projects.

Sure, but I’ve encountered some notable exceptions: LLVM projects (C++11), Playstation 4 games (C++11/14), QT5 (C++11) projects, et al.


None of those are the typical corporation code that most of us are exposed to.


Maybe? I was addressing your “I don’t see the opportunity where to use C++14 outside hobby projects.”

Compilers, games, and operating system components are archetypical systems software.


How many Fortune 500 do you see writing those?


Microsoft, Apple, IBM, Amazon?


How many C++ developers can apply to work on those world wide?

The point isn't what companies are using C++11 or C++14, rather what companies for the common C++ developer on the street.

Sometimes HN seems to think SV is the only place where programmers live.


Google is on C++11, or was when I left in 2014. They may be on C++14 now.


If your employer won't use provably stable and almost entirely backwards compatible language improvements, you might want to just find another job. C++ is a hard enough language that someone out there will be willing to be sane about the language version they use.

I know I'd only touch 20 year old C++ code with the go-ahead to refactor it into C++14. I'd argue that not adopting smart pointers, standard threads and mutexes, and auto type deduction make projects effectively unmaintainable at sufficient scale.


Back in 2005 my employer decided it was about time to move away from C++ in enterprise projects.

Actually in most of the enterprise projects I have been involved, C++ tends to be restrained to bindings to some feature not exposed in the chosen languages.

Changing employer doesn't matter, as most of these decisions come from the customers themselves.

It might work for in-house products, not so well when doing consulting.


Care to elaborate? I don't see how it does.


Yes, in particular, iterator invalidation (problem 2 in that list) seems unsolvable in C++, as far as I can see.

I've hit iterator invalidation in a new c++11 codebase. Perhaps c++14 offers something to help it that I'm not aware of?

edit: "unsolvable" in the sense of the language making it impossible,


Yeah. It isn’t enforced, but it’s easier to mitigate.


I tried to learn a bit of rust and I couldn't shake the feeling that rust is a very ugly language, like uglier than c ugly.


People said this about Clojure and I never fully understood why. I've dabbled in Rust and while I've always loved Ruby for it's aesthetic I never had a problem with Rust. It suits the language. Feels like you're writing serious code in a serious language. Which is appropriate.


What makes you say this? And what is your prior language experience?


I tried to learn a bit of rust and I couldn't shake the feeling that rust is a very ugly language, like uglier than c ugly.

What makes you say this? And what is your prior language experience?

Right there is the problem. There are many programmers for whom it's their day job. Others depend on their code doing something useful and important. But they're not theoreticians, don't have advanced degrees in CS, and haven't written in a dozen languages. A new language has to be usable by them to get traction.

The Rust community, at this point, is mostly people who know several programming languages and want to try a new one. Look at the comments above from people who compare type systems in different languages and are aware of the strengths and weaknesses of different approaches. Note the references to obscure languages and research papers most programmers have never heard of, let alone used or read. This is not the target market for a new language, if it is to be a success. It has to be used by people who don't debate language theory issues while the Super Bowl is on.


> I just hope the Rust crowd doesn't screw up.

If i had a pound for every time i'd heard this sentiment, i'd be a rich man!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: