I could not disagree more! Rust is a great joy to program in. I would rather program in Rust than any other language.
Rust is challenging for many people to learn. That is very different.
I could not disagree more! Rust is a great joy to program in. I would rather program in Rust than any other language.
Rust is challenging for many people to learn. That is very different.
The borrow checker and ownership model is a huge tradeoff. Even those very experienced in it still fight it from time to time. Go made a different trade, they chose GC. And GC _is_ easier. But it has computational overhead (well today anyway).
Consider the cascading implications. Rust has a trait for Copy vs. Move types. This means every type has an implicit, and unknown behavior, on certain assignments:
let v2 = v;
println!("v is: {}", v);
You can't actually tell me whether the above code compiles without consulting other parts of the code. That's not the case for Go. It is thus simpler and more intuitive to program in.But it is a trade-off. Go as a language must have a GC. It will never be suitable for certain tasks as a result.
Go also has less compile time checking. This means faster compiles, quicker dev cycles but less compile time safety. Go chose easier here again. These trade-offs are what make rust seem "difficult" in comparison to go. I don't know if you can ever "fix" them.
I find the ownership model has non-performance advantages in that I find it provides strong design pressure to factor my system well. I would not describe my experience as 'fighting' the borrow checker - I feel positively when the borrow checker sends me back to the drawing board, because it teaches me things about my system I had not properly comprehended.
(There are areas where the borrow checker is overly conservative, but I never hit these in practice.)
> You can't actually tell me whether the above code compiles without consulting other parts of the code. That's not the case for Go. It is thus simpler and more intuitive to program in.
I'm sorry this is just silly. Here's some Go code:
v2 := v + 1
You can't actually tell me if the above code compiles without consulting other parts of the code, because you don't know if `v` can be added to `1`. In Rust, type inference is only local to a function, so you will be able to determine from an actual compile-able code snippet whether or not that use after move is valid.Go also actually might not compile on the exact example you cited (translated to Go, of course) because that code contains an error if `v2` is never used after it is assigned.
In general, you cannot reason about the correctness of incomplete code snippets. This is not specific to Rust.
The ownership and borrowing model may provide many benefits, but those benefits come at the cost of complexity (as defined by Rich Hickey in Simple Made Easy). I have never found the owning/borrowing thing to be very hard. But it is _more complex_. There are _far_ more fundamental concepts baked into Rust then Go. And the interplay of those concepts creates complexity. It requires more knowledge and inherently complicates more things.
The parent that started this says "it requires so much thought". What that means is they have to hold a lot more concepts in their head. It's not that any one concept is hard. It's that holding all those concepts and the interplay in their head at once is hard(er). This is what people mean when they say "Rust needs to improve to attract more non-genius developers."
A huge portion of things you have to think about in the ownership/borrowing model simply doesn't exist with a garbage collected language. You just _don't_ have to think about a huge set of things at all.
Type inference is an example of this tradeoff that both Go and Rust agreed on. This adds complexity and decreases clarity in both languages. The upside is expressiveness and conciseness. We could always make you specify the type with a declaration (like C) and then the snippet you posted would need less external reasoning:
int v2 = v + 1;
While you can't fully reason about any snippet of code, the more core language level concepts I have to address with any snippet the _harder_ and more complex it is for me to consider them.*I'm _not_ trashing Rust here. I'm just stating that some of the good tradeoffs you made here leave the language inherently more complex and thus difficult to use. There are _upsides_ to that. You gain safety, performance, and "pressure to factor [your] system well". Simplicity is very far from everything.
When you're writing acme.org most of those tradeoffs are may not be worth the complexity. When you are writing a safety critical system they damn well are.
*PS on the subject of code snippets and compilation. As engineers, the primary audience of our code is other engineers. As such, I always find some programming language debates have real insight when addressed by my Strunk and White (no really). Consider the following two sentences:
The compiler must know the lvalue. It may then use it later on.
Too me this is the English language equivalent of type inference. We know "it" is "the compiler" based on previous statements. Using pronouns too often in a sentence makes it unclear. Using them too sparingly makes it tedious (and also often difficult to read).
A similar balance holds with concepts the reader must address in a given paragraph. This is the line we all dance when we write code. How many concepts must we hold at once to comprehend what is written?
In contrast, I find the way Go builds concurrency into the language 'complex' in Hickey's sense. Similarly the way that slices and maps are parametric, but other types are not. The implicitness in interfaces. The way capitalization determines privacy. Go seems like a very 'complex' but 'easy' language.
However, I'd even say I'm not convinced that I want my language to be simple. I want my system to be simple; its possible that by abstracting over an inherently complex domain, and by limiting my choices, a complex language can encourage me to create a simple system. I think ownership encourages the creation of simpler systems. I think a well factored system and a simple system are synonyms.
> We’ve established that when ownership is transferred to another binding, you cannot use the original binding. However, there’s a trait that changes this behavior, and it’s called Copy. We haven’t discussed traits yet, but for now, you can think of them as an annotation to a particular type that adds extra behavior.
That's two additional concepts that interplay with variable binding. Two more _implicit_ concepts at that. In contrast maps being parametric doesn't affect other things (other than some syntax which is v0v). It is an additional concept to understand yes. But it's not intertwined with the other language features.
> However, I'd even say I'm not convinced that I want my language to be simple. I want my system to be simple; its possible that by abstracting over an inherently complex domain, and by limiting my choices, a complex language can encourage me to create a simple system. I think ownership encourages the creation of simpler systems. I think a well factored system and a simple system are synonyms.
I agree with you here (especially the later part). Again I do like Go, but I'm not fanatical on this subject. Languages can _certainly_ push you to best practices. Do Rust's? It's probably pretty domain specific.
However, I don't think binding and use of a variable is 'complected' with its lifecycle. These are semantically significant concepts that I like Rust helping me manage, irrespective of their impact on machine memory.
Also, I should be clear that I do web development professionally and I think the best practices Rust encourages are absolutely applicable to that space, and general application development. That those practices are also high performance in Rust is almost incidental.
I'd like to introduce you to resource leaking and the Android activity/context lifecycle...
Just because you don't have to think about them doesn't mean that they aren't concepts that you should still be applying in GC'd languages. You'll just get bit by them when you least expect it or your load crosses some invisible threshold.
That said there's a valid point in that the large majority of software doesn't push the performance/memory threshold enough to make it a primary concern.
With GC'd languages you say "I don't care" to the lifecycle question which can(and will in any moderately complex software) come back to bite you.
GC doesn't guarantee you won't have resource leaks (of which memory is one). It guarantees that when you no longer reference an object it will be freed and that while you have a reference its valid. That's the lifetime problem. The Rust docs call it the same thing for that matter...
I've found that in GC'ed languages (like Python or JavaScript) not taking care of "lifetime", easily results in logic bugs. Better than crashes* - but still problematic.
* Maybe not always better either. Not crashing may lead to silent data corruption. And obvious problems (like crash) tend to be fixed faster than subtle ones, in practice.
It seems a bit like pointers, higher level languages will hide these details from you but you still have to know what they are and how they work, in addtion you have to know how the garbage collector works (that's a big complexity cost right there). In the same way that c forces you to be aware of pointers the borrow checker forces you to be aware of ownership.
I think it's uncontroversial to say that Rust has a tougher learning curve. But once you're past that, I think the jury is still out.
If you operated a nuclear reactor with this mentality, I'd be concerned. If you're operating acme.org with that mindset... godspeed?
In short, I agree with you.
Go definitely performs better, buy I think it'd disingenuous to claim that Go has better tooling.
What Python does have is a far richer third party library base.
I assumed this is what you were talking about. i.e. Ipython, Jupyter, pdb, etc.
That's great until you try to do some real world things with it and run into endless impedance mismatches.
Rust isn't quite so far up its own butt, however the developer community doesn't seem to realize that there's a massive gap between the toy example programs in their documentation and the level of knowledge needed to build something worthwhile.
I don't think Servo and rustc itself are "toy example programs".
I've been through the Rust Documentation (doc.rust-lang.org) and Rust By Example (rustbyexample.com) several times now, and I have to jump on IRC to get answers to (what I think should be) some pretty simple questions.
fn simplified_example<T>(a: T, b: T) -> T
where T: Copy + PartialOrd
{
if a.abs() < b.abs() {
// some irrelevant math goes here
} else {
// slightly different math goes here
}
}
I'd like to be able to call this generic function on anything that has a .abs() method. That includes i8, i16, i32, i64, f32, f64, and so on. Is that a shortcoming in documentation or an unfortunate consequence of Abs not being a trait? Several people on IRC recommended the rust-num/num crate, but that has several other deficiencies, and I don't think I should need another crate (unwanted dependency) to call .abs() on f32 or f64. With help, I came up with a workaround, but this was harder than I think it should be.The grandparent poster said:
>> the developer community doesn't seem to realize that there's a massive gap between the toy example programs in their documentation and the level of knowledge needed to build something worthwhile.
I have to agree with him. The level of Rust-specific knowledge required to get the absolute value of a number/type in a generic function is pretty significant. The example above is just a small part of a library I'm working on and only one example.
I'm clearly not an expert in Rust, and as such I realize my opinion isn't worth much, but it seems to me any attempt to build a numerical hierarchy will result in something fragile. Scheme has a well thought out minimal hierarchy, and even without all the complexity from all the fixed sizes I don't like theirs either. However, it's alright because they have dynamic typing as a release valve. C++ templates dodge the problem because they act more like duck typing, and I think I understand why Rust won't go that way.
Instead, I think you should abandon the goal of trying to create a hierarchy and just treat each small concept as a trait. Rather than creating Signed for things with a sign, and Float for things that are floating point and so on as in the num crate, create Abs as a small concept in its own. Then anything on which you can call .abs() implements the Abs trait. This includes things which aren't "signed", such as unsigned numbers and complex numbers - all of those have a useful (even if sometimes trivial) definition of .abs()
Trying to build a hierarchy reminds me of the stereotypical examples of bad object oriented design - I'm sure you know the cliches where Human derives from Ape, then Mammal, then Animal, etc... A hierarchy is like trying to build a directory tree, but numeric traits are more like labels on your gmail messages. Traits seem more like mixins or interfaces.
You'll end up with a lot of one or two method traits, but you'll also allow people to implement those traits for their own types and implement generic functions over those traits. The Float trait is a really good example of why categorization/hierarchy is unlikely to work well - That trait requires methods like .min_value() which don't make sense for complex numbers, but it is also the interface for .sqrt(), .exp(), and .ln() which have very good definitions for complex numbers. The Float trait in the num crate does not allow me to make generic functions over f32 and Complex<f64> if I want to call .exp().
> For just abs it should only be a few lines of code to make your own
Yeah, the IRC people helped me find a different workaround, but I agree that I'll just make my own trait instead. However, the .abs() methods already exist on all of the signed built in types and so I'll probably call mine .mag() on a Mag trait to avoid confusion (I'm not sure if there would be a collision/compilation error if I used the same names?). This also allows me to implement Mag for unsigned types which is useful for generic code (for examples L1 norms of vectors).
Btw, the docs for i64 have some cut-and-paste errors where they indicate i32 but almost certainly meant i64.
The examples are cut and paste errors in a sense; as it says at the top of the page, the examples are going to often use the wrong number type, because all that code is generated by macros, so for now all the number docs have to use the same type. It's a bug that needs to be fixed, but it's not a trivial one.
Yes, there are a lot of common math functions, but why is that bad? The alternatives are worse.
> The examples are cut and paste errors in a sense; as it says at the top of the page, [...]
I understand, sorry about that. I didn't see the comment at the top of the page, and I wasn't sure you knew.
Also, no worries about the bug! I much prefer over-reporting than under-reporting when it comes to these things.
I'll think about it, but honestly, that sounds super unpleasant. I've seen the bike shedding that happens in those kinds of discussions, and I think it would be difficult to convince any decision makers that there is even a problem if they haven't stumbled upon it already. I don't think many of them do numerical computing, and the language and libraries already scratch their non-numerical itches.
There's a _lot_ of ways you can go in a general purpose programming language; I don't think it's something that "nobody has hit", they just use the workaround of making their own trait(s) and it hasn't been enough of a pain to try and spearhead a change.