Hacker News new | past | comments | ask | show | jobs | submit login

Tangentially, I'd love to see some list of "what does this language make easy" (C: raw memory manipulation!) and "makes hard" (C: memory-safe code)...

Does one exist for Rust?




tbh I don't know enough about rust or the whole field of software development to give an authoritative answer, but if you don't know much about rust, this might help you.

in general, rust is great for pretty much anything c is great for. you can do raw memory manipulation in unsafe blocks if you want, but you write memory safe code by default. at present, rust is definitely slower than c, but there's no inherent reason that it has to be so; mainly it's just the consequence of being a new and immature language.

one neat thing you can do with rust is build safe interfaces to the c libraries that you know and love for a relatively small performance penalty. I am writing a toy graphics application in rust, and it is so much nicer than bare OpenGL, although there are some serious pain points with library maturity.

it can also be a decent substitute for problems you might solve in C# / Java / other statically-typed languages, although it is a bit more strict and explicit than those.

the main things I can think of that rust makes hard are applications where you really don't want static typing (web dev, scripting, etc.) or you have a need to use a lot of specific libraries that you don't feel like writing interfaces for.


I will contest the idea that you don't want static typing for web dev. Curious, what statically typed languages do you have experience with? I ask because you only mention C# and Java, and I often find people who form their ideas about types from those languages think they have to be much more cumbersome than they really do.


> what statically typed languages do you have experience with?

c, c++, java, rust, c#, so the assumption you're making is probably correct.


If you're at all interested in taking the red pill: elm and reason/ocaml are some things worth checking out. There is so much more out there than the C family tree.


> rust is great for pretty much anything c is great for

No it’s not.

> you can do raw memory manipulation in unsafe blocks if you want

You can but Rust is not great at it. At least not in comparison with C or C++.

In C, people have been writing these unsafe memory manipulations for half a century, accumulating knowledge, working on runtime & libraries, and building tools like verifiers, debuggers, profilers. C++ has even more of these, adding some safety features to language and standard library while still being C compatible.


most of this seems to be an issue of maturity for the language itself and the surrounding ecosystem, which I did touch on a little bit. I probably should have made it a more explicit caveat.

aside from ubiquity and maturity, I'm curious what features c has that rust lacks for raw memory manipulation. I have yet to work my way through all of the rustonomicon and I am far from an expert c programmer, so I would appreciate the opportunity to fill in some knowledge gaps.


I agree that for C it’s mostly the ecosystem i.e. external tools & libraries. There’re just a few features in the language and runtime, such as debug heap and the preprocessor.

But C++ has a lot to offer besides tooling. Modern-style C++ (template containers, smart pointers) solve the majority of safety issues solved by safe Rust. But C++ doesn’t hide these raw pointers behind safe abstractions, and that higher level safer stuff is optional.

For specific features helping with raw memory manipulation see e.g. routines from <algorithm> header, http://en.cppreference.com/w/cpp/algorithm They are part of C++ standard library, and yet they support dangerous C arrays, because raw C pointers double as C++ iterators.


I’d characterize the situation differently; Rust handles a lot of things that Modern C++, and not even the Core Guidelines, try to handle. Iteration invalidation is huge. Concurrency and parallelism issues are huge.

I very much welcome these things, as I’m about safe software, not “only Rust”, but I don’t think the “most” claim holds water.


It’s all about tradeoffs.

If you prioritize performance over safety, there’s no way around these problems. To implement efficient algorithms processing trees, lists or graphs, you have to fallback to unsafe code & potentially invalid raw pointers. Unsafe Rust is as dangerous as C++, but for C++, standard library, runtime and tools provide huge help implementing and debugging such things.

If you prioritize safety over performance, of course Rust is way ahead of C++. But Java and C# are much easier to use and deliver safety comparable to Rust. Also in these languages, trees & graphs might be even faster than in safe Rust.


We believe this is a false dichotomy. If Rust is significantly slower than C++, it’s a bug. Generally, most of the time, we have succeeded at this, sometimes being faster, sometimes slower.

Most of those tools work on Rust as well. And we are pretty sure we can have better tools in the future, but that’s a while off.


> If Rust is significantly slower than C++, it’s a bug.

Safe Rust is significantly slower than C++ when working with pointer based stuff like trees and graphs. That’s why people use unsafe Rust for that kind of code.

> Most of those tools work on Rust as well.

It’s not just tools, also language and libraries.

Rust was designed for safety, and apparently unsafe was just neglected. Or maybe it was a decision to neglect making people use safe Rust instead (BTW same decision was made by Java’s designers at Sun).

In any case, the current state of unsafe Rust is not OK. AFAIK, stable unsafe Rust doesn’t even has malloc & free functions.


Yes. Unsafe code must exist for Rust to accomplish it's goals. Rust is also a practical language, and unsafe is an important part of that. The key is, unsafe is a relatively small percentage of code overall; even operating system projects have a very small amount of unsafe.

> apparently unsafe was just neglected.

I wouldn't agree with this. We work on unsafe things all the time; for example, NonNull<T> was just stabilized. As with any open source project, stuff gets done as people have the time and desire to do it.

> AFAIK, stable unsafe Rust doesn’t even has malloc & free functions.

So, this is literally true, but it's not because we hate unsafe or something. It's because a good allocator API is hard. We've been putting a lot of work into it over the last year or so, and it's actually pretty close to being in stable. The team moved to stabilize it back in October of last year, but some last-minute stuff has popped up.


> unsafe is a relatively small percentage of code overall

I don’t think it’s a good idea to talk about percentage of code overall when discussing Rust. Take a look at http://githut.info/ you’ll see that majority of overall code is higher-level GC languages.

I’ve been professionally developing software since 2000, have a lot of experience with different languages and platforms, and I’m speaking from my experience. There’re 2 major reasons why now in 2018 I still pick C++ for some software or some components of it.

(1) Code that relies heavily on native interop. Like OS APIs for lower-level device IO, advanced networking stuff, GPU interop, other OS APIs. All of these APIs are C, or sometimes on Windows it’s C++.

(2) Performance-critical CPU bound code. One part of that is SIMD, but pointer-based structures also help a lot, and they are not small percentage of my code. BTW, another thing missing in current Rust is custom allocators. In C++ I can implement a custom allocator in just a couple hundred lines of code, and plug it into any collection for non-trivial performance gain in some use cases: https://github.com/Const-me/CollectionMicrobench Another C++ feature helping me with these performance-critical calculations is OpenMP.

Of course, Rust evolves quite fast, and it may change some day. But in its current state, I don’t think Rust is an adequate C++ replacement for the kind of problems I solve in C++.


GitHut shows data from 2014, incidentally.

Rust easily accesses C/OS APIs, and has great tools like Rayon for paralellizing code. Both wrap unsafe and have nice safe interfaces too.

If you like C++, you should use it, though. Not everyone will use Rust. That’s 100% okay.


> AFAIK, stable unsafe Rust doesn’t even has malloc & free functions.

It's stable to import the C malloc/free, and it's relatively easy to use Vec as an allocator: https://www.reddit.com/r/rust/comments/7yhhq6/borrow_cycles_...


C++ iterators might look like pointers they aren't necessarily pointers implementation-wise.


That’s true. Nevertheless, you can pass pointers to C++ algorithms accepting iterators.


> "what does this language make easy" (C: raw memory manipulation!)

Depending on what you mean by "raw memory manipulation" that can actually be surprisingly difficult with C, without running into undefined behavior, at least technically-speaking. e.g. type-punning through unions is defined by all implementations I can think of, but technically UB according to the standard.

That said, if you know the UB rules, C/C++ are still the "nicest" mainstream languages to use for that sort of super-low-level manipulation of raw object representations. The non-UB type-punning methods in C/C++ (`memcpy(3)`, casting addresses to pointer-to-`char`), finicky as they are, are still more ergonomic and, ironically, safer than Rust's poorly-designed `mem::transmute()` API.

Language-lawyering is quite the rabbit hole, BTW; for example, it was discovered a few years ago that the wording of the formal C++ memory model defined in the C++11 and later standards means all non-trivial C programs (technically-speaking, not according to any existing or sane implementation) invoke undefined behavior in C++. We'll be lucky if the bikeshedding over the fix for this gets resolved in time to make C++20.

  #include <stdlib.h>

  void *safe_ptr(void *p) {
  	if (!p) exit(1);
    	return p;
  }

  #define INIT(TYPE) safe_ptr(malloc(sizeof(TYPE)))

  int *get_intbuffer(void) {
  	enum {N=3};
  	int *const intbuffer = (int*)INIT(int[N]);
  	int i = N;
	/* Technically UB in C++; lifetime starts
	 * w/ declaration or initializing w/ new
	 * or placement-new, so reads/writes thru
  	 * the result of malloc w/o initializing
  	 * the objects w/ placement-new are UB */
  	while (i--) intbuffer[i] = i;
  	return intbuffer;
  }


> are still more ergonomic and, ironically, safer than Rust's poorly-designed `mem::transmute()` API.

Eh, I disagree. I find:

    let x: f32 = ...;
    let y: i32 = mem::transmute(x);
is nicer than

    float x = ...;
    int y;
    memcpy(&y, &x, sizeof x);
And, in terms of safety, transmute manages the sizes of everything and makes sure they match. There's no risk of passing the wrong size, or having types with mismatched sizes (resulting in either "slicing", or buffer overflows).

The only way I can see transmute being more dangerous than the equivalent is that one can allow the types to be inferred, which can result in badness if the inferred type isn't what the programmer is expecting. This is especially dangerous when type involves a lifetime. (I suppose one could argue that it also makes this dangerous operation easier to do, since there's an easy-to-find function for it. But... this goes both ways, since it also stops people being tempted into using undefined pointer casting like ^(float ^)(x) (using ^ for * since HN likes italics too much).)

> Language-lawyering is quite the rabbit hole, BTW; for example, it was discovered a few years ago that the wording of the formal C++ memory model defined in the C++11 and later standards means all non-trivial C programs (technically-speaking, not according to any existing or sane implementation) invoke undefined behavior in C++. We'll be lucky if the bikeshedding over the fix for this gets resolved in time to make C++20.

I'm curious if you've got a defect report link or similar, because my reading of "6.8 Object Lifetime" in the C++17 draft implies that the lifetime has started for those int objects since the two conditions are both satisfied:

- "storage with the proper alignment and size for type T is obtained": the malloc'd pointer is fine.

- "if the object has non-vacuous initialization, its initialization is complete": ints have vacuous initialization.

But, I'm probably wrong, and would appreciate being corrected.


I think the Rust version is a lot less clear. I don't intuitively know what "transmute" is; if I guess it means "mutate across something... here from a float to an int" I don't know how it knows I mean int, so I have to suppose there's something special in Rust that lets me assign some bytes to a scalar like this; I worry that it does a copy instead of just mutating the bytes of `i32`; finally I'm unclear whether or not this uses unsafe (I bet that it does though). In contrast I know what memory is and I know what copying is.

This is maybe the core of what bothers me about Rust? There are good and valid arguments about C, but its core attraction is "everything is a number, memory is an array". I think Rust has achieved a great thing, but I wish it were like, 50x simpler. I would give up so many things for that.


> I don't intuitively know what "transmute" is

That's fair, but... it's easily resolved by using Rust/reading the documentation. Humans don't intuitively know what, say, 'float' in C means, either, but they learn it quickly.

Additionally, the non-Rust meaning of "transmute" is pretty close to how it's used in Rust: "To change, transform or convert one thing to another, or from one state or form to another".

There's definitely a lot of good in making things obvious to beginners to the language, but there's also always a general collection of "jargon"/symbols one ends up having to learn. Someone's who's a beginner in Rust but familiar with C may spend more time working out what the transmute line means than the C, but someone familiar with Rust wouldn't (and certainly not if they're not familiar with C: they'd have to work out the src/dst order for memcpy).

> I think Rust has achieved a great thing, but I wish it were like, 50x simpler

As others have pointed out elsewhere many times: what would you remove? The core of what people complain about in Rust (lifetimes etc.), is also core to achieving it's goals. All of those core features are pretty orthogonal and fairly minimal.


So I looked up transmute, and it turns out this is how it's called:

    let x: f32 = 0;
    let y: i32 = std::mem::transmute::<f32, i32>(x)
I'm fine with this, actually. With the type information it's clear. I do think it's a little long-winded, but I don't see a way to shorten it without giving up namespacing or safety.

> As others have pointed out elsewhere many times: what would you remove? The core of what people complain about in Rust (lifetimes etc.), is also core to achieving it's goals. All of those core features are pretty orthogonal and fairly minimal.

I'm pretty OK with lifetimes. I think probably they could be a little more explicit and have better syntax, but I think they're complicated by nature and -- as you point out -- core to achieving memory/data race safety in Rust.

I'm happy, however, to create a big gripe list here for you, haha :) Mostly my criticisms are:

- some concepts are unclear because they use overloaded keywords or imprecise language

- some features are confusing due to inconsistent structure/syntax

- some (many) features aren't worth their complexity

Innnnnnn order of the Rust book:

Rust is often different for no real reason. Casting is a good example of this; what was wrong with `(i8)thing`? What does `as` gain us? Couldn't we have used `as` in a more powerful way, like context managers in Python for example?

I'm not wild about `macro!`. Because macros are hygenic, I don't care if something is a macro or not and the `!` makes those calls stick out unnecessarily. I would have preferred that `!` indicated mutation in some way, like in Scheme for example. Or maybe get rid of the `mut` keyword and use `!` in variables. Really anything would be better.

I don't find "everything is an expression" to be that valuable, and it leads to a lot of weirdness. Semicolons suppressing block expression values is one -- why would you ever want to use a block as an rvalue to only assign `unit` to the lvalue? Returning from loops is another, and that looks very weird: `break <expr>`. I can see how if/else as expressions is nice shorthand in a ternary kind of way, but I wouldn't (and don't, in any other systems language) mind just initializing my variables with zero values and updating them inside those blocks.

Generally I like destructuring, but I think Rust's struct destructuring is a little much. I wouldn't mind having... I don't know 4 more lines of variable initialization the 0.0008% of the time I need to do this.

I would prefer that ranges weren't special syntax and instead worked just like any other iterator. This also avoids weird parens in range-based pipelines.

I'm a little on the fence with `match`. I think it strikes the right balance between Python's "use if/else for everything" and C's restrictive/dangerous switch, but most match blocks I see are messy. I think all in all I'm into it, and I like that it's got a history in other languages like OCaml, I just wish I could come up w/ a way to clean it up a little.

match guards make me crazy though; that's exactly what if statements are for. The whole reason I like match/switch more than if/else is that it's restricted to the thing you're matching/switching on. A match guard can run a conditional on anything. It's completely superfluous if your language has if/else.

You can probably guess I don't like match binding either. First I think that's what `let` is for, but I also think using `@` is both very non-intuitive and a big waste of an operator -- all to avoid a single `let` expression.

if/while let I really like, but its weird that they don't follow the same rules as regular let expressions, like can `Some(i)` be an lvalue normally (no, it can't). I would prefer that they did follow the same rules, or that they used different syntax than `let` because they are in fact different.

I wish closures taking no arguments didn't look like the boolean or operator. Using `cfn()` (closure function) here wouldn't have been terrible, I don't think.

`pub` doesn't really mean "public", it means "visibility" and pub without modifiers means "visibility(public)". I do sort of like `mod`, but I dislike having to nest things inside of it. I would prefer a file-based approach, where you declared the file's module at the top.

I would prefer `impl <trait> on <struct>` instead of `impl <trait> for <struct>` because `for` already means something else.

I would have liked to use traits as types in function signatures instead of the clunky generic syntax. Alternatively just require the `where` clause. Both options reduce the number of things you have to know.

I wish the "new type idiom" didn't reuse `struct`. Probably `type` goes there.

I don't really like the ceremony around heap allocation. I know there are a lot of benefits to `Box`, but I'd prefer `*` to Box<T> and `alloc` or even just `new` to `Box::new`. Or hey, if you're against operator reuse, let's use `@` now that we've tossed match binding. Mainly my complaint is this is unnecessarily different from other systems languages.

---

Probably these seem like really small issues, but I really think that all these things together would make Rust much more consistent and clear at a very slight cost to ergonomics.


> Rust is often different for no real reason. Casting is a good example of this; what was wrong with `(i8)thing`? What does `as` gain us? Couldn't we have used `as` in a more powerful way, like context managers in Python for example?

