News for hackers. Stuff that matters.
Re:Thoughts on Rust, a few thousand lines in by steveklabnik on Wednesday January 09, 2019 @04:52PM (#18869324)
Every string is checked. But UTF8 is a multi-byte encoding, and slicing works per bytes, so you if you slice in the middle of a multi-byte character, you may get nonsense. The error happens because of this checking, not in spite of it.

String always assumes full UTF-8. You could make an AsciiString type if you wanted, but it's not provided by the standard library.

Re:Thoughts on Rust, a few thousand lines in by da_chicken on Wednesday January 09, 2019 @05:56PM (#18869880)
The obvious follow up question would be: so why is slicing a string a byte-wise operation and not a character-wise operation? If a string is an array of characters, why does it let me refer to individual bytes without explicitly casting it to a byte array? How often comparatively do you want the nth byte compared to the nth character? I would suspect that's pretty rare.
Re:Thoughts on Rust, a few thousand lines in by burntsushi on Wednesday January 09, 2019 @06:08PM (#18869954)
> How often comparatively do you want the nth byte compared to the nth character? I would suspect that's pretty rare.

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

Re:Thoughts on Rust, a few thousand lines in by int_19h on Wednesday January 09, 2019 @07:27PM (#18870457)
I still think that using the common [] operator for this is a mistake. Strings shouldn't offer [] at all, and instead should provide methods like codepoints(), bytes(), grapheme_clusters() etc for indexing, slicing, and iterating.

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.

Re:Thoughts on Rust, a few thousand lines in by burntsushi on Wednesday January 09, 2019 @08:15PM (#18870682)
I'm quite thankful that Rust has succinct notation for slicing strings. Do note that `string[n]` is not supported, so you'll stumble over an inconsistency in your mental model quite quickly if you think slicing is by codepoint.
Re:Thoughts on Rust, a few thousand lines in by int_19h on Wednesday January 09, 2019 @11:38PM (#18871565)
The lack of direct indexing is a good point. But strings aren't sliced on byte boundaries all that often either - it's far more common to use higher-level APIs like split(), that deal with offsets under the hood, so that sugar mostly ends up being used in the implementation of such APIs. And, really, would something like s.slice_u8(x, y) be that unwieldy over s[x..y]?
Re:Thoughts on Rust, a few thousand lines in by TheCoelacanth on Wednesday January 09, 2019 @09:58PM (#18871159)
How often do you actually want the nth character as opposed to the nth grapheme?

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.

Re:Thoughts on Rust, a few thousand lines in by steveklabnik on Wednesday January 09, 2019 @05:58PM (#18869893)
As stated below, indexing is an O(1) operation, and that is a O(n) operation.

> If a string is an array of characters

It is not, it is an array (technically vector) of bytes.

Re:Thoughts on Rust, a few thousand lines in by da_chicken on Wednesday January 09, 2019 @09:03PM (#18870919)
Who cares if it's O(1) if it causes a panic? What good is high performance if it doesn't complete or isn't safe?

At the very least, shouldn't there be an O(n) method to do character-wise slicing?

Re:Thoughts on Rust, a few thousand lines in by steveklabnik on Wednesday January 09, 2019 @09:09PM (#18870946)
panics are safe. You expect the “I don’t have a bug” case to be fast.

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

Re:Thoughts on Rust, a few thousand lines in by yurymik on Wednesday January 09, 2019 @04:57PM (#18869373)
Why not this?

  fn main() {
      let a = "ab早".as_bytes();
      let a = &a[..3];
      println!("Hello, world!");
  }
Re:Thoughts on Rust, a few thousand lines in by steveklabnik on Wednesday January 09, 2019 @05:00PM (#18869396)
It's not clear to me what you're suggesting; is it that String shouldn't have supported indexing in the first place? That code does work, but you have a &[u8] not a &str.
Re:Thoughts on Rust, a few thousand lines in by yurymik on Wednesday January 09, 2019 @05:15PM (#18869538)
But neither AsciiString. It has a as_str method, but it's still a kludge.

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.

Re:Thoughts on Rust, a few thousand lines in by Someone on Wednesday January 09, 2019 @05:38PM (#18869738)
I think the rust designers want to keep the implicit contract that indexing into a string is fast and O(1).

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.

Re:Thoughts on Rust, a few thousand lines in by steveklabnik on Wednesday January 09, 2019 @05:28PM (#18869654)
> What I don't understand is why slices are indexed in bytes and not in objects.

Slicing is an O(1) operation, and that would be an O(n) operation.

Re:Thoughts on Rust, a few thousand lines in by TheCoelacanth on Thursday January 10, 2019 @10:57AM (#18874750)
It does: `s.chars().take(3)`. It just does it with iterators rather than with indexes because that better communicates the performance characteristics.
Re:Thoughts on Rust, a few thousand lines in by dkarl on Wednesday January 09, 2019 @05:11PM (#18869491)
I think he's suggesting that slicing on strings should be by character, and if you want to slice on bytes, you should explicitly ask to treat the string as a byte array. It makes more sense semantically, and it's safe.
Re:Thoughts on Rust, a few thousand lines in by richardwhiuk on Wednesday January 09, 2019 @05:18PM (#18869558)
Slicing on characters is a linear time operation and indexing is meant to be cheap.
Re:Thoughts on Rust, a few thousand lines in by dkarl on Wednesday January 09, 2019 @05:30PM (#18869672)
That seems like taking it too far. It's like using pointer arithmetic to index a linked list on the assumption that the nodes happen to be allocated contiguously in memory. I mean, I guess the thinking is, indexing a Unicode string isn't cheap, but indexing strings used to be cheap once upon a time, when strings were encoded in fixed one-byte-per-character representations, so let's pretend that's still the case and panic if it doesn't work out.... That's weirdly antithetical to Rust's purported focus on safety.

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.

Re:Thoughts on Rust, a few thousand lines in by comex on Wednesday January 09, 2019 @10:33PM (#18871298)
Indexing a Unicode string is cheap... if you have a byte index. If you want to count out some fixed number of codepoints, then of course you've just moved the cost to calculating the corresponding byte index. But counting codepoints is almost always the wrong thing to do anyway [1]. In practice, it's more common to obtain indices by inspecting the string itself, e.g. searching for a substring or regex match. In that case, it's faster for the search to just return a byte index; there's no benefit to having it return a codepoint index, and then having to do an O(n) lookup when you try to use the index. And byte indices obtained that way will always be valid character boundaries, so you can use [] without worrying about panics.

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

Re:Thoughts on Rust, a few thousand lines in by empath75 on Wednesday January 09, 2019 @07:15PM (#18870403)
Safety is about memory safety. Immediately exiting your program is about as memory safe as it gets.
Re:Thoughts on Rust, a few thousand lines in by Retra on Wednesday January 09, 2019 @06:42PM (#18870185)
Panics are not unsafe. Panic exists in Rust because they are safe. If you don't want a panic on index, just don't index.

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.

Re:Thoughts on Rust, a few thousand lines in by dkarl on Thursday January 10, 2019 @01:00AM (#18871857)
Indexing into a UTF-8 string doesn't serve any reasonable consistent purpose anyway

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.

Re:Thoughts on Rust, a few thousand lines in by Retra on Thursday January 10, 2019 @09:34AM (#18874060)
>If that's true, isn't it the job of a type system to help avoid such nonsensical operations?

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.

Re:Thoughts on Rust, a few thousand lines in by int_19h on Wednesday January 09, 2019 @07:28PM (#18870467)
I think that's too extreme. There are many legitimate reasons to slice non-ASCII text - for example, to split it on newlines.
Re:Thoughts on Rust, a few thousand lines in by mlindner on Wednesday January 09, 2019 @08:45PM (#18870832)
That's not trivial and different languages vary in how they handle new line characters even. https://stackoverflow.com/questions/44995851/how-do-i-check-...
Re:Thoughts on Rust, a few thousand lines in by int_19h on Wednesday January 09, 2019 @11:32PM (#18871541)
You can still split non-ASCII text on ASCII newlines, and quite often that's exactly what needs to be done.
Re:Thoughts on Rust, a few thousand lines in by Retra on Thursday January 10, 2019 @09:34AM (#18874062)
And usually, you don't want it to cost O(n) on top of whatever parser you ran to find those newlines.