Hacker News new | ask | show | jobs
by option_greek 2000 days ago
I wish they would streamline the string handling capabilities. Currently the conversions between String and &str are really ugly to look at. In general the conversion between types don't seem all that great with developers having to either use crates or write their own code. They need to take more inspiration from C# which continues to be the benchmark of elegance (obviously personal opinion). That said, rust is still better than C++ - namely its package management is great. I just wish the compiler doesn't keep second guessing me :D
2 comments

> the conversions between String and &str are really ugly to look at.

This is entirely up to you; unless you find method calls ugly, in which case, you've got bigger problems :)

> They need to take more inspiration from C#

Could you elaborate a bit? I'm not familiar with what C# does here.

What I tend to stumble over is how String and &str each have some methods and features that the other lacks, e.g. concatenation. Which of course makes sense with basic understanding of string slices, but sometimes I can't shake the weird feeling that I'm cloning some String unnecessarily.

And I just wish generic type parameters wouldn't have to be propagated through all types touching them.

That said I'm really happy with how it develops and Rust comes with many little details that I sorely miss in other languages. Thanks for all the effort.

The methods on `String` should be a strict superset of the methods on `str` because `String` dereferences to `str`, thus `String` gets all of the `str` methods for free.

If you're familiar with say, `Vec<u8>` and `[u8]`, `String` and `str` are basically the same except they are contractually valid UTF bytes. So just like you can `push` and `extend` to a `Vec`, you can do the same to a `String`.

With regard to generic type parameters, if you want to code in Java or Go style, you can use dynamic dispatch trait objects to remove type parameters.

> The methods on `String` should be a strict superset of the methods on `str` because `String` dereferences to `str`

I've found that sometimes this doesn't happen automatically (at least when passing as an argument; maybe not when calling methods). i.e., you have to explicitly call .as_str() in some situations. Even as someone who's comfortable with the String/&str distinction and moderately familiar with Rust, it's not clear to me where this is and isn't necessary. The compiler just tells me when I make the wrong guess.

Maybe read a bit on Deref: https://doc.rust-lang.org/std/ops/trait.Deref.html

Any time you have a &String reference, it triggers coercion to &str.

Unfortunately that is not always the case. This fails to compile, for example (and is fairly irritating):

    let my_str = "Hello".to_owned();
    match &my_str {
        "Hello" => (),
        _ => ()
    }
In C# there is a helper class: https://docs.microsoft.com/en-us/dotnet/api/system.convert?v...

I'm still figuring my way around rust so obviously some noob questions follow: -> what's with the move/copy mess ? I know why they are needed but it seem to be in the face with all the explicit '&' all over the place in any reasonably sized code. Why not hide it a bit by letting the implicit copy to happen to simpler structures ? (at compile time).

-> Why no love for inheritance? :) - it makes certain patterns easier to implement

-> Why no love for global/static variables ? I know they are prone to be misused but some patterns like singleton really need a lot of shortcuts to implement. And there will always be some cases where you want to keep variables with static and global scope

You are essentially complaining that Rust is not C#, while at the same time admitting that you don't know much about the language.

Rust is much lower level and makes very different tradeoffs. Sometimes for the sake of performance, sometimes to enhance code readability.

But most of the design decisions are there for a reason, and are good choices.

Simple types (that are small and can be trivially memcopied) can implement the `Copy` trait, which makes cloning transparent. For other types, the `Clone` trait is there with `.clone()`. Having expensive copies be explicit is a intentional design decision.

For value conversions, the `Into/From` and `TryInto/TryFrom` traits make conversions a (usually type inferred) function call (.into(), .try_into()), which is really quite convenient, though at the expense of readability.

Regarding strings: they are are definitely complicated and sometimes awkward in Rust. But I'd argue that strings are inherently complicated. Most languages hide this complexity by just allocating and doing everything on the heap, which is not great in a language that values performance and wants to support environments without allocators.

Expanding a bit on conversions: C#'s `Convert` conflates several different operations.

Examples:

Convert.ToInt32(String) – This is _parsing_. In Rust, use `parse`.

Convert.ToString(Int32) – This is _stringifying_. In Rust, use `to_string`.

Convert.ToInt64(Int32) – This is an _infallible conversion_. In Rust, use `into`.

Convert.ToInt32(Int64) – This is a _fallible conversion_. In Rust, use `try_into`.

