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

FWIW, there's a slightly more concise way of expressing this in Go:

    type state int
    
    const (
        on state = iota
        off
    )



I don't think the concern was verbosity so much as lack of type safety.


After using Go for ~5 years, so many little things in Rust blew me away. I thought I was okay with Go's "enums" but then I saw Rust's Enums. Most notably, the real enum type combined with pattern matching was an eye opener over what I've been missing.

Iterators were another one. The ability to express data transformation (mapps/filters/etc) in a very concise way blew me away. I had no idea what I had gotten used to in Go, though that's definitely not to say that I didn't feel the pain.

There's advanced features in Rust I could live without, but most of it just feels empowering. The beauty in it though, in my mind, is that you don't need to use all that advanced stuff. You can write Rust shockingly similar to Go.

The only thing Go truly nailed in my eyes is green threads. Those will always be better in Go than Rust (though futures are getting way better). Go nailed green threads.

But all the other "lack of features as a feature" left me frequently wanting for more tools to solve simple problems. And I was a Go nut. I have a Gopher plushie in my car for Petes sake.


"You can write Rust shockingly similar to Go."

This applies in any case where one language is more complex than another. You can write almost any style of any language in C++, for example.

The problem is that every team ends up writing in their own subset of these languages, which means it's impossible to ever really achieve expertise. Each team's definition of the language is different, and no one has worked on every team. Ergo no one in the world is actually a C++ expert at any given company's "version" of C++, even if you know every C++ feature independently. You have to follow the style guide which tells you what subset of the language to use and how to use it. This isn't an insurmountable problem but it is a problem. Rust has the same issue.

With Go, everyone can feel free to use the entire language and every team's code ends up looking and feeling incredibly familiar, making it straightforward to contribute to most parts of any code base.


> This applies in any case where one language is more complex than another. You can write almost any style of any language in C++, for example.

True, but my point was primarily that Rust and Go share many of the same patterns. Structs, methods and interfaces can look nearly identical.

This matters in my view when people think you need to go the most efficient and complex way to achieve similar goals as you might in Go. You were fine with performance loss in Go, so why complicate your life in Rust?

It's, at least to me, a useful lesson. Using every tool is a form of premature optimization. Go forced me not to do that, sure. Rust doesn't, sure. So I do hold more responsibility in Rust than I do in Go, but that doesn't mean I can't learn the positives of Go _(boring code/etc)_ without suffering some of the extremes of their decisions _(no enums, pattern matching, etc)_.

> With Go, everyone can feel free to use the entire language and every team's code ends up looking and feeling incredibly familiar, making it straightforward to contribute to most parts of any code base.

Yea, it's a trade-off I suppose. My problem with that though is when I realized I don't like Go's version of verbosity and spreading out logic. I've had pages full of helper functions just to do some minor iteration mapping, flattening, etc.

Having every team keep to the same standard of _(in my view)_ bad still feels bad. Consistent, sure, but consistently bad.


> With Go, everyone can feel free to use the entire language and every team's code ends up looking and feeling incredibly familiar, making it straightforward to contribute to most parts of any code base.

And it goes a step further. I read Go library code and it looks just like code I would have written. It's easy to understand and makes sense.


I came to go from C++ and was so glad about how few sharp objects were lying around, working in software with just one allocator, just one string type. I’m possibly damaged by previous experiences but ultimately I’m also glad not to have to pretend I’m clever. I just go brrr and solve the problem with for loops and go home to my loved ones at 5:30 exactly.

Like proper enums might be nice I guess, but really what I love are all the other things which are not there. Lack of enums has not hurt me deeply. What hurt me was languages where someone might conceivably express the COM apartment model and also think it was a good idea.


Do you somehow labor under the impression that Rust programmers stay up at nights and on weekends to learn to use "clever" tools like iterators, enums, and sum types?

These aren't clever tools. They're dumb tools like chisels and hammers. Yeah, you can just use a screwdriver to chip things away or to whack something in, but there are better tools that are purpose-built and let you do the job faster, more precisely, and with less effort.


> The only thing Go truly nailed in my eyes is green threads. Those will always be better in Go than Rust (though futures are getting way better). Go nailed green threads.

If you think Go's green threads are great, you should see erlang's. In erlang if you need to construct a mutual failure domain (have two green threads mutually destruct if one fails) you can use the link() function. Done. You can also trivially choose to have one of them be notified on the failure of the other instead of being destroyed.


The crazy thing is that sum types (Rust enums) and pattern matching have been around for at least 30 years. I'm simply not interested in learning any new language that doesn't have sum types, they allow you to write incredibly expressive and terse code.