(type)expression is annoying to parse: in, say, '(i8)x' there's no way to tell that it's a cast expression until you get to the x. That is, (i8) is a valid expression (if there's a variable called i8), and is something like (x)(y) a cast or a function call?

In any case, destructors and move semantics gives most of the benefits of context managers.

> I'm not wild about `macro!`. Because macros are hygenic, I don't care if something is a macro or not and the `!` makes those calls stick out unnecessarily. I would have preferred that `!` indicated mutation in some way, like in Scheme for example. Or maybe get rid of the `mut` keyword and use `!` in variables. Really anything would be better.

It's a bit of a personal argument, but being explicitly marked highlights where weird things may happen, like side-effects that happen twice or returns out of the current function (a function call itself can't return: only `return`, `?` and macros that use them). But sure, it's something people might not like.

> Semicolons suppressing block expression values is one -- why would you ever want to use a block as an rvalue to only assign `unit` to the lvalue?

This is just consistency. Why have block-as-an-r-value as the only case when a semi-colon isn't allowed? Furthermore, there's places for blocks that are formally r-values, but perfectly legitimately have type (), like the arms of match blocks and bodies of closures. And, lastly, not having a special case like this makes writing macros easier.

> I'm a little on the fence with `match`. I think it strikes the right balance between Python's "use if/else for everything" and C's restrictive/dangerous switch, but most match blocks I see are messy. I think all in all I'm into it, and I like that it's got a history in other languages like OCaml, I just wish I could come up w/ a way to clean it up a little.

Match is necessary for working with `enum`s, and enums are great tool for avoiding allocations (e.g. Option<T> instead of a T* that's possibly null) and generally for guiding towards type safety. Having a single entity that does the complete deconstruction of an enum is important with move semantics, or else one would be forced to do a lot of extraneous nested "as_mut"/"is_none" etc. checking, especially for nested deconstructions.

> match guards make me crazy though; that's exactly what if statements are for. The whole reason I like match/switch more than if/else is that it's restricted to the thing you're matching/switching on. A match guard can run a conditional on anything. It's completely superfluous if your language has if/else.

It's very convenient for conditionalizing based on enums... and yes, it's unrestricted, but that's for consistency: why restrict it?

> You can probably guess I don't like match binding either. First I think that's what `let` is for, but I also think using `@` is both very non-intuitive and a big waste of an operator -- all to avoid a single `let` expression.

Do you don't like binding any variables in a match, or specifically doing it with @? I don't think you can use a let to emulate it, at least not without a lot of clunkiness. In any case, it's rarely used and rarely seen, and yes, probably not worth `@` (a keyword could be better).

> if/while let I really like, but its weird that they don't follow the same rules as regular let expressions, like can `Some(i)` be an lvalue normally (no, it can't). I would prefer that they did follow the same rules, or that they used different syntax than `let` because they are in fact different.

If they followed the same rules, they wouldn't be conditional and there would be no point. One can regard the 'if' and 'while' addition as exactly that difference in syntax: "if" means conditional, so an "if let" is a conditional let. I would think adding more keywords/syntax has a larger downside than the small expansion of let's behaviour, but it's hard to say without actually being able to compare it in practice. The "killer" argument for me is that other languages use the same syntax, so there's no particular reason for Rust to be different here, given it's mostly an aesthetics argument.

> `pub` doesn't really mean "public", it means "visibility" and pub without modifiers means "visibility(public)". I do sort of like `mod`, but I dislike having to nest things inside of it. I would prefer a file-based approach, where you declared the file's module at the top.

Rust does have a file-based approach.

"pub" does mean "public", just more restricted than globally. Which, to be fair, is usually what public means in english. Visibility/access control has been an endless argument in Rust.

> I would prefer `impl <trait> on <struct>` instead of `impl <trait> for <struct>` because `for` already means something else.

'for' means something else in a completely different context. I don't really see the benefit in distinguishing them, but sure, I guess you could rename a keyword.

> I would have liked to use traits as types in function signatures instead of the clunky generic syntax. Alternatively just require the `where` clause. Both options reduce the number of things you have to know.

You'll be excited for some of the "impl trait" stuff.

> I wish the "new type idiom" didn't reuse `struct`. Probably `type` goes there.

It's literally just an idiom built on top of a struct. There's nothing special about it. I take it you want the idiom to be baked into the language to be slightly nicer.

> I don't really like the ceremony around heap allocation. I know there are a lot of benefits to `Box`, but I'd prefer `` to Box<T> and `alloc` or even just `new` to `Box::new`. Or hey, if you're against operator reuse, let's use `@` now that we've tossed match binding. Mainly my complaint is this is unnecessarily different from other systems languages.*

Box is (only very slightly) special: it's more consistent to not preference it over other pointer types. In any case, Box literally used to be "~" and Rc "@". People complained endlessly about the impenetrable sigils.

Also, Box shouldn't be that common in most Rust code.


> (type)expression is annoying to parse

Ehhh I think it's not that bad, no harder than arithmetic expression parsing certainly. It's just one more grammar rule. Plus Rust supports everything that's needed already because of operator overloading.

> In any case, destructors and move semantics gives most of the benefits of context managers.

They (well, destructors anyway) are less explicit though. When you use a context manager in Python you know it's cleaning things up. When you "use" a destructor in Rust you usually don't ever know you did. This can get you into trouble if you're relying on RAII to cleanup after you: you can get a handle to things and then enter a long loop or call chain. Sure you can avoid that by calling `drop`, but if context managers were the idiom this kind of thing would never be an issue. But I admit the difference is pretty small -- and in fact might be surprising to systems programmers so probably it's the right choice (even if destructors themselves are kind of mind boggling).

> This is just consistency. Why have block-as-an-r-value as the only case when a semi-colon isn't allowed? Furthermore, there's places for blocks that are formally r-values, but perfectly legitimately have type (), like the arms of match blocks and bodies of closures. And, lastly, not having a special case like this makes writing macros easier.

Well mostly I would deal with all this by removing "everything is an expression". Match blocks would just be regular blocks like `switch` in C or what have you, and so on.

> Match is necessary for working with `enum`s, and enums are great tool for avoiding allocations (e.g. Option<T> instead of a T* that's possibly null) and generally for guiding towards type safety. Having a single entity that does the complete deconstruction of an enum is important with move semantics, or else one would be forced to do a lot of extraneous nested "as_mut"/"is_none" etc. checking, especially for nested deconstructions.

Well switch/if/else have worked fine for a long time; and now C/C++ compilers warn you on missing branches when switching on enumerated values (Rust won't if you have a fallthrough case). But I prefer switch/match to if/else because if/else are too general for just switching on a variable (which is why I'm very dismayed at match guards -- they wholly abrogate the benefit of match over if/else), and I think switch's behavior is generally too restrictive (this is the only problem I have with switch provided you use proper blocks instead of the souped-up goto it really is). Match has so many features baked into it that every time I see one I have to stop and take a deep breath. It's very much geared towards writing and not reading, I feel.

> [match guards are] very convenient for conditionalizing based on enums... and yes, it's unrestricted, but that's for consistency: why restrict it?

I kind of went into this above, but in a language that doesn't have a good match/switch (like Python) you'll frequently run into code like this:

    if value == 1:
        # do a thing
    elif value == 2:
        # do a thing
    elif value in range(3, 20):
        # do a thing
    elif totally_unrelated_function_call() and other_thing == 98:
        # do a thing
    elif value >= 20:
        # do a thing
But when you have switch, you can't run that 4th conditional and have it shortcircuit the 5th conditional. Switch is, in that way, very much specifically for breaking down enums, so when I see one I can restrict my thinking to that variable and that variable alone.

Unless, of course, we're using match guards. Then I have to consider everything again, and I wonder why we're not just using if/else. Of course I understand that match is an expression so that's another "benefit", but if/else also have that behavior in Rust and I would get rid of that anyway.

> Do you don't like binding any variables in a match, or specifically doing it with @? I don't think you can use a let to emulate it, at least not without a lot of clunkiness.

I don't think this is too bad:

    fn main() {
        println!("Tell me type of person you are");

        let my_age: age();

        match my_age {
            0             => println!("I'm not born yet I guess"),
            // Could `match` 1 ... 12 directly but then what age
            // would the child be? Instead, bind to `n` for the
            // sequence of 1 .. 12. Now the age can be reported.
            1  ... 12 => println!("I'm a child of age {:?}", my_age),
            13 ... 19 => println!("I'm a teen of age {:?}", my_age),
            // Nothing bound. Return the result.
            _         => println!("I'm an old person of age {:?}", my_age),
        }
> Rust does have a file-based approach.

Sure but I guess my argument is the file/folder hierarchy approach combined with mod is a little clunky. I think it's clearer to just declare a module at the top and let people use whatever folder structure they want.

> Visibility/access control has been an endless argument in Rust.

Hah, OK fair. I guess you can't please everyone ;)

> I take it you want the idiom to be baked into the language to be slightly nicer.

Yeah like `type` or some such. It's a little weird to kind of overload struct this way (and don't get me going on enum haha -- it's a variant!!!!!!!!!!! it's a union!!!!!!! it's anything other than an enum!!!!!).

> People complained endlessly about the impenetrable sigils.

Oh yeah, I was one of them. But my complaint was all the extra sigils. There was `~` for an owned pointer, `@` for GC pointers, and the `mut` suffix for mutable versions, and the worst sin of all was that `*` was strictly for unsafe pointers. So the one you're most likely to recognize is the one you'll basically never see. Booooo.

But I guess mostly what it comes down to is that "everything is an expression" greatly weirds the language for me, match is a super feature, and there are weird tricks that don't really make sense like "Use _ as the default case in a match" and "if you're destructuring in a match and you don't care about some fields, just use `..`". At least in switch, the default case is called "default".


> Ehhh I think it's not that bad, no harder than arithmetic expression parsing certainly. It's just one more grammar rule. Plus Rust supports everything that's needed already because of operator overloading.

No, it forces you to have a cover grammar for things that could be either a type or an expression, i.e. you need to be able to parse the superset of both possibilities, from when you see the '('. This isn't something Rust needs or has at the moment.

> They (well, destructors anyway) are less explicit though. When you use a context manager in Python you know it's cleaning things up. When you "use" a destructor in Rust you usually don't ever know you did. This can get you into trouble if you're relying on RAII to cleanup after you: you can get a handle to things and then enter a long loop or call chain. Sure you can avoid that by calling `drop`, but if context managers were the idiom this kind of thing would never be an issue. But I admit the difference is pretty small -- and in fact might be surprising to systems programmers so probably it's the right choice (even if destructors themselves are kind of mind boggling).

Yeah, that's why I said "most". :)

It's fair that destructors are implicit, but context managers are used for a lot of bread-and-butter clean-up like `with open(filename) as f:` etc, for which the destructor barely does anything.

Additionally, context managers end up being super "infectious": they're much harder to store, manipulate and return than an object with a destructor. E.g. how do you write a function f that opens a file and returns it for the user to use (maybe it does something tricky to find which file to open, or something)? You'd need coroutines or passing a closure into f to be able to manipulate the file handle while its context was open. I don't think the infrastructure required is the right trade off for a systems language (it seems like it'd end up with a fairly strong compile-time vs. runtime performance trade-off, and Rust's compile times are bad enough as they are).

> Well mostly I would deal with all this by removing "everything is an expression". Match blocks would just be regular blocks like `switch` in C or what have you, and so on.

Then you end up with a pile of ceremony and junk, with 'return's everywhere (some functions would get 50% larger/more noisy, just from the 7 characters "return "), and, as with most things, it makes macros and generating code more annoying. However, just to be clear, these changes you're suggesting are making Rust less consistent.

There are many places where Rust is different to C and C++, but it is also more consistent, which is one of your complaints. (You can see "everything is an expression" style of thinking has benefits even in C: the classic do ... while(0) trick for macros, plus GCC's statement expressions.)

> Well switch/if/else have worked fine for a long time; and now C/C++ compilers warn you on missing branches when switching on enumerated values (Rust won't if you have a fallthrough case). But I prefer switch/match to if/else because if/else are too general for just switching on a variable (which is why I'm very dismayed at match guards -- they wholly abrogate the benefit of match over if/else), and I think switch's behavior is generally too restrictive (this is the only problem I have with switch provided you use proper blocks instead of the souped-up goto it really is). Match has so many features baked into it that every time I see one I have to stop and take a deep breath. It's very much geared towards writing and not reading, I feel.

It's not clear to me how much Rust you know from this sentence: you Rust enums do more than C/C++ ones? Each case can contain data, and match is the only way to get at that data. There's no other way to conditionally deconstruct an enum down into its parts, other than 'if let' and 'while let' but those have the problems of if/else.

Having data is the main motivation for match guards: to conditionalise on things that only make sense for that arm. Matches get matched in order, meaning if a guard fails it falls through to check the next one, this means that guards even on data not from that arm are useful (e.g. maybe an arm only applies in certain cases, in which case other variants should take precedence). However, yes, match guards are rare.

In any case, my experience is almost all matches are simple pattern matching, there's no guards, no @s. It's theoretically possible (and occasionally occurs, sure) that someone writes a ridiculous match using its 3 separate constructs, but that's true of many things? I personally find the most annoying thing is how deep the code of a match ends up being indented.

Lastly, I hate the "X has worked fine" arguments: "mail has worked fine for for a long time, why do we need email". It feels like an intellectual shortcut to cut off discussion: if X has worked fine, it should be easy enough to defend it in comparison to the new thing (which, to be fair, you do :) ). In any case, match recognizes that switch has worked fine, and does what it can do (except fall-through, but pattern-alternation with | covers most of why fall-through is used in practice).

> Then I have to consider everything again, and I wonder why we're not just using if/else

Because it fundamentally doesn't work with enums.

Also, I feel any restriction is pretty pointless because you can always have dummy use of a value:

  fn always<T>(_: &T) -> bool { true }
  match x {
    Enum::Variant(a) if global && always(&a) => { ... }
    ...
  }
It's fair that people usually wouldn't do this, but still, it seems less consistent: it's special-casing the scoping rules for expressions in a match-guard, for somewhat arbitrary "code style" reasons.

> I don't think this is too bad:

That's not the impossible/difficult cases: nested matches are:

  let foo: Option<AnEnum> = ...;
  match foo {
    Some(inner @ AnEnum::Variant(_, _ , _)) =>  { ... }
    _ => { ... }
  }
Doing this with just a 'let' requires matching each contained value of the nested pattern and then reconstructing that whole thing.

However, I agree that @ is barely useful and it's definitely rarely used.

> Sure but I guess my argument is the file/folder hierarchy approach combined with mod is a little clunky. I think it's clearer to just declare a module at the top and let people use whatever folder structure they want.

This is again something that's argued endlessly about (and I think? there's been recent work/proposals to change it).

I personally find the core pub/no-pub + mod + use system (of 1.0, I've lost track of the various additions) is nicely minimal, with those three pieces that fit together quite orthogonally/consistently. This has benefits like ease of navigation and consistent behaviour between projects, rather than the C++ style of a namespace splattered across hundreds of headers.

However, it's definitely true that it is clearly clunky and hard-to-use for a lot of people.

> Oh yeah, I was one of them. But my complaint was all the extra sigils. There was `~` for an owned pointer, `@` for GC pointers, and the `mut` suffix for mutable versions, and the worst sin of all was that `` was strictly for unsafe pointers. So the one you're most likely to recognize is the one you'll basically never see. Booooo.*

So a sigil for Box/owned pointer is okay, but not for any other library-defined types? The current behaviour is consistent: types built deeply into the language (& and &mut are the building blocks of safety, and const/mut are the building blocks of every other pointer; both of which are completely dependency-less: no allocations, etc.) get sigils, and those that are plain library types do not.

While it's fair/a little weird that raw pointers get s, I think it's fairly defensible, for a few reasons: Box is quite rare in Rust (& and &mut are used most often, when pointer-like objects are needed), raw pointers are often used when close to C so there's a sense in which not using would be "being different for no real reason", and `unsafe` code is unpleasant enough as it is to read and write using long verbose types wouldn't help. But I would also think it's defensible to not have raw pointers have sigils.

> Use _ as the default case in a match

NB. this is also consistency: _ is "match any value" in every pattern, whether as the last arm of a match or elsewhere. (And, to be clear/linking to the next point, it's always match any single value.)

> if you're destructuring in a match and you don't care about some fields, just use `..`".

While it's fair that .. is a little impenetrable, what's the alternative? Listing every field? Not writing anything at all, and having no reminder/indication that there's ignored data (and also no help with refactoring when adding fields to the type)?

---

I'm probably seeming kind-of ranty here, but I think a lot of these sort of "Rust isn't consistent/is too complicated" discussions come down to familiarity. Don't get me wrong, it's definitely unfortunate that it's unpleasant to write when one is unfamiliar (would be way better if it was smooth from the start), but there is a core consistency.

I also think it's worth separating out "Rust is complicated" and "Rust is different", although the consequence of the two probably end up being similar in a lot of cases (hard to build an accurate mental model because things are unexpected).


> I'm probably seeming kind-of ranty here, but I think a lot of these sort of "Rust isn't consistent/is too complicated" discussions come down to familiarity.

Not at all! I'm honestly really grateful you're engaging. Let me get home and I'll respond fully :)


> No, it forces you to have a cover grammar for things that could be either a type or an expression, i.e. you need to be able to parse the superset of both possibilities, from when you see the '('. This isn't something Rust needs or has at the moment.

100% agree, but I think it's probably worth the extra complexity in Rust's implementation to restore familiarity with casting.

I don't know if this is already a concept somewhere (I feel like it has to be) but I think that given a software or information encoding problem, there's a certain base complexity. You might be able to solve that problem in multiple different ways, splitting up the complexity in each one, but the total amount of complexity is still there.

Memory management is a good example. Memory must be managed somehow, and in languages like Python, Java, and even Rust (with lifetimes and (A)Rc) the complexity of that management is in the language/platform implementation whereas in languages like C it's in the application. Regardless, it exists.

So I would prefer that this complexity be in the implementation in order to maintain familiarity with the long history of systems and applications languages. I recognize it's more work for Rust, but as a user of Rust and not a maintainer, I'm OK with that ;)

Re: Context managers, I think we agree here and in fact, basically all I want out of a context manager is Rust's blocks. I guess a block without a statement is a little strange, but actually in Rust it's kind of idiomatic so I can get behind it.

> Then you end up with a pile of ceremony and junk, with 'return's everywhere (some functions would get 50% larger/more noisy, just from the 7 characters "return "), and, as with most things, it makes macros and generating code more annoying.

Woof, I do _not_ consider `return` noisy. Along the same lines as "everything is an expression is weird", I think implicit returns are weird. I guess it's maybe like everything; like when you work in Python you think braces and semicolons are annoying noise, when you work in Java you think manual memory management is annoying noise, and now maybe that goes for `return` in Rust. I'm almost never irritated by "ceremony" -- I'm pretty good at typing. Instead it's the "neat tricks" and inconsistent structure of programs that really eats my time and burns my brain cycles.

I'll admit to not being a huge fan of macros -- especially in systems languages. I think inlining functions is far less surprising, and the fact that you have reduced power compared to macros means there's far less surprising behavior (i.e. "why am I returning early..."). I know they're good for getting rid of "ceremony" but I'm guessing we'll end up disagreeing about how important that is :) But consequently I'm not really willing to give up anything to make macro writing easier.

> However, just to be clear, these changes you're suggesting are making Rust less consistent. There are many places where Rust is different to C and C++, but it is also more consistent, which is one of your complaints. (You can see "everything is an expression" style of thinking has benefits even in C: the classic do ... while(0) trick for macros, plus GCC's statement expressions.)

Haha well, I'm not gonna defend do/while(0). Textual macros and optional braces are obviously (now) not a good idea.

Consistency is fine as long as it's good consistency. Sure C mixes a lot of statements with a few expressions, but that never bothered me because that's practically all mainstream languages. "Everything is an expression" is consistent, sure, but at what cost?

> There's no other way to conditionally deconstruct an enum down into its parts, other than 'if let' and 'while let' but those have the problems of if/else.

I guess really what I want is for there to be a construct that switches between different enums, and there to be a different construct that switches between different values. Conflating the type with the value is confusing to me. Ex:

    fn inspect(thing: ThingEnum) {
        match thing {
            case ThingEnum::ThingOne {
                switch thing.thing_one_field {
                    case 1 {
                        // do ThingOne.thing_one_field == 1
                    }
                }
            }
            // etc.
        }
    }
Yeah it's a little pyramid-y, but hey welcome to matching and variant types. If you really worked things around you wouldn't need to nest so far, but that's probably too much of a syntax change:

    fn inspect(thing: ThingEnum) {
        thing=>variant(ThingEnum::ThingOne) {
            switch thing.thing_one_field {
                case 1 {
                    // do ThingOne.thing_one_field == 1
                }
            }
        }
        // etc.
    }
Anyway there are a lot of benefits. The distinction between switching on type and value is very clear. Blocks and control flow are very clear. It uses previously standard constructs (switch/case). I really don't need to know anything about Rust to know how this works; it is self-evident. Really maybe the only ambiguous thing is "match", which should probably be like "variant" or something, but whatever.

> In any case, my experience is almost all matches are simple pattern matching, there's no guards, no @s. It's theoretically possible (and occasionally occurs, sure) that someone writes a ridiculous match using its 3 separate constructs, but that's true of many things? I personally find the most annoying thing is how deep the code of a match ends up being indented.

100% agree.

> That's not the impossible/difficult cases: nested matches are:

    let foo: Option<AnEnum> = ...;
    match foo {
        Some(inner @ AnEnum::Variant(_, _ , _)) =>  { ... }
        _ => { ... }
    }
While I get what this does, it looks very noisy. Losing the ability to do this in a single construct is fine w/ me if this is the result.

> ...C++ style of a namespace splattered across hundreds of headers.

That's a fair point and worth worrying about, haha. Good call.

> So a sigil for Box/owned pointer is okay, but not for any other library-defined types?

Mostly I just didn't think the (A)Rc pointers needed a sigil, and I thought it was weird that what most people would think of as a pointer used to use `~` and now uses `Box`, whereas the pointer you would (mostly) never use is the one with the most familiar sigil (`* `). Feels like that one should be `Raw` and `Box` should be `* `. Library-defined or otherwise doesn't really matter to me; and if you're building something that can't allocate, the compiler will just tell you when you can't use `alloc`/`new` or whatever. EZ.

I get what you're saying about unsafe code being closer to C though. I guess I would have made regular Rust closer to C and had unsafe be less ergonomic, as an interesting way to discourage people from using it (see Python's prolific use of `__` everywhere), so that's probably the root of our disagreement here.

