Go sucks not because it's accomodating to poor designs. It sucks because it is in itself poorly designed and that leaks over to the design of entire applications.
Why have functions return err, nil? Why even allow for a runtime error here? It's a really simple fix. You don't even have to make the language complex to support this. Instead the entire program is littered with holes and if statements you have to check in order to prevent a actual crash
Why not? It doesn't make any difference in practice. Without a complete type system you must write tests to ensure that error conditions (to stay with your example, although this also applies broadly) do what you need of them. If you somehow introduced a runtime error there, your tests would be unable to not notice. Whether your compiler cries or your test suite cries when you screw up is not a meaningful difference.
> You don't even have to make the language complex to support this.
A complete type system is insanely complex to implement and even harder to write against.
Without a complete type system, all you can have is silly half-measures. Maybe the error becomes an optional/result type with forced unwrapping, for example, but you still haven't asserted in the types what needs to happen with the error. So you still need to write the same tests that you had to write anyway. So, other than moving where you discover the problem – from your tests to the compiler – nothing has changed.
The half-measures are a cute party trick, I'll give you that, but makes no real difference when actual engineering is taking place. They might, however, give a false sense of security. They might even convince you that you don't need to write tests (you do). Maybe those make for desirable traits?
> Doesn't make any difference in practice meaning you never had a runtime error while running go?
I am not sure I have written enough Go to comment there, but I have worked extensively in other languages where runtime errors are possible, similar to Go in that regard. I have encountered runtime errors in said tests now and then, sure, but then you know about it and deal with it... So, in practice, no different than if the compiler told you that there is a possible runtime error.
> Who says you need a complex type system?
It is needed if you want to avoid the need for said tests. With a complete type system the type system can become your test suite, so to speak. But the languages people normally use, even those with "advanced" type systems, are nowhere near expressive enough for that. Meaning that you have to write the tests anyway. And then you'll know if there are any runtime errors as soon as you run your tests because how could the tests run without encountering the runtime error too? It is not like a CPU magically changes how it works if it detects that a test is being run. So, in practice, the type system doesn't change the outcome. But it is a cool party trick. I'll give you that.
That said, aside from these hand-wavy, make-believe stories, you are still very right that Go would benefit from sum types. For the reason that they map to the human model of the world very well, succinctly communicating structures that are often needed to be expressed. Languages are decidedly for humans. You can sort of work in the same basic idea in Go using interfaces, but it is far more confusing to read and understand than sum types would be. For a language that claims to value readability...
> So, in practice, no different than if the compiler told you that there is a possible runtime error.
If your program has runtime errors then that means you can deploy it to production and catch your errors in production.
If your compiler catches all possible runtime errors and refuses to compile. Then you will have no runtime errors in production guaranteed by proof. The program cannot even exist with runtime errors. It can only exist with no runtime errors.
So no difference catching errors in production vs. compile time? I beg to differ. Big fucking difference imho.
> It is needed if you want to avoid the need for said tests.
I’m referring to the fact that you don’t need a complex type system to design a language that will absolutely never have any runtime errors. You’re going off on a tangent here about how you need a type system to have less tests which is completely different from what I’m talking about. This entire paragraph you wrote here is like you’re responding to an irrelevant topic.
> I am not sure I have written enough Go to comment there
Honestly it seems that you haven’t just not written enough go. But basically any programming language . It seems that you’re not clear about runtime errors and you seem to have only encountered these types of errors during tests. So yes you don’t have much experience imho and rob pike deliberately targeted the language towards people like you.
> If your program has runtime errors then that means you can deploy it to production and catch your errors in production.
That questions: Why are you allowing your programs to be deployed when tests are failing? This is not a realistic scenario in the real world. Yes, you can invent contrived hypotheticals all day long, but it is meaningless. We've been clear that we are referring to practical settings.
But, but, what if there is a bug in your compiler that sees the runtime error slip through??? Who gives a shit? In some imaged world it may be possible, but it is not realistic. Not worth talking about.
> I’m referring to the fact that you don’t need a complex type system to design a language that will absolutely never have any runtime errors. You’re going off on a tangent here
What you are referring to is clear, but it cannot be considered in a vacuum. The alternative is to see the program keep trodding along, but do the wrong thing. In that case who cares if the program crashes instead? You're getting incorrect behaviour either way.
What you actually need is assurances that the program won't do the wrong thing top to bottom. That requires either a complete type system or, more realistically in the real world, testing. If you go the testing route, you'll know about any runtime errors when you run your tests.
> Honestly it seems that you haven’t just not written enough go.
There is nothing unique to Go here. Many popular languages suffer the same problem. But, if we want to place extensive Go experience as a requirement to speak to this then we have to defer to your experience. Perhaps you can choose an example of where you wrote code in Go that produced a runtime error, show us your tests, and explain how the condition evaded your checks and balances? – I'm fascinated to learn how your code ran perfectly while under test but then blew up in production.
> That questions: Why are you allowing your programs to be deployed when tests are failing? This is not a realistic scenario in the real world. Yes, you can invent contrived hypotheticals all day long, but it is meaningless. We've been clear that we are referring to practical settings.
Tests don’t catch everything. You can have a billion tests and there can still be uncaught runtime errors.
If you had a language that probably does not have runtime errors you don’t even need one test. Your program cannot fail in that way.
I honestly don’t think you know what you’re talking about. I didn’t make up a single hypothetical. This is real. Production errors can happen in spite of tests. Are you not familiar with this happening? It just means this: no experience.
Your compiler having a bug or not is orthogonal to the topic. Again you don’t know what you are talking about. If our compiler allows for runtime errors but is fully correct then no amount of tests can guarantee a runtime error will never happen. Golang as a fully correct compiler cannot be gauranteed to have no runtime errors with tests ever.
> What you are referring to is clear, but it cannot be considered in a vacuum. The alternative is to see the program keep trodding along, but do the wrong thing. In that case who cares if the program crashes instead? You're getting incorrect behaviour either way.
You’re writing this because you don’t have experience with programs that can never crash. A program that doesn’t crash doesn’t mean you never exit the program. The program can exit if you want it to. You just need to deliberately tell the program to exit. In golang if you do a division by zero, the program crashes. If you had sum types all divisions return an optional. Both paths of the optional must be handled by exhaustive matching so you must handle the case where the division yields a number or its undefined. If you want the program to exit when it is undefined you can do so. In golang the compiler doesn’t force you to handle both outcomes It just crashes. It’s the same with out of bounds access of an array.
Again real world testing doesn’t guarantee shit. A “complete type system” can be as extensive as dependent types like COQ or much simpler like rusts where you just have sum types and exhaustive pattern matching.
> Perhaps you can choose an example of where you wrote code in Go that produced a runtime error, show us your tests, and explain how the condition evaded your checks and balances?
Oh easy. we had a function that calculates velocity from a stream of input data. That’s (p2 - p1 / t2 - t1). Our integration tests and unit tests have dozens of tests that never yielded an error and we never saw an error in production for years. We switched to a new iot device that sometimes sent identical measurements to our system. Division by zero. We had a crash in production.
> I'm fascinated to learn how your code ran perfectly while under test but then blew up in production.
You’re inexperienced that’s why you’re fascinated. If you have formula involving velocity there an almost infinite amount of combinations that will never produce a runtime error and an infinite amount of parameter combinations that do. True full coverage that completely proves the function works with tests involves infinite tests. Better to prove the function works via proof with a simple extension to the type system. Sum types.
Again, they will catch your runtime errors if your behaviour is covered. If your behaviour isn't covered, then you're just shifting the problem to the program doing the wrong thing instead of crashing. That is not a win. It might even be worse! So, this doesn't matter in practice. Your purely academic view of the world doesn't work with the discussion taking place, I'm afraid.
> If you had a language that probably does not have runtime errors you don’t even need one test.
Go on.
Here, let's use your example:
> That’s (p2 - p1 / t2 - t1).
Traditionally, the calculation is (p2 - p1) / (t2 - t1). I'll assume you had a non-standard situation that necessitated a different formula, but this could have equally been a mistake. It wouldn't be too hard to forget the inner parentheses. We'll assume that divide by zero was already eliminated by the type system, but now show us how sum types would avoid someone from making that mistake.
Maybe you did need tests after all...
> You’re writing this because you don’t have experience with programs that can never crash.
Not so. I spend most of my days writing code in programming languages that do provide such guarantees. It is a cool party trick, but doesn't really matter at the end of the day because they still don't offer the expressiveness to ensure that the program does what is expected of it. I still have to write tests, and once I've written the necessary tests to ensure all the behaviour is correct, there is no practical way you can miss a crash situation.
> Our integration tests and unit tests have dozens of tests that never yielded an error and we never saw an error in production for years.
And had you avoided the crash you'd get erroneous results from the function instead. You're not really any farther ahead. You still need assurances that the function actually behaves correctly. And if you had those assurances, you'd have caught the divide by zero condition.
You're not going to convince me that a complete type system is academically better. I already agree with that. But absent a complete type system, you're going to have to resort to tests. Once you've written those tests, you're going to uncover the runtime errors anyway.
> Division by zero. We had a crash in production.
Your fuzz tests never tried passing in values that would lead to division by zero? For such a simple function that has many possible states that can lead to that condition, that seems completely inconceivable. Hell, I just tried it for fun and it found the issue in less than 100 tries! This must have been the time you were talking about where you deployed to production without running the tests?
But let me be try to be clear: The compiler warning you that you haven't considered a division by zero case does not mean you've handled it correctly. Absent a complete type system, you still need tests to ensure that the behaviour is consistent with expectations even in those edge cases. But with those tests, runtime errors can't go unnoticed anyway, so you didn't really need the type system.
> Better to prove the function works via proof
Agreed. Complete type systems are unquestionably better theoretically. Writing tests is tedious. But it remains that with the languages people actually use, even those with "advanced" type systems, you can't prove much. You have to fall back to testing, and at that point you're going to uncover the runtime errors too.
>But, again, they will catch your runtime errors if your behaviour is covered. If your behaviour isn't covered, then you're just shifting the problem to the program doing the wrong thing instead of crashing. That is not a win. It might even be worse! So, this doesn't matter in practice. Your purely academic view of the world doesn't work with the discussion taking place, I'm afraid.
So you're saying write tests that cover every possible behavior. Makes sense right? It's like saying write code without any bugs. Simple! You're not getting it. You can go run around telling people to write tests that eliminates 100% of bugs and that if you think that will eliminate all bugs from the world, well you're just not experienced.
>Go on. To continue with the original example, I have a function that tries to write to a file. If that fails, the caller is to try to write to a file on a different device. If the caller does anything else the program is broken with serious consequences and should not be shipped to production. Express that expectation using sum types. Hell, express it using any type construct available in popular languages. Good luck!
You can do this on rust. Literally it's the core of the rust sum type system. Good luck? Have you done basic programming with rust? Here's some psuedo code:
match getFile(fileName) {
Some(file) => do someghing
Error => match getFile2(fileName2) {
Some(file) => do something
Error => exit()
}
}
The above is psuedo code. The thing with the match operator is that the program DOES not compile if you do not handle both SOME and ERROR. For golang you can handle the Some and then it crashes if there's a problem. You aren't required to explicitly handle it.
>Not so. I spend most of my days writing code in programming languages that do provide such guarantees. It is a cool party trick, but doesn't really matter at the end of the day because they still don't offer the expressiveness to ensure that the program does what is expected of it. I still have to write tests, and once I've written the necessary tests to ensure all the behaviour is correct, there is no way you can miss a crash situation.
There is a way. You're just not getting it. there's about infinite ways to crash a program that has runtime errors.
>Your fuzz tests never tried passing in values that would lead to division by zero? For such a simple function that has many possible states that can lead to that condition, that seems inconceivable. Hell, I just tried it for fun and it found the issue in less than 100 tries! This must have been the time you were talking about where you deployed to production without running the tests?
Hell I used a programming language with no run time errors and I didn't write a single test. Amazing! There are tons of functions complex enough such that your fuzz test will miss it. Again we had this code working for years because we implictly assumed said devices will never pass duplicate data.
Also we don't write fuzz tests. We just do basic testing. Fuzz testing is something our start up doesn't have time for. We would prefer guarantees without the need of extra testing/work/time in this area.
>Traditionally, the calculation is (p2 - p1) / (t2 - t1). I'll assume you had a non-standard situation that necessitated a different formula, but does serve as a great example of how behaviour is your real concern. One could easily input the formula you gave where they expected (p2 - p1) / (t2 - t1) and sum types wouldn't care one bit.
Nope your formula is correct. I just assumed you were intelligent enough to know what I meant even though I didn't put in all the parenthesis (I'm typing on my phone afte rall). I thought wrong.
>Agreed. Complete type systems are cool. Writing tests is tedious. But we await your proof to my case for a realistic setting where one uses a typical production programming language. If all you have is silly half-measures that only cover a small number of cases, you're not really proving much. All you are doing is giving yourself a false sense of security.
Yes rust. Jesus. You're so inexperienced you don't even know when it's standing in front of your face. You don't need the borrow checker from rust. You only need the sum type. Then take the sum type apply it to division by zero and out of bounds array access and all IO calls. Boom that's it. No more runtime errors.
>There's a good way to change that. Let's see your code!
just look at elm man. Yuo don't even know what I'm talking about because you literally don't have experience. You want to see code that never crashes? Get some experience with Elm and you'll see why it never crashes and you'll see it doesn't take a "complete" type system to make it that way. Elms type system is woefully simplistic.
Rust is like 80% of the way there... the reason why people don't use it is lifetimes and the borrow checker and the complexity associated with it. Additionally rust left some holes so it can crash (like division by zero), but it has all the primitives needed to prevent it.
> Good luck? Have you done basic programming with rust?
Have you? I am sorry that the good luck did not shine upon you. With the types remaining intact, I modified the (pseudo)code:
match getFile(fileName) {
Some(file) => do someghing
Error => do something else unintended
}
It still compiles. You failed.
> The thing with the match operator is that the program DOES not compile if you do not handle both SOME and ERROR.
But as you come off your hubris, you can now see above it will compile even when you screw up the error handling. So you haven't gained anything. You still need to write tests to ensure that you are actually doing the right thing. And once you've ensured you are doing the right thing, how do you think crashes are going to go beyond that? Right. Not going to happen.
> Fuzz testing is something our start up doesn't have time for.
Testing, is testing, is testing. If you have time to write tests, you have time to write fuzz tests where they are appropriate. To throw a test out the window just because it has a slightly different execution model (as provided by the language; not something you have to build yourself) is bizarre. In fact, in your case it seems it could have supplanted the other tests you wrote, actually saving you time not only while writing the code, but also later when you had to waste time dealing with the issue. Time clearly isn't as constrained as you let on.
> For golang you can handle the Some and then it crashes if there's a problem. You aren't required to explicitly handle it.
Technically, in Go 'Some' should always be valid, regardless of whether or not there is an error. That is a fundamental feature of the Go language. Given (T, error), the values are not dependent. You don't need to explicitly handle the error. That is a huge misunderstanding. The same would not be true in Rust, which does consider them to be dependent by design, but Go is a completely different language. You can't think Go as being Rust with different syntax. There is a lot more to languages than the superficial.
>Have you? I am sorry that the good luck did not shine upon you. With the types remaining intact, I modified the (pseudo)code:
Your modification makes no sense. "do something else unintended" Wtf does that even mean? What are you doing? Why don't you spell it out? Because in golang you can do this:
v, err := getFile(fileName)
v.read()
And that's a fucking crash. You understand examples are used to illustrate a point right? And that your example shows you missed the point. Hey why don't I insert some psuedo code called "blow up the earth" in my program and that disproves every point ever made by anyone and I'm right. Genius.
>But as you come off your hubris, you can now see above it will compile even when you screw up the error handling.
Think of it like this. The point I'm illustrating is that in rust, you have to handle an error or the program won't compile. In go, you can forget to handle an error and your program will compile. You're going to have to write a bunch of tests to only POSSIBLY catch a missed error handling case. Understand? I don't think you do.
>Testing, is testing, is testing. If you have time to write tests, you have time to write fuzz tests where they are appropriate. To throw a test out the window just because it has a slightly different execution model (as provided by the language; not something you have to build yourself) is bizarre. In fact, in your case it seems it could have supplanted the other tests you wrote, actually saving you time not only while writing the code, but also later when you had to waste time dealing with the issue. Time clearly isn't as constrained as you let on.
It's not bizarre. It's again, lack of experience from your end. Why do I want to spend time writing generic test code that executes off of fuzzed input? I can write test specific code for specific use cases and that's much faster to write then attempting to write tests that work for generic cases.
Also how about not writing tests all together? I mean that's the best solution right? Honestly not to be insulting here, but it's not at all bizarre that you're not seeing how a better type system is better then tests that check for runtime errors. The root of it is that you're just stupid. Like why jump through a bunch of hoops and just call what I'm saying "bizarre" and just be straight with me. We're both mature right? If I think you're truthfully stupid and you think of me the same, just say it. We can take it. Why dance around it by calling my points "bizarre". No your points aren't "bizarre". They are stupid and wrong.
>Technically, in Go 'Some' should always be valid, regardless of whether or not there is an error.
That's why go is bad. You don't need to handle an error if err is not nil. V will be a nil here. And you know what's the only thing you can do with a nil besides check if it's a nil? Crash the program. Literally.
With rust, you can do this:
match getFile(fileName) {
Some(file) => do someghing
Error => {}
}
and do nothing. Which is the same effect as golang. But rust at least tells you to explicitly watch for it.
>You can't think Go as being Rust with different syntax. There is a lot more to languages than the superficial.
It's not about what I think of the language. It's about the intention of the designers. Go was made for people with not much experience. Straight from the horses mouth. Pike is saying he designed it for you.
>That is a fundamental feature of the Go language.
I think you're kind of not getting it. Seriously like the feature of golang is to allow you to unintentionally crash the program and you think that's a good thing?
v, err := getFile(fileName)
doSomething(v)
Take some time to think here. I know you think you're smart, but you need to hit the brakes for a second. Think: What is the purpose of the above code? If err actually is not a nil, and v ends up being a nil. What is the purpose of this type of logic to even exist? Is it for v to crash somewhere in doSomething? Are you saying that a fundamental feature for golang to crash somewhere inside doSomething?
Really think about this. You literally said it's a feature for golang to not handle an actual error and for v to still be "valid." So if err is not nil, v is a nil. What happens here? You think this is a feature? Or are you just not thinking straight? Just pause for a second.
Another thing to help you along: You know the inventor of the null/nil value called it his billion dollar mistake right? Have you thought about why it's a huge mistake? Here's a hint: You can't do anything with a null/nil except check if it's a null or crash the program by using it improperly. The existence of a nil/null signifies the existence of feature that you can only use to crash your program unintentionally.
Why doesn't elm crash? Why is rust safer then most languages? A big part of the reason is both languages don't have nulls or nils. And that doesn't have anything to do with a "complete" type system.
Hopefully you get it now. If not I can't help you.
The earlier comment already spelled it out. Hence why it wished you luck as we knew you would not be able to deliver. Yet right on cue you hilariously tried anyway.
I take that you haven't actually read anything in this discussion? If you have read it, you haven't understood it. Slow down, comprehend before posting. It seems you've become so fixated on telling us how sum types work – something we understood decades ago – that you have missed the forest for the trees.
> Why do I want to spend time writing generic test code that executes off of fuzzed input?
Remember when you mistakenly wrote (p2 - p1 / t2 - t1)? That's why. A complete type system would negate the need for testing there, but if all you have is sum types... There is no difference between a fuzz test and any other test other than the tooling will feed it 'arbitrary' inputs. It is not like they take more time to write or something. Testing is testing is testing.
In fact, it took me like ten seconds to write the fuzz test for your function when I tried it out earlier. This time argument is disconnected from reality. You have 10 seconds to spare to ensure correctness, especially when you admitted to writing a bunch of useless tests instead. One good test would have gone further and saved you time.
> I think you're kind of not getting it.
Let's change that. Consider your code:
v, err := getFile(fileName)
doSomething(v)
Presumably your requirements dictate that your code must do something when getFile fails, so you are obviously going to write a test for that scenario. How do you envision that potential crash condition evading the test? The CPU steps through until getFile fails, then checks if you are running a test and if it so it invents a valid file handle, but if it notices you are in production it returns a corrupt file handle and then crashes? That doesn't make any sense. What does make sense to you that explains how your code will pass tests but fail in production?
If what you are struggling to say is that you have no such requirement to deal with the failure so it is unspecified behaviour, and thus you didn't feel the need to write a test for it, all you can do with sum types, to keep the compiler happy, is purposefully panic. It is unspecified behaviour. Anything else but crashing would be nonsensical – something that is true even if it were written in Rust. Is the source of the panic significant?
> Seriously like the feature of golang is to allow you to unintentionally crash the program and you think that's a good thing?
It doesn't matter in practice. When the program is going to do the wrong thing, it makes little difference how it goes about doing it wrong. But if you really have to choose, crashing is better than silently corrupting data, yes. The latter is far more scary.
However, better is to ensure that your program doesn't do the wrong thing in the first place. If you have a complete type system, that's you best bet. But that isn't realistic. No language you are going to encounter in the real world has that. Meaning, prepare to learn how to write tests. They aren't easy to do right, but you're going to have to do it anyway because without a complete type system you have no other choice.
Anything else is running on hopes and prayers. If you honestly believe that hope and prayer is sufficient, then why are you so worried about crashing?
>Presumably your requirements dictate that your code must do something when getFile fails, so you are obviously going to write a test for that scenario. How do you envision that potential crash condition evading the test? The CPU steps through until getFile fails, then checks if you are running a test and if it so it invents a valid file handle, but if it notices you are in production it returns a corrupt file handle and then crashes? That doesn't make any sense. What does make sense to you that explains how your code will pass tests but fail in production?
Sigh. You said it was a feature. You said v being nil was a feature lol. Now you want to test it to see if it crashes? I'm saying it should be tested, but YOU said it was a feature for golang to always return a valid v. Now I know you're just not aware of what you're talking about.
>If what you are struggling to say is that you have no such requirement to deal with the failure so it is unspecified behaviour, and thus you didn't feel the need to write a test for it, all you can do with sum types, to keep the compiler happy, is purposefully panic. It is unspecified behaviour. Anything else but crashing would be nonsensical – something that is true even if it were written in Rust. Is the source of the panic significant?
I don't need to specify the difference here. You're just being stubborn. You already know that in rust you need to explictly panic vs. golang you can panic mistakenly with a hidden nil. This discussion is over.
Why have functions return err, nil? Why even allow for a runtime error here? It's a really simple fix. You don't even have to make the language complex to support this. Instead the entire program is littered with holes and if statements you have to check in order to prevent a actual crash