Algebraic datatypes started with Hope, in the late '70s at Edinburgh. Rod Burstal, David MacQueen and Don Sannella.

https://smlfamily.github.io/history/SML-history.pdf

https://dl.acm.org/doi/10.1145/800087.802799


For all the complaints about the breakneck change in rust, it's a very "boring" language: all of its features with the single exception of the borrow checker already exist in other languages.


I know this won't be popular to say on HN, but I think Rust has the same problem Perl does. The language has so many systems to learn and a tough syntax that it looks unreadable to people just starting out.


I mean I could see learning Rust being really hard if you only know something like Python or JS. The only "system", as you say, that is present in Rust that doesn't have something analogous in C++ is the borrow checker, and lifetimes still exist in C and C++. Rust is significantly simpler and easier to learn than C++


C++ has a much shorter time to first non-"hello world" program than Rust. C++ has a lot of features, but few of them are mandatory for general development. With Rust you have a pretty steep hill to climb before your first non-trivial program compiles.


C++ and Rust, IMO have a very similar featureset, Rust just puts that upfront as properly part of the language. Those C++ features are pretty much mandatory for general development, and likewise you will find them in most open source and production projects. Programming without them is just C++'s one of many ways that it gives you enough rope to hang yourself.

Yes, you could program C++ without even knowing what std::unique_ptr (and I talk to many college grads with C++ on their resume who don't know what unique_ptr is, or that C++ has more than one type of pointer). But Rust won't let you use raw pointers (as part of the language), whereas in C++ you will be told "make sure you have read Google's 10,000 word style guide before committing any code".


I seriously doubt that. It takes serious effort to learn a whole other language (cmake) to do anything useful in C++.


I strongly disagree.


I believe https://news.ycombinator.com/item?id=23715759 works as a response to your point. In my eyes syntax is the least interesting thing of any language, their semantics are way more important, and quite a bit of syntax ends up being derived from it, and the rest boils down to aesthetics. The syntactic complexity that Rust has is there because it is encoding a lot of information, modulo things like "braces vs whitespace blocks" and "<> vs []" which, again, come down purely to style. Also, having a verbose grammar is useful for tools like the compiler and IDEs because having lots of landmarks in your code aids on error recovery and gleaning intent from not-yet-valid code.


It's not any particular feature that makes a language a mess. It's the interaction between the features. It's a bit like mixing paint, it's very easy to end up with greyish poop.

Go was designed by very experienced programmers that understood the cost of abstraction and complexity well.

They didn't do an absolutely perfect job. It's probably true that Go would be a better language with a simplified generics implementation, enums, and maybe a bit more. That they erred on the side of simplicity shows how they were thinking. It's an excellent example of less is more.

Most programmers never gain the wisdom and/or confidence to keep things boringly simple. Everyone likes to use cool flashy things because it makes what can be a boring job more interesting.

But if your goal is productivity, and the fun comes from what you accomplish, then the code can be relatively mundane and still be very fun to write.


> It's the interaction between the features.

Precisely, and this is one area where go fails completely. The features don't interact well at all!

Tuple returns are everywhere, but there are no tools to operate on them without manually splitting the halves, checking conditionally if one of them exists, and returning something different based on each possibility. Cue the noise of subtly-different variants of `if res, err := nil; err != nil` in every function.

Imports were just paths to repositories. Everything was assumed to just pull from the tip of the branch, and this was considered to be just fine because nobody should ever break backwards compatibility. They've spent years trying to dig themselves out from under this one.

Everything should have a default zero value. Including pointers. So now we go back to having to do manual `nil` checking for anything that might receive a nil. But thanks to the magic of interfaces, if you call a function that returns a nil interface pointer, it will directly fail a nil comparison check! This is completely bonkers.

Go has implicit implementation of interfaces which makes exhaustive checking of case statements impossible. So you type-switch and hope nobody adds a new interface implementation. So you helpfully get strong typing everywhere except for the places you're most likely to actually mess something up.

Go genuinely feels like a language where multiple people each had their pet idea of some feature to add, but nobody ever came together to work on how to actually make those features work in concert with one-another. That anyone could feel the opposite is absolutely incomprehensible to me.


Given that I am involved in the Rust project I'm very likely biased, but given that I've focused on the learnability of the language (diagnostics and ergonomics) I have a bit of context on this subject.

When designing a language there are intrinsic (what things the project wants to focus on, be they features of the language or the associated tooling that affect the language, like generics or compilation speed) and extrinsic (external impositions like being able to run on certain platforms, or interfacing with existing technologies like being able to run a statically linked binary in Linux or being able to debug using gdb or calling C libs without runtime translation) design constraints. All languages have (or should have) an objective of being easy to learn, pick up and use long term. It might just not be the top priority.

For the sake of argument you can take Python where expressiveness at runtime and clean syntax are prioritized over speed, Go where fast compilation and multithreaded microservices are prioritized over more complex language features, and Rust where fast binaries and expressiveness are prioritized over ergonomics (when push comes to shove this is the case, otherwise you wouldn't need to call `.clone()` or add `&` to arguments when calling a method ever), you can see how these objectives permeate every decision throughout the language.

When it comes to Rust in particular, I feel it is still a boring language despite the appearance of too many features, precisely because of how they interact between them and fit together naturally. It is not the best fit for every use case, but it is one of the projects out there that is embracing the fact that it can't be as easy to learn as it could be (without sacrificing some of the constraints that make it interesting as a systems language), but we can rely on the compiler being a necessary part of the developer toolchain to make the compiler understand the user's intent when they do things that make sense from extrapolated misunderstanding of the language and help them write the "correct" code instead. This has the added benefit that reading the code is easier because you have to "guess" much less what it is doing. Remember that if the code can confuse a parser it will also confuse humans. On the opposite end of the spectrum you have JavaScript, where it's grammar has a lot of optional or redundant ways of doing the same thing (think semicolon insertion), which makes the act of reading and debugging code harder. This is a reasonable approach in a case like the web, less so in a compiled language that can evolve independently from the end users' platform.


The thing it nails is that mediocre programmers (like me) can easily understand and reason about source code written by others. In the C++ world, this can be very hard and thus time consuming. Go on the other hand let's me focus on the business value as opposed to becoming a language lawyer.


True until the code blows up to extraordinary sizes because of lack of expressive features.

Rust code is incredibly easy to understand. Far, far easier than C++ in my opinion.


It is type safe. Any integers with a defined type cannot be used as values of type "state" without an explicit conversion occurring.

Type safety does not guarantee exhaustive matching, unfortunately, because the underlying type is still an integer, but that's a separate issue.

https://play.golang.org/p/K0m4hfmw8C1

I wish Go had proper, exhaustive enums too (sum types preferably), but you're incorrect when you say that they don't have type safety.


You're right that you can't mix named types with the base type.

However, they are not enums in any way either, since there is no limit on their possible values to some restricted set - they really are just integers, even more than in C# or C++, even though there is no implicit conversion.


> they really are just integers, even more than in C# or C++, even though there is no implicit conversion.

What do you mean by this?

C#: https://repl.it/repls/ExhaustedDisgustingRam

C++: https://repl.it/repls/BackGroundedRobot

C# and C++ offer exactly the same thing that Go offers... enums that are really just integers.

You can assign arbitrary values to those C# and C++ enums, even though you're not supposed to, just like in Go.

Rust offers something truly better, where the enum abstraction doesn't leak at all.


In C++, you get an error at runtime: https://godbolt.org/z/C_JFez

Note though that C++ allows using enums as bitfields, so some form of loading non-enumerated values has to be allowed.


> In C++, you get an error at runtime

Your statement implies that errors for using invalid values are guaranteed at runtime as a feature of the language. That entire statement is incorrect.

You really don't get errors... I literally pasted a link to a working, non-erroring example in the comment that you responded to. You clearly saw the code. Did you click "run"?

You only get an error at runtime if you add "-fsanitize=undefined" (or "-fsanitize=enum"), where the compiler will inject some code into your binary.

But the error doesn't actually stop code execution: it just prints a warning!

Here is a link with the sanitizer enabled, and no warning is even printed for using an invalid value: https://godbolt.org/z/NA7FNQ

So, not only is the sanitizer not even comprehensive, it's not actually a feature of C++. It's a best-effort feature from the compiler to add a non-standard runtime code sanitizer to your binary. Warnings for using invalid values are not guaranteed.


You had to use casts to get those to compile. Go will let you shoot yourself in the foot by accidentally writing "z = 3", which is way more likely than accidentally writing "z = (Something)3".


The point is that those languages’ enums are very much integers too. Do those languages allow you to write out as many types of integers literally in your code? No. But that’s not the point being addressed here. You can reread the comment I was responding to, and there’s no hint that I can see anywhere that they’re talking about untyped literals.

The point I’m making is that C# and C++ enums are not exhaustive, and not guaranteed to be any particular predefined value. They’re just integers. (unlike Rust: https://play.rust-lang.org/?gist=c9e499ba8d3abe08e5e54c74f54...)

C#, C++, and Go all share the same flawed enum representation strategy. Use Rust if you want to get better enums... those other languages aren't any better for enums. C#, C++, and Go enums are all type safe, and they're all perfectly capable of holding completely unexpected values. (Although, C++ in the older permissive mode was not type safe period when it came to enums, if I remember correctly. I admit it's been long enough that I could be misremembering.)

I already addressed your point in detail with you elsewhere.

Please, by all means review the final 3 lines I wrote in this comment for more info: https://news.ycombinator.com/item?id=23715897

I agree that being allowed to write any type of integer as a literal is theoretically an ergonomic issue when it comes to enums, but it’s one issue I’ve literally never seen happen even once in practice. I point out that there are linters available to address the issue if it’s one you feel so strongly about. Surely you use linters in every language?

I have been paid professionally for years to work in both Rust and Go codebases. Linters are essential for both languages, and CI is where you guarantee that no lint-failing code is allowed to pass.

I’m well aware of Go’s flaws, but people throughout this thread (yourself included) have made tons of baseless claims about Go. Just because it’s popular to hate on a given language doesn’t make it okay to use incorrect statements for that purpose, such as saying that Go’s enums aren’t type safe. They are a separate type. The language does allow you to write integer literals of any integer type. It also does this with untyped float and untyped string literals as well, as a fun fact.

Go has a number of legitimate flaws, including the absence of both generics and sum types. You could even legitimately complain about untyped literals —- they are really nice in some ways, but they do have some trade offs.


> also will compile, because integer literals are untyped until they are used

Doesn't that make them type-unsafe? In C++ or Haskell, implicitly assigning integer literals like that isn't valid.


> Doesn't that make them type-unsafe?

No, it doesn't. Go's enums are type safe. You can't accidentally mix two different kinds of enum values, or accidentally use some random value of type "int" where an enum value is expected. The type system protects you from values of the wrong type being used. This was demonstrated by that Go Playground link.

Literals in Go are untyped until they're used. How they're used determines the type, and then they have a very real type, with very real type safety being enforced. So, if you're using a literal "3" where an integer of type "state" is expected, the Go language specifies that the type of "3" is "state". This is an ergonomic issue when you're expecting exhaustive Sum Types, but not a type safety issue.

Should all literal integers just always be a specific type? Let's decide that all literals should be of type "int". Great. Now you can't type a large, 64-bit integer literal to pass as a value to a function, because that would overflow the "int" type, even though the argument is desired to be "int64".

There are trade-offs to every approach.

> In C++ or Haskell, implicitly assigning integer literals like that isn't valid.

I can't comment on how Haskell does things, but C++ is more complicated than you seem to think.

https://en.cppreference.com/w/cpp/language/integer_literal#T...

"The type of the integer literal is the first type in which the value can fit, from the list of types which depends on which numeric base and which integer-suffix was used."

Go's approach is equally type safe here. It assigns the type to the literal based on the expression the literal is used in.

As I said before: I really wish Go had proper Sum Types. But enums in Go are type safe, contrary to what you have claimed in several comments here.


I'm not saying integer literals shouldn't be polymorphic. I'm just saying that polymorphism should only extend to numbers, not enums.


A desire to restrict the polymorphism of integers is fine, but it doesn't really change the type safety argument at all.

"Enums" here are just another type of integer, exactly like in C# and C++. They're not a separate concept. I wish Go had Sum Types or even just exhaustive, non-integer (as far as the programmer knows) enums, but neither of those are requirements to have a type safe enum. The only difference vs C++ and C# is that Go has untyped literals, which are quickly handed a type based on where they're used.

Enums in Go have type safety, which is the point you disagreed with. You can't assign values of the wrong type to an enum-typed value without explicit conversion in Go. That's type safety.

There are linters that can be used if you want to enforce more restrictions: https://github.com/THE108/enumlinter

Linters that fit your team's expectations are a good thing to use in any language, and Go is no exception here.

Would this linter be unnecessary in other languages? Sure. I would argue it's still unnecessary, because I've never even once seen anyone accidentally type a literal value in Go where they meant to use one of the predefined enum values. It's certainly possible, but it hasn't caused me any lost sleep.


Fair enough. I would say if the way someone uses the Go "enum pattern" is causing them to have issues with type safety then their code could probably use a refactor, but the point does stand.


How is it possible to use the Go "enum pattern" without having issues with type safety? It's inherently not type-safe at all.


By "issues" I mean software defects.


`iota` is one of the strangest features in Go by far for me. So much complexity for such a simple feature with so few applications, instead of implementing even simple enums...




Consider applying for YC's Spring batch! Applications are open till Feb 11.

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

Search: