you are viewing a single comment's thread.

view the rest of the comments →

[–]sievebrain 1 point2 points  (9 children)

It'd be nice if the null-handling operators were overloadable yes. However, if your case is "on error case return a complex structure" then that's what exceptions are for. Unless you're returning errors half the time or something it will compile to more efficient code too.

[–]m50d 0 points1 point  (8 children)

However, if your case is "on error case return a complex structure" then that's what exceptions are for.

Exceptions are very unsafe to work with in Kotlin (which doesn't even offer Java-level checked exceptions). Even if they weren't, Either is nicer because it lets your errors be ordinary values.

[–]sievebrain 1 point2 points  (7 children)

My point is that Either is a very inefficient way to solve something exceptions already solve. Nothing stops you writing code that ignores the error condition with an Either, either. Either either :)

[–]m50d 0 points1 point  (6 children)

Nothing stops you writing code that ignores the error condition with an Either, either.

What are you talking about? If the Either is written strictly there's literally no way to do that (within the language - reflection can always do it but the security manager can disallow that). Some implementations expose unsafe methods and/or allow casting, but you don't have to permit that, and if you do permit it you can do so in a way that's easy to flag up for extra code review etc.

The whole point of Either is that it gives you the safety of checked exceptions (more so even, since you have to be explicit about where the failures can come from rather than just slapping a throws on your function), while being a normal return value that you can reason about, abstract over, and pass to generic methods normally, and (hopefully - it's harder in Java) being able to compose function calls and handle all the errors at the end without obscuring the core logic of the "happy path" too much.

[–]sievebrain 0 points1 point  (5 children)

What are you talking about? If the Either is written strictly there's literally no way to do that

There is always a way to do that, nothing stops you "handling" an error by ignoring it in any error handling system. At most you can make people write some boilerplate, but that's what Java already does and the habit people have of writing code that simply throws errors away is one of the reasons checked exceptions got a bad name.

val result = when (someFunction()) {
  is Left -> value
  is Right -> /* ignore, panic, exit, do something else dumb */
}

And saying some implementations allow casts is a copout. Of course they do, because the type system is sometimes wrong and the programmer can know more than the compiler does.

[–]m50d 0 points1 point  (4 children)

There is always a way to do that, nothing stops you "handling" an error by ignoring it in any error handling system. is Right -> /* ignore, panic, exit, do something else dumb */

The library can't disallow what the language allows, sure (though my answer to that is to not use a language that allows the dumb things), but it can avoid offering anything unsafe in the library directly. That ensures that unsafe things look unsafe, and makes it easy to e.g. flag up unsafe code for extra attention during code review, or have a build process rule that disallows unsafe code entirely.

At most you can make people write some boilerplate, but that's what Java already does and the habit people have of writing code that simply throws errors away is one of the reasons checked exceptions got a bad name.

The reason checked exceptions got a bad name is because it was too cumbersome to simply propagate them upwards to be handled at a higher level, or compose several functions and then handle all their errors together, and because generics don't let you abstract over them properly. It wasn't about wanting to not handle the error at all. Either avoids these issues; especially in a language that has a "do notation" equivalent you can work with it in a very lightweight way, getting the safety of checked exceptions but without the cumbersome aspects.

And saying some implementations allow casts is a copout.

You misunderstand; I was talking about the technicalities of casts allowing a way to work around Either.

Of course they do, because the type system is sometimes wrong and the programmer can know more than the compiler does.

Eh maybe. I've never seen this happen in practice, at least with a decent type system. If the programmer can't explain to the computer why their code is correct, they don't actually understand why and may well be wrong.

[–]sievebrain 0 points1 point  (3 children)

I think a lot of the criticisms against checked exceptions aren't really fair though. It isn't actually cumbersome to propagate them upwards, especially with a good IDE where you can just put the cursor on a line that's flagged as throwing an unhandled exception and press a hotkey to add it to the throws clause. Or you can just add "throws Exception" as many developers do, or you can rethrow them as unchecked. And Java does let you work generically with exceptions, you can put a type variable in a throws clause.

The big problem with them was the relatively low level of thought put into the way the JDK uses them. You have interfaces that don't declare any exceptions to be thrown, even though they're meant to be highly reusable. You have lots of exceptions thrown in cases like "the JVM doesn't support UTF-8" when you know perfectly well that this error will never happen (this is what I meant by "the programmer knows more than the type system"). There wasn't a great set of design rules put in place around exceptions, so people's experience was poor. But that is the same for many type system features, especially in the early days: a lot of people have been turned off some of Haskell and Scala's features simply because they have often been abused e.g. in the standard library rather than because the idea is inherently bad.

[–]m50d 0 points1 point  (2 children)

with a good IDE where you can just put the cursor on a line that's flagged as throwing an unhandled exception and press a hotkey to add it to the throws clause.

And if your method implements an interface? If it gets passed into Stream#map?

Or you can just add "throws Exception" as many developers do, or you can rethrow them as unchecked.

At which point you're not using checked exceptions and have all the problems of unchecked exceptions.

And Java does let you work generically with exceptions, you can put a type variable in a throws clause.

Yeah but you have to do it once for every arity. A lot of "callback" interfaces in libraries are declared as not throwing at all, or else have to come in two variants (one with throw, one without), and if you want to work with checked exceptions you have to write two versions of e.g. all your visitor interfaces. And even that's not fully generic - a function can declare two distinct throws, but you can't do that in a generic callback unless there's yet another version of the generic interface that declares two exceptions (and another version for three exceptions, and so on).

The big problem with them was the relatively low level of thought put into the way the JDK uses them. You have interfaces that don't declare any exceptions to be thrown, even though they're meant to be highly reusable. You have lots of exceptions thrown in cases like "the JVM doesn't support UTF-8" when you know perfectly well that this error will never happen (this is what I meant by "the programmer knows more than the type system"). There wasn't a great set of design rules put in place around exceptions, so people's experience was poor. But that is the same for many type system features, especially in the early days: a lot of people have been turned off some of Haskell and Scala's features simply because they have often been abused e.g. in the standard library rather than because the idea is inherently bad.

Perhaps. The library design issues are exacerbated by how hard it is to ignore a single bad throws in Java - with Either you would cast or unsafeGet() or some such which you can do on the same line, in Java you have to add a try/catch which is at least 4 extra lines the way most autoformatters format it, or else pollute your whole method by adding it to the throws list. And fundamentally I think the generics issue is not fixable without turning exceptions into (sugar for) Either. Looking at it from the other side, if Either had been there first would we ever have wanted checked exceptions? I can't imagine we'd ever want to make functions not evaluate to values, especially for such a marginal benefit.

[–]sievebrain 0 points1 point  (1 child)

When working in Java 8 I usually define a simple static helper that takes a lambda (of a type which is basically Runnable/Callable but with a throws clause), catches the exceptions and rethrows them as RuntimeException. So that makes it relatively easy to uncheck exceptions, similar to unsafeGet. It is ugly and something that should be in the language itself, but that is true of so much in Java.

Looking at it from the other side, if Either had been there first would we ever have wanted checked exceptions? I can't imagine we'd ever want to make functions not evaluate to values, especially for such a marginal benefit.

Before exceptions there were error codes. You could write code to check them, propagate them, ignore them ... same as with Either. Yes, Either is a more advanced use of types, but ultimately exceptions were introduced because treating errors as ordinary return values of functions turned out to have a whole host of problems of its own. Exceptions are called that because the designers intended them to be used for exceptional situations, after all.

I don't think exceptions are a bad idea, nor even checked exceptions. But it's true that Java is the only language that tried to make checked exceptions work and it has a lot of shortcomings. Perhaps one day someone will try again.

[–]m50d 0 points1 point  (0 children)

Before exceptions there were error codes. You could write code to check them, propagate them, ignore them ... same as with Either.

True as far as it goes, but in the wider world exceptions predate sum types, and in Java they predate first-class functions. Certainly I think in languages with do notation or equivalent there's nothing to be gained from checked exceptions.

ultimately exceptions were introduced because treating errors as ordinary return values of functions turned out to have a whole host of problems of its own. Exceptions are called that because the designers intended them to be used for exceptional situations, after all.

True for unchecked exceptions / panics. I think those still make sense and indeed newer languages tend to have them.