|
> What happened to my bicycle for the mind? The bicycle you pick depends on the problem you're trying to solve. You can divide up all programming languages into two categories: 1. Those that allow you to state your problem in terms of ideas. You write your code, the compiler or interpreter does some optimisations, and the end result runs fast enough for your use case. 2. Those that allow you to state your problem in terms of machine instructions. You demand a certain level of performance, or want every memory allocation to be explicit. The compiler or interpreter still does some optimisations, but instead of inserting more instructions implicitly if it has to, you'd rather your program straight-up fail to build. The choices made by a language's designers will make it seem unnecessarily complicated if you're looking at a (2) language when you have a (1) problem, and will seem like it's making too many assumptions for you if you're looking at a (1) language with a (2) problem. For example, let's go back to string concatenation, which, as you saw in Rust, looks like this: format!("{}{}", x, y)
And, yes, in other languages it looks like one of these two things: x + y
concat(x, y)
If all you want is to end up with a "String" that's made up of those two other strings in it, this is good enough. I've done this bajillions of times over the course of my career. (Truly, I am a string-concatenating expert.)But a String in Rust, under the hood, contains a pointer to a heap-allocated buffer. And getting one of those requires a memory allocation, and Rust makes those explicit. So the method of concatenating two strings you choose depends on what machine instructions you pick: • If you don't really care, or if you need a new heap-allocated buffer, then `format!("{}{}", x, y)` will give you what you want. • If `x` is already a heap-allocated String, and you know for a fact that you aren't going to need to use it again, then you can re-use the existing buffer with simply `x + y` (or something like `x.push_str(y)`). • If `y` is a heap-allocated String but `x` isn't, you can re-use that existing buffer with `y.insert_str(0, x)` (but this requires it to copy the bytes in the buffer to make room for the string you're inserting). • If neither is a heap-allocated String, and you don't want to allocate any more memory, you'll have to skip concatenation altogether and do something else (for example, if you're just writing the concatenated string somewhere, and don't need a buffer to put it in, then you can do `write!(somewhere, "{}{}", x, y)` which won't allocate). > Surely the computer should be doing the heavy lifting? I find that the Rust compiler does an excellent job of doing the heavy lifting for me: I don't have to worry about iterator invalidation or use-after-free, surprise memory allocations or numeric type conversions, or unexpected performance drops when I make a small change. But this is all because I'm expecting a type (2) language, rather than a type (1) language. It's the last part I'd like to focus on, because you may have read the above list and thought "a compiler could choose the appropriate optimisation for me!". And, well, it probably could! After all, if the only difference between the first option `format!("{}{}", x, y)` and the second option `x + y` is whether the string `x` gets used after concatenation or not, a compiler could certainly check whether this happens and optimise out the allocation if necessary. But then you need to worry about keeping this optimisation when the code changes. Sticking with the example, suppose you have code like this in some hypothetical (1) language that allows you to express ideas but optimises them the best it can, where `x` and `y` are Strings still: var frobozz = x + y;
And then someone on your team makes this change: var frobozz = x + y;
/* skip a couple dozen lines of code */
var gunther = x + z;
A code change that starts using `x` for a second time now results in an allocation being introduced a couple dozen lines above from where the code change was, because it can now no longer be optimised out. If this is still good enough for you, because you're solving a (1) problem, then there's nothing wrong with this. But if you're solving a (2) problem, then the language has kind of... let you down.So don't be sad! It just depends on what problem you want the language to solve for you. |