> While it's fair that .. is a little impenetrable, what's the alternative?

I would get rid of struct destructuring entirely. It's only real use is inside of match, and that's packing more things into match.

> I think a lot of these sort of "Rust isn't consistent/is too complicated" discussions come down to familiarity

Oh I'll definitely cop to being 1000x better at C than I am at Rust, and the more I use it the more I'm fine with it. But moving from C to Rust (or any language to Rust) is so hard because of all of these things. I mostly work in C, Python, Java, and JavaScript and Rust is very different from all of those -- and it's hard for me to justify those differences. And as a C programmer, the borrow checker isn't responsible for the learning curve. Rather, it's all the "neat" things in Rust. All I really wanted was C with a borrow checker, or Java without GC and a 90s idea of OO (hand waving a lot here). I honestly don't see why we had to tack on all this extra stuff.

> I also think it's worth separating out "Rust is complicated" and "Rust is different", although the consequence of the two probably end up being similar in a lot of cases (hard to build an accurate mental model because things are unexpected).

Definitely. Point taken :)


> 100% agree, but I think it's probably worth the extra complexity in Rust's implementation to restore familiarity with casting.

This is framing it a trade-off between writing something complex once versus forcing everyone to handle that papercut. Which, usually, I'd agree with the going with the complex-but-only-once (a question of asymptotics, after all).

However, I'm not sure it's entirely like that in this case: at the very least, this complexity here is revealed to the programmer (they have to do the same parsing switch in their head, even if it's usually fairly obvious). Additionally, this complexity applies, somewhat, to tools that work with code too, not just the compiler (e.g. limited editors trying to do syntax highlighting without running more detailed semantic analysis). This seems like such a minor thing to introduce such a heavy penalty, but maybe there's something more annoying you're finding with 'as'.

> Woof, I do _not_ consider `return` noisy. Along the same lines as "everything is an expression is weird", I think implicit returns are weird. I guess it's maybe like everything; like when you work in Python you think braces and semicolons are annoying noise, when you work in Java you think manual memory management is annoying noise, and now maybe that goes for `return` in Rust. I'm almost never irritated by "ceremony" -- I'm pretty good at typing. Instead it's the "neat tricks" and inconsistent structure of programs that really eats my time and burns my brain cycles.

Sure, I can see that; like any symbols, the brain quickly glazes over keywords like 'return', but it's still a little bit of processing. In any case, implicit returns do feel a bit weird to me at times, but Rust is statically typed, and the returns are never in surprising places (i.e. it's always the last thing in a function/block), which means I don't have to think about it: if it type checks, it's usually what I meant.

I really do think this is just a familiarity thing: coming from languages with a strong statement vs expression distinction it's weird, coming from mathematics/languages with out the distinction, it isn't. The language isn't particularly more complex because of it, it is just slightly different.

It's true that Rust's target market is mostly the former set of languages, so one could argue maintaining familiarity is critical (something Rust acknowledges: {} for scope and <> for generics driven by that), but it's also an argument that would have kept us writing slightly improved assembly languages forever.

> Consistency is fine as long as it's good consistency. Sure C mixes a lot of statements with a few expressions, but that never bothered me because that's practically all mainstream languages. "Everything is an expression" is consistent, sure, but at what cost?

Yes, what cost? I genuinely don't see a cost other requiring some people to get used to it, and I do see costs to the other approach.

I personally hate the C/C++ pattern of having to declare things and then initialize them later. The C++ code I'm currently writing has several places where I've been forced to write things similar to:

  const char *name;
  switch (someEnum) {
  case X: 
    name = "...";
    break;
  case Y:
    name = "...";
    break;
  // ...
  }
I personally find the following to be so much nicer:

  let name = match someEnum {
    X => "...",
    Y => "...",
    // ...
  };
In particular, everything is together, I'm not having to skip over the low-information-density "case", "break" and "name =" to find the interesting bits (the enum variant and the string it corresponds to), plus I'm not having to what reconstruct those two statements are actually trying to do (it's just initializing name).

An additional, although possibly contentious, benefit is this lets type inference work: name didn't need a type. This is more important when the type is long and complicated: type inference lets one use that complicated type without having to write it out, whereas in the declaration version, one might be tempted to go to a simpler type for development ease (or duplicate code, to not have to have 'name' live outside the switch) even if it is slower (e.g. collecting an iterator to a Vec).

(I also forgot to mention ternary ?: in C: it's also partly an acknowledgement that if-as-a-expression is useful.)

> I guess really what I want is for there to be a construct that switches between different enums, and there to be a different construct that switches between different values. Conflating the type with the value is confusing to me. Ex:

At the very least, this makes type checking harder (both for compilers and for people trying to write/understand the code, and compiler error messages): the 'thing' variable doesn't have type 'ThingEnum', it has a changing type that starts as 'ThingEnum' but switches to some restriction of that in a branch.

This opens a whole can of worms about wanting `if x is ThingEnum::ThingOne` to also restrict the type or `assert!(x is ThingEnum::ThingOne)`, and then wanting this type restriction thing to be more first class (e.g. abstracting it away behind functions, like `x.is_some()` succeeding "setting" x's type to Option::Some so that one can access the contained value).

This all sounds great and useful! And, it practically all already exists naturally with Rust's (and that of most other languages with similar enums[1]) current model: the enum variant itself is a first class way to reason about the various variants, and extracting the data (or at least, binding the thing to a new variable) at the point you find out the variant means there's never a worry about making sure the compiler understands all the ways in which to decide that an enum value is actually a specific variant.

I don't see much upside to emulating C-style manual tagged unions here.

[1]: This is a point where other people would complain about breaking with other practice for no good reason too (anyone who had used Haskell or OCaml or similar would find this system unnecessarily clunky).

Focusing on a single value also doesn't generalize/scale: for instance, if one is making a decision that depends on more than one value:

  fn maybeAdd(x: Option<i32>, y: Option<i32>) -> Option<i32> {

    match (x, y) {
      (Some(left), Some(right)) => Some(left + right),
      (Some(left), None)        => Some(left),
      (None, Some(right))       => Some(right),
      (None, None)              => None
    }

  }
(One could, theoretically, merge the two Some/None lines, with an | pattern, I like it like the above, since the RHS is so simple. The last 3 lines of this specific example could be reduced to "(left, None) => left, (None, right) => right", but that doesn't apply generally so I didn't do it for this one.)

Under a separated scheme (plus returns) the match looks like:

    match x {
      Some => match y {
        Some => Some(x.value + y.value),
        None => Some(x.value),
      }
      None => match y {
        Some => Some(y.value),
        None => None,
      }
    }
Similar to the "name" example above, I find this doesn't clearly express how I think about code, and I have to reverse engineer it: I don't want to follow the tree to see "if x is some and y is some then add, otherwise [y is none] so return x, ...". That's how a computer thinks, but that's not how I want to think about my code in almost all cases: usually I want to know the task the code is doing, not the details of every little step (the latter only in cases of micro-optimisation of a tight loop/hot function: if necessary, I can still write the Rust in that form). The Rust more declaratively expresses "if they both exist, add the values, otherwise if one of them exists, return that, otherwise return nothing".

Also, slightly related, but there has been semi-regular discussions around allowing an individual enum variant to be treated as a struct, essentially, so that instead of needing to define a whole new struct for any variant that might need to be manipulated on its own, one can refer to the variant directly:

  struct Foo { x: i32, ... }
  enum Bar { 
    Foo(Foo),
    ...
  }
  enum Baz {
    Foo { x: i32, ... } 
    ...
  }
Then Baz::Foo would be a struct-like type. (However, any proposal here would still require a match and new variable bindings and so on, because that avoids all the problems of having things have variable types.)

(Continued...)


(...)

> While I get what this does, it looks very noisy. Losing the ability to do this in a single construct is fine w/ me if this is the result.

FWIW, I agree, just demonstrating that replacing @ in patterns isn't the easy case as it is with the classic integer range example you gave. :)

> Mostly I just didn't think the (A)Rc pointers needed a sigil, and I thought it was weird that what most people would think of as a pointer used to use `~` and now uses `Box`, whereas the pointer you would (mostly) never use is the one with the most familiar sigil (` `). Feels like that one should be `Raw` and `Box` should be `* `. Library-defined or otherwise doesn't really matter to me; and if you're building something that can't allocate, the compiler will just tell you when you can't use `alloc`/`new` or whatever. EZ.*

The one I think of as a pointer is & and &mut. And, there's a whole pile of reasons that Box doesn't get used nearly as much as * and malloc in C:

- arrays and non-owning pointers use different syntax (Vec/[] and &/&mut respectively)

- proper generics means much fewer places where one needs to create a void* to pass data around (e.g. std::thread::spawn vs. pthread_create)

- enums means polymorphism and optional-ness can be done with "inline" types with little ceremony.

But yes, as I said, it is a little weird that the dangerous pointers gets relatively nice syntax.

> I would get rid of struct destructuring entirely. It's only real use is inside of match, and that's packing more things into match.

If you're including tuples as structs, I strongly disagree: it's great for `let` and multiple returns (and even true structs, not tuples, are sometimes nice there, although 'foo'/'bar' has less benefit versus a struct's 'x.foo'/'x.bar' than versus a tuple's 'fooAndBar.0'/'fooAndBar.1'). Also, .. works for struct enum variants: the Baz::Foo variant above is valid Rust syntax now.

> Oh I'll definitely cop to being 1000x better at C than I am at Rust, and the more I use it the more I'm fine with it. But moving from C to Rust (or any language to Rust) is so hard because of all of these things. I mostly work in C, Python, Java, and JavaScript and Rust is very different from all of those -- and it's hard for me to justify those differences. And as a C programmer, the borrow checker isn't responsible for the learning curve. Rather, it's all the "neat" things in Rust. All I really wanted was C with a borrow checker, or Java without GC and a 90s idea of OO (hand waving a lot here). I honestly don't see why we had to tack on all this extra stuff.

Yeah, it's definitely true that new things have a "change budget": how different they can be before it's too much. Rust has been designed with this in mind (things like {} and <>, as I mentioned above), but different people's threshold for it are different. There's lots of changes in Rust over C that make code more declarative and require less mental reconstruction (for me), which means I enjoy writing Rust more than if it didn't have them, but it's definitely true that they aren't literally necessary, or that they could maybe be phrased in more restricted ways that match closer to C.

I've personally found being exposed to many very different languages has helped inform the code I write in all others. Each new one definitely takes some getting used to, but my experience is that having touched several different paradigms has made me both more flexible (less attached to any particularly way of doing things) and a deeper understanding of even the "boring" languages: the trade-offs and "whys". There's value to things being the same, but there's also value in being able to break out of that mold and doing new things even if there's not an obvious benefit when focused on the old style.

I hope that you find Rust more and more enjoyable as you use it more, and if not, that's unfortunate: not everything works for everyone. Hopefully, at the very least, Rust inspires other languages that suit you better. :)


Just chiming in that I really like your absurdly long HN comments about Rust, and that while I have a script that gives me an RSS feed of them, others don't.

You should consider a blog where you just copy-paste long HN comment threads about Rust you find yourself writing.


Thanks for the kind words! I do have a blog with various writings about Rust (link in my profile), but I don't think publishing comments would be appropriate for me right now (at least, not without more work). :)


> However, I'm not sure it's entirely like that in this case: at the very least, this complexity here is revealed to the programmer (they have to do the same parsing switch in their head, even if it's usually fairly obvious). Additionally, this complexity applies, somewhat, to tools that work with code too, not just the compiler (e.g. limited editors trying to do syntax highlighting without running more detailed semantic analysis). This seems like such a minor thing to introduce such a heavy penalty, but maybe there's something more annoying you're finding with 'as'.

To be honest, I really think you're blowing the complexity of this way out of proportion. Plus like, it's not like Rust's grammar is this incredible thing of beauty: look at `where` clauses or lifetime syntax. If Rust were really optimizing for human readability, it's hard for me to imagine this is the result. It's purely a style thing, just like `let` instead of `var` and so on, and I don't think sacrificing programmer familiarity for a designer's idea of style is a good tradeoff.

> I really do think this is just a familiarity thing: coming from languages with a strong statement vs expression distinction it's weird, coming from mathematics/languages with out the distinction, it isn't. The language isn't particularly more complex because of it, it is just slightly different.

I disagree; I think the language is significantly more complex as a result. "Everything is an expression" leads to a lot of assigning out of `if` and `match` expressions, consequently there's a ton of pressure to dump a lot of things into them. `match` in particular is really just a regex for variables, except 100x bigger. In most ways I consider that a regression.

> It's true that Rust's target market is mostly the former set of languages, so one could argue maintaining familiarity is critical (something Rust acknowledges: {} for scope and <> for generics driven by that), but it's also an argument that would have kept us writing slightly improved assembly languages forever.

I don't think that C/C++ are being held back because they have statements though. My whole point is that Rust fixes the main issues with C and C++ (weird tricky behavior, super dangerous memory management, data races, concurrency, no/bad standard library), but then for considerably less gain tacks on a lot of functional programming ideas.

To be clear I'm not at all against functional programming. I just think most systems programmers aren't functional programmers because there really haven't been (and still aren't, as Rust isn't really functional) functional systems languages. And because Rust doesn't have the benefits of a lot of functional languages (super cool Lisp macros, etc.) I struggle to see the point beyond a style preference.

> I personally hate the C/C++ pattern of having to declare things and then initialize them later.

Like this for example. Do you hate it enough to pull all the statements out of a language? Feels like time that could be better spent somewhere else.

> Yes, what cost? I genuinely don't see a cost other requiring some people to get used to it, and I do see costs to the other approach.

The vast, vast majority of programmers come from languages with statements, and one of the main complaints about Rust is its learning curve. I think that's a significant cost.

Like, when I build an application in Java, Python, or C, "I had to initialize a variable using a function instead of an if/match statement" is nowhere on the pitfall list. That's not what our industry struggles with. It struggles with program complexity, logic errors, concurrency, and architectural confusion. "Everything is an expression" does nothing to address those issues. Lifetimes and the borrow checker do, and I think those departures are great investments for systems programming. Assigning from a match solves a problem I never had.

> I personally find the following to be so much nicer:

  let name = match someEnum {
    X => "...",
    Y => "...",
    // ...
  };
Really the only reason I use `switch` so much in C is that there's not a lot of polymorphism. Otherwise I greatly prefer to have this logic internal to the thing I'm matching on:

  let name = someEnum.get_name();
But again, because `match` is the way Rust does everything, look it's another match expression.

> (I also forgot to mention ternary ?: in C: it's also partly an acknowledgement that if-as-a-expression is useful.)

Aha well, as you might imagine I really dislike the ternary -- in really any language. They're hard to read, hard to edit, too easy to make a mess... basically all my arguments about `match`. Honestly just use an if.

I don't want to keep repeating myself, but my main beef isn't readability or whatever. It's that I don't understand what it gets me beyond using if. If ternaries somehow (really) solved a NULL problem, or helped me with error handling, or let me avoid use-after-free, then those are all great and welcome tradeoffs. But all it does is save a couple of lines, and because my problem has never been that I had to use a couple extra lines I just don't care about features where that's the sole virtue. I need more.

> I don't see much upside to emulating C-style manual tagged unions here.

Well I guess my point is mostly that I want to differentiate between matching on a value and implementing inside-out polymorphism. I don't like that they're smashed together in match because I think they're very different, and it leads to a lot of mixed up logic inside match expressions. The point isn't to emulate tagged unions so much as it is to separate concerns.

> [1]: This is a point where other people would complain about breaking with other practice for no good reason too (anyone who had used Haskell or OCaml or similar would find this system unnecessarily clunky).

And I would totally agree with them if Rust were a language in the vein of Haskell or OCaml, but it's not; it's a mainstream systems language. If Rust's pitch were, "Hey, do you like Haskell? You'll _LOVE_ Rust!" then they'd have a valid complaint. But Rust's pitch is, more or less, "Hey are you tired of C/C++ pitfalls or slow Python/Ruby/JS code? Give Rust a whirl!" When optimizing for familiarity and shallow learning curve you gotta pick an audience; you can't have it both ways.

> Focusing on a single value also doesn't generalize/scale: for instance, if one is making a decision that depends on more than one value:

  fn maybeAdd(x: Option<i32>, y: Option<i32>) -> Option<i32> {

    match (x, y) {
      (Some(left), Some(right)) => Some(left + right),
      (Some(left), None)        => Some(left),
      (None, Some(right))       => Some(right),
      (None, None)              => None
    }

  }
Oooooh, I think this is a very good point, but honestly this is just half-hearted polymorphism. These two enums should be in a struct, and this logic should be in its implementation. Then I think it's fine to do something like this:

  def maybe_add(self):
      if self.left and self.right:
          return self.left + self.right
      if self.left:
          return self.left
      if self.right:
          return self.right
I mean, I don't want to pick apart a clear hypothetical example. My point is that we already have a tool that can handle those scaling concerns: if and encapsulation/polymorphism. And they're much, much better than match because they scale to more than 3-4 variables; past that and match is just terrifying.

> I've personally found being exposed to many very different languages has helped inform the code I write in all others. Each new one definitely takes some getting used to, but my experience is that having touched several different paradigms has made me both more flexible (less attached to any particularly way of doing things) and a deeper understanding of even the "boring" languages: the trade-offs and "whys". There's value to things being the same, but there's also value in being able to break out of that mold and doing new things even if there's not an obvious benefit when focused on the old style.

Hah, tell me about it! You should've seen me the day I discovered there were different "types" of numbers in C (coming from Python)! There's a lot to learn, no question.

But there's a reason behind C's proliferation of numeric types, one that aligns with its purpose. I don't dislike `match`, etc. because I'd never worked with it before. I dislike it because it encourages so many bad practices in order to solve a problem I never had.

> I hope that you find Rust more and more enjoyable as you use it more

I do actually. Maybe you're getting this, but I identify more as a grump when it comes to programming so I gravitate towards grumpier languages (C, Go, etc.) so Rust's eagerness about some of the ML stuff is a little off-putting. But really, compared to integer promotion in C like, `match` is nothing :)


> To be honest, I really think you're blowing the complexity of this way out of proportion. Plus like, it's not like Rust's grammar is this incredible thing of beauty: look at `where` clauses or lifetime syntax. If Rust were really optimizing for human readability, it's hard for me to imagine this is the result. It's purely a style thing, just like `let` instead of `var` and so on, and I don't think sacrificing programmer familiarity for a designer's idea of style is a good tradeoff.

Having a nice grammar in this respect is more of a technical beauty and elegance than an aesthetic one, but it translates into simpler tooling and so on, which can be an aesthetic one. To be honest, I also think you're blowing the value of having "(Type)value" syntax way out of proportion. It's not like Python or JavaScript use it, and everyone seems to cope fine, and even C++ (theoretically) prefers the far more clunky static_cast<T>(...).

> Otherwise I greatly prefer to have this logic internal to the thing I'm matching on:

Oh, yeah, obviously abstracting out into functions for common functionality is better than ad-hoc matches, but there's lots of cases where that's just overhead and fiddly and less clear. People already complain a lot about Rust requiring needless ceremony and being unergonomic, and encouraging people to go through the ceremony of defining functions for little things seems to be going against that.

> Do you hate it enough to pull all the statements out of a language?

Rust has statements: match, if and the loops can be used as statements, and you're actually free to write your assignments C/C++ style (type inference even works for it):

  let name;
  match someEnum {
    X => name = "...",
    Y => name = "...",
    ...
  }
  doSomething(name)
Moving the "name =" out of the match seems like a tiny step that makes the code less noisy and nicer. But, it is a style preference.

Rust has not pulled statements out of the language, just upgraded things from "always a statement" to "can be an expression".

> I don't want to keep repeating myself, but my main beef isn't readability or whatever. It's that I don't understand what it gets me beyond using if. If ternaries somehow (really) solved a NULL problem, or helped me with error handling, or let me avoid use-after-free, then those are all great and welcome tradeoffs. But all it does is save a couple of lines, and because my problem has never been that I had to use a couple extra lines I just don't care about features where that's the sole virtue. I need more.

The value of 'match'es for things where 'switch' or 'if' are reasonable is being declarative and uniformity with the cases where 'switch' and 'if' are not reasonable. It's fair that it's unfortunate that things can be misused to create confusing code, but I don't think that's a reason to remove something in and of itself, or else a language would have to be extremely small.

> Oooooh, I think this is a very good point, but honestly this is just half-hearted polymorphism. These two enums should be in a struct, and this logic should be in its implementation. Then I think it's fine to do something like this:

What do you mean by polymorphism?! Just being able to have "None" or a value in the same variable as in Python?

Contesting every example by saying that it should just be wrapped up into a struct/function is... kinda missing the point. You still end up with that code somewhere (although it's true for the name example being in a function means 'return' works), and, you end up with a ridiculous number of near-pointless functions and types. In any case, there's not nearly enough context to say that that example should be wrapped up into a type: what's the connection between the two values other than that they should be added? There's a reason no-one proposes writing `a + b * c` as

  (Add {
    left: a, 
    right: (Multiply { left: b, right: c }).doIt()
  }).doIt()
And this random example is only one step above plain arithmetic in terms of abstraction.

In any case, your proposed variant is... not a good version of my code. There's nothing stopping `if self.left: return self.right` (oops!) and it fundamentally doesn't fit with Rust's approach to conversions between types. As I said earlier, adding the type system features to defend against the first thing ends up, in the limit, being more complex than (but pretty similar to) having 'match' and using the existing type infrastructure. This is relevant to Rust's goals, both in being a more reliable systems programming language in general, and also for safety: with move-only types and stronger references, guaranteeing safety but still being useable I think would mean having a lot of that complicated infrastructure.

> And they're much, much better than match because they scale to more than 3-4 variables; past that and match is just terrifying.

I strongly disagree that 'if' scales to more than 3-4 variables, and that 'match' scales worse.

'match'-without-'if' is more restricted than 'if' and so is easier to understand (understand at a high-level): if I see a match on some variables, I know that it's going to be looking at those variables immutably, and structurally. The state of the variables at the start of the match completely determines which code runs. (And, but I guess you probably disagree with this, even with 'if's on arms, there's still not less structure than a plain sequence of 'if's. And, this lack of structure is clearly flagged, whereas with an 'if' chain, everything looks the same.)

But, with an arbitrary sequence of "if"s, it's a free-for-all, anything could happen up to and including mutation of things queried later, there's no static checking that I'm interacting with the variables I want to, and there's no single source of truth for what those variables even are (like the ... in match ... {), and there's no checking for things like handling all the cases. "if" having more power and so being worse is exactly the same reason that "goto" is frowned upon: it is too flexible and so too hard to understand. The same reasoning can be seen in C's 'switch' versus "if (x == Value) ... else if (x == OtherValue) ...".

It's true that an sequence of 'if's with lots of variables doesn't look particularly different to one with only a few variables (unlike Rust's match), but this is deceptive: it's going to at least as hard to understand what that if/else if chain is actually doing, not what it seems like it is doing, even in idealised cases (and, for fairness, if you're thinking the worst case of 'match' with @s and ifs, one really should be thinking of the worst cases of ifs).

---

Anyway, wrapping up this thread, you've convinced me that Rust could be a little more minimal ('match' doesn't need @ or 'if', and the convenience of everything-is-an-expression isn't needed), but I don't think there's even close to "50x" space for simplification.

There's a lot of consistency between various parts without many clunky interactions, which, I find, is where the most annoying complexity in programming languages appears. A lot of different to a major component of the audience, but a lot of that difference is bringing in conveniences from the last few decades of programming language research/experimentation.

It might be an interesting experiment for you to take a moderately large Rust program and convert it into "MISRust" (ala MISRA C, or maybe "misfit Rust" :P ), that doesn't use the C statements as expressions anywhere and just does single level 'match's without ifs or @s everywhere (etc.) just to see what it looks like. I suspect it wouldn't even be too hard to write a clippy-style lint that enforces all those rules.


> Eh, I disagree...

The reason transmute is (particularly) unsafe is because it can be quite difficult to tell whether a variable is bound to a value, a (possibly implicitly-dereferenced) reference or a slice, and so, given transmute's implicit type inference, it's very easy to quietly get either the source or destination type very wrong.

The size-checking is also I would argue useless bordering on worse than useless (false sense of security) since any two references or two slices are the same size, but are unlikely to actually be interconvertible, and it also fails to check alignment.

C and C++ (particularly the latter) make it much more difficult to inadvertently cast between values and pointers, and the strict aliasing rules (aliasing almost always illegal except through `char`) are draconian enough to discourage the practice of type-punning-through-indirection (and ensuing alignment bugs) altogether.

`memcpy` (and unions-as-implemented) is nice because it's explicit and it just works.

> I'm curious if you've got a defect report link or similar

I'm having a hard time finding the detailed original paper pointing it out, but see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p059...


> The reason transmute is (particularly) unsafe is because it can be quite difficult to tell whether a variable is bound to a value, a (possibly implicitly-dereferenced) reference or a slice, and so, given transmute's implicit type inference, it's very easy to quietly get either the source or destination type very wrong.

Ah, so just the type inference I mentioned? I agree, and certainly try to never let transmute infer when I use it.

However, I don't think one can end up with implicit dereferences or wildly unexpected types since there always has to be some context: the return value of transmute is completely unconstrained, meaning there needs to be something that suggests the type.

Additionally, C++ can suffer in a similar way (but not quite identical) due to auto:

    auto source = ...;
    T dest;
    memcpy(&dest, &source, sizeof source);
Or, in extreme cases: `auto dest = ...;`.

> The size-checking is also I would argue useless bordering on worse than useless (false sense of security) since any two references or two slices are the same size, but are unlikely to actually be interconvertible, and it also fails to check alignment.

... C-style memcpy is strictly worse than all of this.

But yes, maybe a false sense of security. But that's a bit like arguing that a safety guard on a buzz-saw doesn't stop all problems, and so is pointless.

I agree that failing to check alignment of pointer destinations is unfortunate, but it's only one of many many problems that can occur with `unsafe` and even `transmute` itself.

However, it's worth nothing that differing alignment between values being transmuted is not a problem: transmute::<[u8; 4], u32>(byte_array) is fine, despite the byte array only having alignment 1.

> C and C++ (particularly the latter) make it much more difficult to inadvertently cast between values and pointers, and the strict aliasing rules (aliasing almost always illegal except through `char`) are draconian enough to discourage the practice of type-punning-through-indirection (and ensuing alignment bugs) altogether.

I don't agree:

- there's little difference between C/C++ and Rust other than the inference thing. C++ has C-style casts, and reinterpret_cast.

- strict aliasing ends up being commonly violated for transmute-style casts (it's just so temptingly easy, and there's nothing that actually stops it compiling), and `unsafe` in Rust is also a fairly major discouragement to doing bad things.

> `memcpy` (and unions-as-implemented) is nice because it's explicit and it just works.

transmute is only fractionally less explicit (and it's well known bad practice to let it be completely implicit), and also just works.

> I'm having a hard time finding the detailed original paper pointing it out, but see ...

Thanks; interesting!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: