Hacker News new | ask | show | jobs
by 9rx 433 days ago
> Why even allow for a runtime error here?

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?

1 comments

>Why not?

TO prevent a runtime error. You say it Doesn't make any difference in practice meaning you never had a runtime error while running go? Impossible.

>A complete type system is insanely complex to implement and even harder to write against.

Who says you need a complex type system? You just need exhaustive evaluation of sum types. That's one feature that's it.

Removing run times errors doesn't mean building the most complex type system in the world.

> 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.

> Tests don’t catch everything.

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.

> You’re inexperienced that’s why you’re fascinated.

There's a good way to change that. Let's see your code!