Hacker News new | ask | show | jobs
by dgellow 1579 days ago
I often see people complaining how bloated or large the language is, but I never see specifics. As someone who started learning and using Rust at the beginning of this year I was surprised to find out how simple the language is given its reputation on forums. What makes you feel the language is becoming too large, where is the feature creep, and where is the complexity you're talking about? Are you talking about the stdlib or language features?
3 comments

Some people (including me) felt how async was added was worrying.

Adding ".await" means when I teach Rust we now need to say "x.y is member access. Unless y is 'await'. Then it's something totally different". That's the type of strange rule that C++ has, and if you gather enough of them, languages become very hard to teach.

However, async seems to be the only big language feature which effected many things which has been added.

.await is a postfix keyword - it effectively means "crazy control-flow (monadic) magic is happening here". "?" for failure handling is another bit of postfix syntax, that means something very similar. A good IDE will show it with a different highlight, so that it will never be confused for a struct member or method call.

This stuff was discussed by the community for a long time - bikeshedding about syntax is a well-known "trap" in programming language design. It has turned out to be (IMHO) reasonably intuitive, and preferable to the alternatives.

My biggest problem with await is it's very non-obvious if you don't know to expect.

Seeing a '?' is obviously a "new thing", so people go look up what it is (in practice, I would expect any Rust programmer to learn '?' really early, but if they somehow miss learning it, they will know to go look it up).

I've seen beginners assume they were "missing the await member". Of course, once you know it's something new you can learn it. I would have prefered something like .!await, just to make clear it's a "new thing".

Isn't "?" just a sugar for something like the following? That's how I think about it in my mind at least.

  // with "?"
  let v = x?;

  // without "?"
  let v = match x {
    Ok(o) => Ok(o),
    Err(e) => return Err(e), // exit current scope due to the return
  }.unwrap();
That feels like something that could be done with a macro.
It's more like

  let v = match x {
    Ok(o) => o,
    Err(e) => return Err(e.into()),
  };
> That feels like something that could be done with a macro.

Indeed. It used to be a macro called try!.

Ah, thanks for the correction, that's better yes
Yes, that's control flow magic. It was prototyped with a macro, but failure handling comes up all the time in practical programming, and it's important to make it as ergonomic as possible.
I see. I think reading your initial comment I took issue with the hyperbole "crazy control-flow (monadic) magic is happening here".

But yes it is a hidden control flow, and taking in account hidden control flows is inherently more complicated (or at least has a steeper learning curve).

That's a fair point. I haven't touched async yet but I've seen lot of discussions regarding "await" and that seems to create a good amount of confusion. Still we are multiple magnitude away from the level of complexity C++ has.
Yeah, the await syntax is just bizarre. One of the worst eyesores of any languages I've personally encountered.

An await macro or function would have sufficed but because Rust is still mainly a hipster language with a hipster community, they had to choose a hipster syntax after years of bikeshedding about how to make it absolutely perfect.

Prefix syntax has drawbacks that were constantly pointed out in that long discussion. In particular, it doesn't compose well in practical scenarios. .await just 'flows' better and in a more intuituve way.
Completely agree. I've used it at work now for about a year and the postfix notation flows perfectly with more intricate usages of futures. Especially with the sense that you construct one, then decide when to await it, or await a collection or whatever. Following the rust style of immutable variables and chaining function calls.

You can even await the same future multiple times in a select statement keeping it executing a bit further each time until it returns, if you do it by mutable reference. Really quite magic.

Now we just need async closures and some nice async style iterators. Something like FuturesOrdered or even FuturesUnordered in an inline iterator style allowing efficient nice composability. Without any of the pitfalls and gotchas those two currently have.

I think that might be what throws many people off? The regular style of method chaining and having the cold futures just be a dead piece of memory you can pass around until actually passed to the runtime?

> An await macro or function would have sufficed

We tried, and it literally did not suffice.

Works in other languages fine.
Different languages have different semantics. The challenges here are related to the ways Rust works specifically, as well as its goals.
Complexity is a death by a thousand cuts issue. I suspect most people complaining are going to have a hard time enumerating the many small sources of complexity they encountered.
Rust isn't a complex language by the standards of languages like C++. Complexity arises by features that work together in surprising ways. Rust has a medium-large amount of features (similar to Python (though obviously with a more imposing learning curve than Python)), but those features tend to compose very well, which keeps complexity from ballooning.
It is true that Rust is competing most directly with C++, but "less complex than C++" might be a true statement about every single language in existence that has more than a dozen professional users.
Kind of, Python, C#, Java have enough stuff on language + standard library to compete with C++.

A pub quizz with them would be just as fun.

I personally think those are all nowhere close. There are like ten different ways to initialize values in C++ and they all have slight differences.
Believe me, C# 10, Java 17 and Python 3.7, including standard library and runtimes across 25 - 30 years, have plenty of question material.
Rust doesn't reuse concepts well. The different capabilities and subsystems are all build from disjoint building blocks, which you could call modular in a sense, but it doesn't make them easy to use, learn, or joyful.

The module system, the macro system(s), the core language, the borrow checker. It's all different micro-languages without a shared foundation.

I think it's easier to when you compare Rust to simpler languages like Carp or Zig.

Carp due to its homoiconicity and lisp heritage allows you to write macros in regular old Carp. Zig doesn't really have Macros, but comptime (compile time) code evaluation, which results in something with a lot of the benefits of a macro system without the drawback of unreadable DSLs, because it too is plain old Zig annotated with `comptime`. Zigs module system is build on Struct name-spacing, so if you know Structs, you know the module system. Its build system is also build on Zig code.

My guess is that Rusts origins are partially to be blamed for this, the web is build the same way, disjoint standards that are developed by different groups of people. It's just that just like the web, the sum of the parts is less elegant and usable than each constituent.

Serious question about Zig’s comptime - isn’t that just like Rust’s const fn, except every function is implicitly opted in to being const?

Where this could be a problem - I write a comptime function, only using other functions that I’ve verified can be executed at compile time. But now the implementation details of those functions (that they’re comptime) has leaked to their definition. Now a change to the impl of those functions could break my code, without the authors of those functions realising it.

Perhaps this is not a problem in practice?

Kinda, but much more than that and the ergonomics are quite different as the entire language is basically available. For example in Zigs comptime, types are first class citizens which can be queried and operated on. This gives you generics without a lot of type system magic, and even const generics over arbitrary types. All without a macro system or Turing-Complete type checking (similar in flavour to the DeBruijn criterion: the comptime code/higher order proof system is turing complete but the type/core checking is straight-forward).

And yes the problem you describe could exists, but I've not encountered anything like it in practice. But the idea you had there captures the difference between Zigs and Rusts type-system/generics quite nicely:

> Rust tries to proof universally qualified correctness and behaviour of code, i.e. regardless of its context, while zig only proofs you the correctness of specific instantiations.

While this may sound like a bug, I'd argue that it's a feature, as it's YAGNI applied at a type level, and for example allows you to do things like:

  switch (builtin.target.cpu.arch) {
    .x86_64 => //do intel stuff
    .aarch64 => //do arm stuff
    else => @panic("unknown arch!");
  }

With the switch arms that don't apply simply being ignored and never type checked. This allows you to use a basic language construct where C requires a preprocessor and Rust requires macros or compiler magic.
The Rust solution for your arch variation specifically is hardly onerous, and does just the same, ignoring the code in not-matching branches:

  #[cfg(target_arch = "x86_64")]
  …

  #[cfg(target_arch = "aarch64")]
  …

  #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
  compile_error!("unknown arch!");
(If you got to much more complex cfg branches, cfg-if could be worthwhile, which lets you write `cfg_if! { if #[cfg(…)] { … } else if #[cfg(…)] { … } else { … } }`. But for only a few simple ones, I prefer to keep it out.)
Yes, it can happen. In practice is not a big deal because the kind of stuff that you would want to run at comptime is pretty much all logic that manipulates its inputs without other side-effects, but one could for example add logging to a function and break its ability to be run at comptime (Zig might add in the future ways of solving this problem).

I would say that more in general the problem between public interface vs implementation details is much bigger than any type system and requires humans to negotiate what should be considered part of the public interface through other means (eg: tests, comments). Comptime is one example, another is ABI stability (eg the layout of a struct, or just its size), another could be speed or memory consumption.

Zig doesn't have an official package manager yet, we'll see what happens once we get one and people in the community have to start communicating stability guarantees to their users.