In all these cases, Rust gives you more immediate semantic information about the conversion, and in fewer characters too!

> Why not hide it a bit by letting the implicit copy to happen to simpler structures.

This is already the case. Built-in types that are simple enough to be copied implicitly already are (roughly: those which don't manage any memory or other resources), and you can enable this for your own types with `#[derive(Copy)]`, as long as they are composed only of implicitly copyable types.

    #[derive(Copy)]
    struct S {
        x: i32,
        y: usize,
        z: Option<Result<(), ()>>,
    }

    fn f(x: S) {
        // ...
    }

    fn main() {
        let s = S { x: 0, y: 0, z: Some(Ok(())) };
        f(s);
        f(s);
    }

Something like `String` isn't implicitly copyable in Rust, because it manages memory, and therefore copying it would require a heap allocation.

The Rust way of forcing non-trivial clones to be explicit is much better than C++ IMO, where someone forgetting a `&` or an `std::move` somewhere can cause an innocuous-looking function call to be arbitrarily slow.

In C# there are not implicit copies either (except of value types), because more complex types in C# are accessed via pointers to garbage-collected heap objects. Rust doesn't have a garbage collector, though.

The misunderstanding may come down to the fact that strings are "primitives" in many languages - for usability reasons - despite carrying the memory/performance traits of a full, heap-allocated "object". If someone has never worked in a language where strings are not primitives, I can see how they might be irked/confused by suddenly having to deal with that.
> Why no love for global/static variables? I know they are prone to be misused

Rust doesn't deal with "prone to misuse" but "provably safe, else explicitly unsafe, or made safe by the programmer through wrapping code." Global variables are inherently unsafe and need to be wrapped as such.

Of course you'll need global state at some point in your life. What Rust does is yell to remind you that it isn't thread safe.

If you stop thinking about a Rust program as a script where the runtime figures the hard stuff out for you, then these design decisions make a lot more sense. You have a lot more knobs to twiddle than in C#, no garbage collector, and no runtime. The compiler is pretty smart, but it tells you the things it knows and you're responsible for working around the limitations to create sound programs that the compiler can optimize.

Rust has From/Into and TryFrom/TryInto that do the same thing, as far as I can tell. It's not clear to me what the differences are, maybe someone else in this thread will know. :)

> Why not hide it a bit by letting the implicit copy to happen to simpler structures ?

This is the Copy trait.

> Why no love for inheritance

There are a variety of reasons, but one interesting one is that inheritance and strong type inference have issues, and we have very strong type inference. Beyond that, there are various other reasons, but what it really comes down to is that there's just not a ton of pressure to actually implement it; it's not enough of an impediment for Rust users to justify adding it. Most requests come from people who do not write Rust, and once people get into Rust and how it works, they don't seem to need it much anymore.

This is of course very general and there are some people who love it and want it badly anyway, but "some people exist who want this feature" is not enough to make it happen. Rust already has a lot of features, and some people say too many. We have to be careful here.

> Why no love for global/static variables ?

What does "love" mean? Rust absolutely supports these.

Sorry for got to mention the 'mutable' part. So a mutable singleton variable that has to be marked with an unsafe keyword (and forces the functions that use to be marked as unsafe). The other day I was struggling to implement a singleton (for loading large data from disk) and finally decided to just pass it down the whole chain from the main method. May be I'm missing some easy pz way of doing this ?
So yes, that is because it is unsafe, according to Rust's definition of safety. However, there are tools you can use to remove this.

First of all, I find that many people aren't using multiple threads, and therefore, what they want is a thread-local, not a static. For that, you can use https://doc.rust-lang.org/stable/std/macro.thread_local.html

If you do need a static, then there are libraries like https://docs.rs/lazy_static/1.4.0/lazy_static/ and https://crates.io/crates/once_cell to help too. We have talked about adding something like this to the standard library, but it hasn't landed yet. https://github.com/rust-lang/rust/issues/74465 is the tracking issue for when it does. The reason that we don't have this yet is that it is not super urgent, given that the libraries already exist.

Now, both of those give you immutable statics, but that's where interior mutability comes in. As you can see from the thread_local examples, you can use a RefCell there, and if your type is simple enough, maybe even regular Cell. For lazy_static or once_cell, you may want to use Mutex<T>, or RwLock<T>, or some other type. For simple integers, the various Atomic* types might be better, for example. The reason this isn't built in is exactly because there are so many options; people need all of these specifics, so we have to provide them, so there's no single built-in thing.

Here's how you can do global mutable state in Rust:

  #[macro_use]
  extern crate lazy_static;

  use std::sync::Mutex;

  lazy_static! {
    static ref ARRAY: Mutex<Vec<u8>> = Mutex::new(vec![]);
  }

  fn do_a_call() {
    ARRAY.lock().unwrap().push(1);
  }

  fn main() {
    do_a_call();
    do_a_call();
    do_a_call();

    println!("called {}", ARRAY.lock().unwrap().len());
  }
I agree that this isn't the most ergonomic, but like most unergonomic things in Rust, there are reasons for it being so.
(You'd probably replace the first two lines with "use lazy_static::lazy_static;" in today's Rust, that older style isn't as idiomatic.)
> 'm still figuring my way around rust so obviously some noob questions follow: -> what's with the move/copy mess ? I know why they are needed but it seem to be in the face with all the explicit '&' all over the place in any reasonably sized code. Why not hide it a bit by letting the implicit copy to happen to simpler structures ? (at compile time).

So there's a lot to unpack here, but implicit copies do happen - if it's a copy-able data structure. Strings are huge, so no implicit copy. As far as `&` being too much to identify references vs not, i'm not sure how it could be made any shorter to identify a reference. You're already, commonly, hiding the lifetime associated with that, so it's really `&'a str`, but Rust lets you drop the `'a` most of the time.

Considerable effort has been put into easing the language and lowering syntax. What's left is essentials imo. I _want_ to see when something is a reference. I _want_ to know generic types. etcetc.

> -> Why no love for inheritance? :) - it makes certain patterns easier to implement

I can't speak much here. I've been using Go and Rust for so long i've forgotten what classical inheritance is actually useful for haha. The Go/Rust pattern of Structs, shared behavior, etc cover all use cases for me. i don't find myself missing something, fwiw, but i can't speak to which is "best".

> -> Why no love for global/static variables ? I know they are prone to be misused but some patterns like singleton really need a lot of shortcuts to implement. And there will always be some cases where you want to keep variables with static and global scope

You can have global variables fwiw. Granted, i use `lazy_static` which simplifies it a bit, but there's nothing i'm aware of which prevents this pattern. I've typically just used it in tests though. Globals are the devils candy ;)

If you just prefer the "helper" style of calling, Rust allows methods to be called either way:

  let foo1: String = "bar".into();
  let foo2 = core::convert::Into::into::<String>("bar");
I'm not sure what you mean by conversion between String and &str? To get an owned String from a &str you just use the `into` method, no?
Strings implement a lot of different conversions, since they're very general. You've got:

* Into, with s.into()

* An inherent method, .as_str()

* Deref coercion, &s

* Reborrowing, &*s (this builds on Deref too but isn't a coercion and can be done in places where coercion doesn't kick in)

... and probably some others I'm forgetting.

Right but as a general rule you'll mostly only be using `s.into()` to get a `String` from an `&str`. Or `&s` to deref `String` to a `&str`. I'm not sure why this would require a crate to handle?

The other ways are more "advanced", for when you're dealing with (for example) potentially unsafe coercions or you don't want to rely on inference for some reason.

I agree with you that I'm not sure what your parent is talking about, I'm just here to give all the examples. Deref coercion takes 99% of my String -> &str conversions, and I reborrow for that rare 1%, personally.
Yeah sorry, I'm just really confused about what's being asked for.
-> specifically this one: "Reborrowing, &*s"

Would have been easier to implement with a copy constructor I guess. Why not implicitly clone in some cases (since classes like String gets used so frequently).

Well, Rust doesn't have constructors, let alone copy constructors. Clone goes from &T -> T, so that is the exact opposite conversion needed here, let alone auto-clone.

Automatically copying strings may not be a great idea: https://news.ycombinator.com/item?id=8704318

> Why not implicitly clone in some cases (since classes like String gets used so frequently)

Because they get used so frequently. Why would we want to add expensive clones to frequently used operations?

This would be like asking why LINQ methods in C# aren't deferred by default. Yes, there's a few situations where that would be nice but it would make the functions largely useless because of how poor performance would be.