Yes, the compiler's error messages are extremely good. They often give correct and useful suggestions on how to change code to make it compile. This can be invaluable when dealing with trickier corners of the language such as explicit lifetimes.
Can someone chime in as to why C++ compilers in particular are notorious for extremely verbose yet unhelpful error messages? Is it something endemic to parsing the language that there can't be errors as helpful as Rust's?
The short answer, devoid of some interesting nuance, is that C++’s template system is similar to an untyped functional programming language, so the errors you get are the compiler’s attempt to make sense of the large tree of nonsense it couldn’t reduce.
C++ also suffers from years of backwards-compatibility twister creating abundant cases of ambiguity if you do something wrong. The compiler will do it’s best, but if your code suddenly parses wildly differently because your semicolon is missing, oh well. Rust is far simpler in that regard, so there are fewer instances where your code might accidentally make sense to the compiler if you have an error.
> Rust is far simpler in that regard, so there are fewer instances where your code might accidentally make sense to the compiler if you have an error.
I say this from a place of love for Rust, but the first time you run into a massive generic type tree error thats root cause is a type parameter three levels deep not implementing Send (or some other trait) it takes your breath away.
There are some great jokes about this in Deisel and Futures for example. It makes Java generics look like a baby just starting to craw.
The error messages are good, you just need to go grab a coffee while you read through the book on type theory that’s been dumped to your screen.
Similar stuff happens on Haskell too. I guess that since the type system is an unityped logic programming language, it is an even worse place than C++ templates. It is a smaller problem on practice only because the type declarations are much shorter programs than C++ templates.
Let me just stress the first point, because that's exactly where Rust and C++ differ: C++ templates really are untyped. There is no way to check whether a template definition is correct. A C++ compiler has to first expand the templates ("complete monomorphisation") and then perform type checking. Any errors you get will be in terms of the template instantiations, not your original code.
Unlike C++, Rust has a type system for the full language. In particular, traits are type checked once and you get errors in terms of the code you wrote yourself. There are other compilers that use complete monomorphisation, such as the MLton compiler for Standard ML, where you don't hear horror stories about terrifying error messages because your code was checked before being specialized.
I want to stress that this is a terrible design decision in terms of usability, because it is incredibly attractive from an implementation perspective. Parametric types are a delicate issue in an imperative language and always end up rejecting some perfectly fine programs. Monomorphisation both eliminates this issue and potentially leads to more efficient code, but your type errors will suffer.
I could say more about this - there are more trade-offs involved - but actually working with template heave C++ code is a better argument than anything I could say.
...That said, the examples in the blog post actually don't use any complicated machinery, and the quality of the compiler errors just comes down to very good engineering on the part of the Rust development team. :)
As well as templates, C++ has function overloading, so when a function is named in an error message it has to show all the arguments as well. And if it can't find a function variant that matches a call, it'll print out a "did you mean this one?" message for all of them.
And of course C++ harks back to an era where error messages were not considered much of a concern. It took the Clang project for C compilation errors to start improving (including kicking gcc's ass), and C is not the most difficult language to parse/validate.
I suspect it's the inverse: the Rust compiler team has specifically put a lot of effort into good error messages, whereas for C++ compiler developers it's been a low priority. Some of the inspiration came from Elm [1], which has famously good error messages.
C++ template system is too generic (Turing complete), and it is usually the one to blame for the verboseness of error messages.
Something like Concepts would probably make it more like a proper "generics" mechanism and allow better error messages - ie, "this type does not implement EqualityTrait" instead of "signature mismatch <detailed and very verbose function declaration 1> and <detailed and very verbose function declaration 2>".
Despite this, newer g++ versions have better error messages overall - but sometimes you are stuck with an older version of the compiler as well.
I don't see that big a deal. In a language, say Ada, I would pass in an "IN" parameter, which cannot be altered in a procedure (and by default in a function). In other languages I would keep the scope/lifetime extremely small, and just not mutate or leak memory. This is just plain ol' good programming practice IMHO.
Is there another more compelling use case. In concurrent applications perhaps?
Yes, being able to pass ownership of a Vec to another thread is important, because otherwise the two threads have to coordinate over when to call its destructor.
Great write up, when I started coding in Rust, I sometimes used to get frustrated, but then realised how helpful the checker is and really made me think about structure and mechanics of my code, making me a better developer.
From that writeup, it seems like one is forced to program Rust in an object oriented manner. Is it possible to program it in a functional manner like Lisp, or a procedural manner like BASIC or assembler?
> And tldr is yes it lends itself to a functional style
Rust does a good job of supporting a functional style efficiently. It's also considered idiomatic to code functionally. Probably the biggest, obvious exception is that Rust embraces mutability because it's statically safe to do so i.e. you aren't as susceptible to the traditional disadvantages of mutability.
> You do not use inheritance nor do you have objects
That's only partially true though. Rust does support interface inheritance and polymorphism in its trait system. And enums are a closed form of sub-typing. RAII is also idiomatic and I'd argue that many would class that as object-oriented.
The way I'd characterise the OO approach in Rust is that the team looked at the experiences of using OO in other languages and picked out what they considered the valuable pieces and left the dangerous ones (like implementation inheritance).
They must by definition be. I disagree vehemently with your statement. If you were correct, then there would be absolutely no point to programming "functionally", it would be absurd. Might as well go back to programming in Java or C++, which is utterly counterproductive.
No, functional programming is a paradigm. An algorithm is a language independent, abstract description of solving a specific task, like for instance searching or sorting.
Functional programming and object oriented programming are (largely) orthogonal issues. It is, in fact, quite possible to combine both approaches in a language.
Java and C++ are really imperative + OOP, not pure OO. The closest to pure OO are going to be languages like Smalltalk which still has some imperative concepts and some concepts from functional programming (closures and higher order functions via blocks, in particular).
Scheme allows for object oriented programming, though not directly in the base language spec. You have to build out some infrastructure for it, but then it's quite effective for it.
You're implying that Rust's traits "partially" make for an object system. Given Rust's trait are essentially a restricted form of Haskell's typeclass, these would by your assertion "partially"+ make for an object system as well.
Interesting. I'm coming from Java that looks-like an object to me. I assume method dispatch here is based on the "receiver" as well right? What differentiates it from an object? Not having identity? or Not being self describing (like a.getClass()) maybe?
Syntax is just syntax. Imagine a function that takes two 32-bit numbers, and adds them together:
fn foo(x: i32, y: i32) -> i32 {
x + y
}
You'd call it like this:
foo(5, 6) // would give back 11
However, this is just syntax. There's no reason this can't be
5.foo(6)
It's purely a different way of calling the same thing. The first argument goes before the dot.
In some languages, this is 100% interchangable. In Rust, it's not 100%, but it's pretty true. For example, the wrapping_add method adds two numbers together, wrapping around if the number is too large. These are equivalent:
let x: i32 = 5;
assert_eq!(x.wrapping_add(5), 10);
assert_eq!(i32::wrapping_add(x, 5), 10);
You can use either method or function call style.
These aren't objects, they're just primitive 32 bit numbers. There's no heap allocation, there's no extra bookeeping info.
There are some more details, but that's the basic idea. Does that make sense?
What difference does it make how one calls it if I have to program the same way I'd have to program if I were using object oriented paradigm? Or even worse, if I'm forced to debug and understand code written in object-oriented style, how does that help debug and understand the program?
It's a travesty this syntax or anything resembling object-oriented programming is allowed.
By the by, the best programming languages aren't ones where there is more than one way to do it, but ones where there is one and exactly one way to do any given thing and the reason is simple: the code is easier to understand and therefore enhance or debug for anyone proficient in that language; in addition, it reduces the total number of mistakes in software written in such a language. AWK is one example of such a language.
In my experience, it’s not much more OO than Haskell. Even in many functional languages, you typically want some OO — you’ll want some core type that you export opaquely, and then you have functions doing logical operations on that.
I think two of the biggest differences are:
- Because the type and memory systems make it possible to skip heap allocation if you’re careful, it’s “idiomatic” to prefer avoiding techniques that still require it. This means avoiding long-lived closures. Note that closures still are used very heavily, but typically aren’t passed around between many owners like in Lisp or JavaScript.
- Similarly, because Rust can give you incredible safety and speed if you use it a certain way, people tend to write with interior mutability very often. However, unlike in most languages, Rust’s type systems is a tremendous help to the programmer with mutability, and it’s much, much more likely that you’ll see interior mutability done right. It allows you to program in a style that feels very functional even while using many imperative components. A lot of your code still ends up as basic pattern matching and simple closures.
Regarding closures, note that Rust closures don't allocate on the heap unless you explicitly use a Box to put them on the heap. Long-lived closures aren't a problem, because closures by default borrow the variables they close over (and like all borrowed references, these cannot extend the overall lifetime of the borrowed data); alternatively they may take ownership of their closed-over variables, but in that case the closure can only be called exactly once (as expected for owned values).
A closure taking ownership of the closed-over variables doesn't actually imply that it is FnOnce (only callable once). You can perfectly well have a Fn or FnMut closure that owns its captures, as long as you don't try to do any moves inside the closure. If you need to do a move in an FnMut closure, you can also wrap the captured variable in an Option, and use .take()
I would. That's the whole point: in object-oriented programming, everything revolves around the state machine, whereas in functional programming, every function is idempotent and the state machine irrelevant. Lisp is a good example of that.
> From that writeup, it seems like one is forced to program Rust in an object oriented manner.
That's really not the case. In fact I'd say the opposite, the focus on ownership, limitations of the borrow checker (partial borrows through function calls) and lack of inheritance makes "object oriented programming" rather difficult. That functions are commonly namespaced through traits or structs does not intrinsically make it "object oriented".
It has a number of functional APIs (many standard structures have various HoFs for manipulating them and combining them), although the ownership and borrow checking bits can also make them somewhat less convenient.
I just sent this around to my entire (mainly Java) team. I think it might be the best intro to describe the fundamental memory model that I’ve read.
The walk through this basic example, showing interface changes that enforce different aspects of the code, are wonderful. It took me a long time to grok some of these basic concepts, and this explains them so concisely.
I might use this as an intro to Rust for anyone who starting to learn the language. Kudos to the author for expressing these concepts in such elegance.
That's a nice way of introducing type state programming in an affine programming language!
In general though, type state programming would be even nicer in a linear language. For example, in the http server I could start writing a response without ever writing a body. Rust would happily accept this code, because Rust is always allowed to throw away resources.
An affine language controls the duplication of resources, while a linear language controls both duplication and destruction of resources. In a linear Rust, a data structure would have to implement Drop in order to be silently deleted and in the http server "HttpResponseWritingHeaders" should not implement Drop.
50 comments
[ 2.7 ms ] story [ 89.0 ms ] threadC++ also suffers from years of backwards-compatibility twister creating abundant cases of ambiguity if you do something wrong. The compiler will do it’s best, but if your code suddenly parses wildly differently because your semicolon is missing, oh well. Rust is far simpler in that regard, so there are fewer instances where your code might accidentally make sense to the compiler if you have an error.
I say this from a place of love for Rust, but the first time you run into a massive generic type tree error thats root cause is a type parameter three levels deep not implementing Send (or some other trait) it takes your breath away.
There are some great jokes about this in Deisel and Futures for example. It makes Java generics look like a baby just starting to craw.
The error messages are good, you just need to go grab a coffee while you read through the book on type theory that’s been dumped to your screen.
Unlike C++, Rust has a type system for the full language. In particular, traits are type checked once and you get errors in terms of the code you wrote yourself. There are other compilers that use complete monomorphisation, such as the MLton compiler for Standard ML, where you don't hear horror stories about terrifying error messages because your code was checked before being specialized.
I want to stress that this is a terrible design decision in terms of usability, because it is incredibly attractive from an implementation perspective. Parametric types are a delicate issue in an imperative language and always end up rejecting some perfectly fine programs. Monomorphisation both eliminates this issue and potentially leads to more efficient code, but your type errors will suffer.
I could say more about this - there are more trade-offs involved - but actually working with template heave C++ code is a better argument than anything I could say.
...That said, the examples in the blog post actually don't use any complicated machinery, and the quality of the compiler errors just comes down to very good engineering on the part of the Rust development team. :)
[1] https://blog.rust-lang.org/2016/08/10/Shape-of-errors-to-com...
Something like Concepts would probably make it more like a proper "generics" mechanism and allow better error messages - ie, "this type does not implement EqualityTrait" instead of "signature mismatch <detailed and very verbose function declaration 1> and <detailed and very verbose function declaration 2>".
Despite this, newer g++ versions have better error messages overall - but sometimes you are stuck with an older version of the compiler as well.
Is there another more compelling use case. In concurrent applications perhaps?
There was a recent post comparing rust to haskell https://www.fpcomplete.com/blog/2018/10/is-rust-functional. And tldr is yes it lends itself to a functional style...
Rust does a good job of supporting a functional style efficiently. It's also considered idiomatic to code functionally. Probably the biggest, obvious exception is that Rust embraces mutability because it's statically safe to do so i.e. you aren't as susceptible to the traditional disadvantages of mutability.
> You do not use inheritance nor do you have objects
That's only partially true though. Rust does support interface inheritance and polymorphism in its trait system. And enums are a closed form of sub-typing. RAII is also idiomatic and I'd argue that many would class that as object-oriented.
The way I'd characterise the OO approach in Rust is that the team looked at the experiences of using OO in other languages and picked out what they considered the valuable pieces and left the dangerous ones (like implementation inheritance).
It supports no more than Haskell's typeclasses (rather less in fact), are typeclasses an object system?
> And enums are a closed form of sub-typing.
Rust enums are pretty standard sum types, a staple of statically typed functional languages.
> RAII is also idiomatic and I'd argue that many would class that as object-oriented.
That's defensible but setting one hell of a low bar on the concept of object orientation.
No, did someone say they were?
> Rust enums are pretty standard sum types, a staple of statically typed functional languages
Indeed
I'm not entirely sure what your point is. Functional and OO aren't mutually exclusive.
They must by definition be. I disagree vehemently with your statement. If you were correct, then there would be absolutely no point to programming "functionally", it would be absurd. Might as well go back to programming in Java or C++, which is utterly counterproductive.
Java and C++ are really imperative + OOP, not pure OO. The closest to pure OO are going to be languages like Smalltalk which still has some imperative concepts and some concepts from functional programming (closures and higher order functions via blocks, in particular).
Scheme allows for object oriented programming, though not directly in the base language spec. You have to build out some infrastructure for it, but then it's quite effective for it.
You're implying that Rust's traits "partially" make for an object system. Given Rust's trait are essentially a restricted form of Haskell's typeclass, these would by your assertion "partially"+ make for an object system as well.
That doesn't really have anything to do with whether one can or cannot call Haskell's typeclasses an object system.
Thank you for the insight.
Syntax is just syntax. Imagine a function that takes two 32-bit numbers, and adds them together:
You'd call it like this: However, this is just syntax. There's no reason this can't be It's purely a different way of calling the same thing. The first argument goes before the dot.In some languages, this is 100% interchangable. In Rust, it's not 100%, but it's pretty true. For example, the wrapping_add method adds two numbers together, wrapping around if the number is too large. These are equivalent:
You can use either method or function call style.These aren't objects, they're just primitive 32 bit numbers. There's no heap allocation, there's no extra bookeeping info.
There are some more details, but that's the basic idea. Does that make sense?
It's a travesty this syntax or anything resembling object-oriented programming is allowed.
By the by, the best programming languages aren't ones where there is more than one way to do it, but ones where there is one and exactly one way to do any given thing and the reason is simple: the code is easier to understand and therefore enhance or debug for anyone proficient in that language; in addition, it reduces the total number of mistakes in software written in such a language. AWK is one example of such a language.
I think two of the biggest differences are:
- Because the type and memory systems make it possible to skip heap allocation if you’re careful, it’s “idiomatic” to prefer avoiding techniques that still require it. This means avoiding long-lived closures. Note that closures still are used very heavily, but typically aren’t passed around between many owners like in Lisp or JavaScript.
- Similarly, because Rust can give you incredible safety and speed if you use it a certain way, people tend to write with interior mutability very often. However, unlike in most languages, Rust’s type systems is a tremendous help to the programmer with mutability, and it’s much, much more likely that you’ll see interior mutability done right. It allows you to program in a style that feels very functional even while using many imperative components. A lot of your code still ends up as basic pattern matching and simple closures.
No, never, because it makes the state machine incredibly hard to follow and an absolute nightmare to debug. From experience.
(Before Scala, research was mostly trying to show the opposite: how OO is an edge case of FP, but that didn't work quite so well.)
I mean, Common Lisp has had CLOS since at least 1984.
So it's a multi-paradigm language where any manner of coding is possible.
That's really not the case. In fact I'd say the opposite, the focus on ownership, limitations of the borrow checker (partial borrows through function calls) and lack of inheritance makes "object oriented programming" rather difficult. That functions are commonly namespaced through traits or structs does not intrinsically make it "object oriented".
It has a number of functional APIs (many standard structures have various HoFs for manipulating them and combining them), although the ownership and borrow checking bits can also make them somewhat less convenient.
The walk through this basic example, showing interface changes that enforce different aspects of the code, are wonderful. It took me a long time to grok some of these basic concepts, and this explains them so concisely.
I might use this as an intro to Rust for anyone who starting to learn the language. Kudos to the author for expressing these concepts in such elegance.
In general though, type state programming would be even nicer in a linear language. For example, in the http server I could start writing a response without ever writing a body. Rust would happily accept this code, because Rust is always allowed to throw away resources.
An affine language controls the duplication of resources, while a linear language controls both duplication and destruction of resources. In a linear Rust, a data structure would have to implement Drop in order to be silently deleted and in the http server "HttpResponseWritingHeaders" should not implement Drop.