I would prefer it if this feature didn't exist at all rather than cause runtime panics.
https://play.rust-lang.org/?gist=e02ce5e9aacfee3a2b4917d5624...
I would prefer it if this feature didn't exist at all rather than cause runtime panics.
https://play.rust-lang.org/?gist=e02ce5e9aacfee3a2b4917d5624...
It's not useful to blindly read at random offsets in UTF-8 strings. If it didn't panic, you'd get garbage. If offsets were automatically moved to skip over garbage, you wouldn't know what you're getting, and your overall algorithm would likely end up with nonsense (duplicated or skipped chars).
For algorithms that don't care about characters or UTF-8 validity, there's zero-cost `.as_bytes()`.
And in the rare cases, when it's not a bug, then one can just use `as_bytes` which would be good to do in any case, to indicate to other humans that this is not a bug.
B.t.w. I love the error message `[..3]` generates: "thread 'main' panicked at 'byte index 3 is not a char boundary; it is inside '早' (bytes 2..5) of `ab早`'" — I've never seen such easy to understand error messages in any language (except for in a few cases in Scala).
e.g. my_hashmap["foo"] will panic at runtime if the key "foo" is not present, or return the associated value if it is. But my_hashmap.get("foo") will return None if "foo" is not present and Some(value) if it is.
First of all, panics are perfectly safe. None of this has to do with safety guarantees.
Second, the [] syntax is controlled by the Index trait, which returns an &T, not an Option<&T>. It does this due to Rust's error handling philosophy. There's two kinds of errors: recoverable and unrecoverable errors. When something shouldn't fail, unless there's a bug, you shouldn't be using Option/Result, you should panic. When something may normally fail, and you want to be able to handle that explicitly, you should use Option/Result.
If [] always returned an Option, you'd be seeing tons and tons and tons of unwraps. It's not the right default here. However, that's why the .get method also exists: If you do think that this may fail, but not due to a bug, then you should use .get instead, which does give you an option.
TL;DR: everything is tradeoffs, and we picked a specific set of them, and that's how they all play out together.
Personal commentary: this is the kind of thing that's largely concerning until you actually use the language more, IMHO. Dealing with Options all the time here would feel really bad. Consider the other sub-thread about floats; it often feels like boilerplate for no good reason. That would introduce this for every single time you want to index something, which is a very common operation.
This would also free up the [] to be used for generics and avoid syntactical warts like ::<> parsin
It doesn't remove those warts, it moves them.
If you want all the data after a : character, you slice on the index of the :. The character after it is going to be the beginning of a UTF-8 character.
You do not under any circumstances guess that the colon is at position 6 in the string. That's not safe. Why are you going cowboy in a language that is so obsessed with safety?
It's like the default rule in a lexer, if it ever gets to it then it's an unrecognized character and lexing stops so error handling can proceed.
--edit--
Which I now realize was probably your point.
Granted, this is certainly incorrect but has little to do with safety, especially if the downstream code has to revalidate everything anyway.
I suppose one could have it return StrWithInvalidSurrounds, where just the first (at most) 3 and last (at most) 3 bytes might be invalid, which would then allow for O(1) revalidation to a &str, and even other operations like continuing to slice... But this is even more clunky for actual use!
I think a moderately less clunky API might have been to not use integers for byte indexing, but instead some ByteIndex wrapper type that string operations return, meaning one can't just write `s[..5]` in an attempt to get the first 5 characters of the string.
(Also, there's str::get that returns an Option: https://doc.rust-lang.org/std/primitive.str.html#method.get )
String always assumes full UTF-8. You could make an AsciiString type if you wanted, but it's not provided by the standard library.
fn main() {
let a = "ab早".as_bytes();
let a = &a[..3];
println!("Hello, world!");
}Also, you can get the same performance from an operation that returns a byte array instead of a string. If that kind of performance is what you want, then a Unicode string is simply not the right type to use.
Indexing into a UTF-8 string doesn't serve any reasonable consistent purpose anyway, because it is an abstraction of text that doesn't provide support to the notion that a "character" is more fundamental than a word or paragraph, etc. Rust's string slicing exists solely to make ASCII text easy to handle. If your text is not ASCII, then you shouldn't be slicing it at all. Thus the panic.
If that's true, isn't it the job of a type system to help avoid such nonsensical operations? If "slice" only makes sense for byte arrays and ASCII strings, it could be provided on those types without being defined on UTF-8 strings.
Panics are not unsafe. Panic exists in Rust because they are safe.
That's "safe" by a very limited definition of safety. It's one step up from undefined behavior, granted, but it's not a very high standard. In practice, in most programs, you'd want to ensure that such a panic would never happen, and personally I think the language's unhelpfulness in that regard is a wart.
It's not strictly true, because there are situations where you want to slice UTF-8. For instance, if you already know where the code point boundaries are for newlines. But if you know that, then you've run something like a regex with >O(1) behavior and you certainly wouldn't want string slicing to do redundant work.
>hat's "safe" by a very limited definition of safety
That's the definition of safe that is used. Safety in the context of Rust means memory safety. (Division can panic, btw.) If you don't see why undefined behavior is so much worse than a panic, then do some research on it. If you want programs that never fail, you need a comprehensive plan that takes into account things like hardware failure. A programming language can't do that.
You suggest just using a byte array instead, but then you'd lose the guarantee that what you're working with is valid Unicode. Contrary to your assertion, it is useful to have a type that provides that guarantee, yet which can still be operated on efficiently.
[1] https://manishearth.github.io/blog/2017/01/14/stop-ascribing...
This example was basically a suggestion to throw0u1t: if they want to cut in the middle of the utf-sequence for whatever reason, they can [edit:] do it without extra crates.
What I don't understand is why slices are indexed in bytes and not in objects. If String has an ability to check that we're cutting in the middle of the character sequence, why doesn't it provide an ability to take 3 fully formed characters.
Slicing is an O(1) operation, and that would be an O(n) operation.
If you want to find the one millionth codepoint of a UTF8-encoded string, you have to more or less (1) visit every byte of the string.
If, on the other hand, you want to find the codepoint that covers the millionth byte, on the other hand, you have to read at most four bytes (read the millionth byte, and there are three cases:
- it’s a full codepoint. If so, you‘re done.
- it is the first byte of a multi-byte codepoint. If so, read forwards in the string for up to 3 continuation characters.
- it is a continuation character. If so, search backwards in the string for the first byte, then, if necessary, read forwards to find more continuation characters.
So, that is O(1)
(1) you can skip continuation characters, but these typically are rare.
> If a string is an array of characters
It is not, it is an array (technically vector) of bytes.
At the very least, shouldn't there be an O(n) method to do character-wise slicing?
You can, but it depends on what you mean by “character”, as that’s not a concept in Unicode. Every kind of thing you could mean has a method, specific to it, since they’re different things.
(char in Rust is a Unicode scalar value, and you can collect into a Vec<char> and then slice it, as an example of one of those things. And that’s still O(1) at the cost of using up to four times the memory.)
It's exactly the opposite of what you expect. Getting the nth codepoint is often (not always) semantically incorrect since a codepoint isn't necessarily one character. Multiple codepoints might combine to form one character. (In Unicode, these are called grapheme clusters.)
Byte offsets are used a ton because you might often have the index to a position in the string from some routine, like, say, a search[1].
I've been working on text related things in both Rust and Go for several years. Both languages got this part of their strings exactly right given that their representation in memory is always a sequence of bytes.
[1] - https://doc.rust-lang.org/std/primitive.str.html#method.find
The reason being that the behavior of [] for string varies widely in different languages, and so this is something that's best made explicit, both to force the author of the code to consider whether their assumptions are valid and reasonable for what they're trying to do, and to give additional context to anyone else reading the code.
As it is, I suspect a common class of bugs for Rust will be with people assuming that [] slices codepoints, because it seems to work that way for ASCII.
There is pretty much no case where indexing by character actually makes sense because it is almost always incorrect and it is always inefficient.
Indexing by byte is rarely useful, but it does have some usefulness since it can be used correctly and efficiently since you can easily find the next or previous character by searching a maximum of four bytes for the a byte that has a MSB of 0. If you want to do something like get a &str that would fit in a n-byte buffer, then byte indices will let you do that efficiently and correctly.
So it handles the (very) common case of needing the same encoding multiple times (e.g. for all file paths on Windows), while not introducing too much overhead in memory or speed.
I could be mistaken on exact details though, especially since I recall there being multiple implementations even within py3.x.
This is great for high-level code, but painful to work with from native code, because it usually needs some specific encoding to call into other libraries, and it's usually UTF-8 - so you need to re-encode all the time.
Basically, string indexing is a lot harder than it seems at first glance, depending on what you want.
[1]: https://doc.rust-lang.org/std/string/struct.String.html#meth...
IIRC that's what grapheme clusters are for.