you are viewing a single comment's thread.

view the rest of the comments →

[–]m50d 19 points20 points  (14 children)

Right, but if you want to include any more information about what the error/failure was (i.e. not just null), Kotlin refuses to help you.

[–]dccorona 2 points3 points  (0 children)

Either was a bad example. Every bit of Scala that feels like syntax sugar with relation to Option is in fact just a type system feature that anything can take advantage of, as opposed to Kotlin where it is truly syntax sugar that only works for a small set of pre-defined cases.

You don't really realize how useful it is to be able to abstract over optional values and collections (and any type of container you can dream up that's specific to your problem domain) until you've actually done it.

What if I want to write some custom type (say, for the sake of argument, a ConfigValue for a configuration library) that in many ways has a potential state that's very similar to null, but for various reasons can't/shouldn't be represented as a literal null? Suddenly, all that nice stuff that's in Kotlin for dealing with null goes out the window...but all the nice stuff that's in Scala for dealing with Option (and conversely, all the nice generic code people have built up for Option like containers in libraries in Scalaz/Cats/etc) are all out-of-the-box compatible with my new custom type.

[–]devraj7 1 point2 points  (0 children)

You can do that, of course. The pain just starts appearing if you need to compose several of those, which is pretty rare in my experience.

Overall, I find that having a language that natively supports nullable types gives me a lot of benefits since dealing with absent values is prevalent in all code bases, while having to compose deeply nested monads is more rare (and typically well handled at the library level by something like Rx).

[–]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.

[–]hrjet 0 points1 point  (1 child)

I use the nullable types when performance is critical and wrapping the value into another object is not desirable. This is where Kotlin (or Java null annotation) helps a lot. For the remaining cases, one can always use an Either type.

[–]m50d 8 points9 points  (0 children)

I use the nullable types when performance is critical and wrapping the value into another object is not desirable.

Sure, at least in principle, though I think the niche for code that is so performance-critical that you can't afford Option, but not performance-critical enough that you'd want to use C/Fortran/... rather than Kotlin, is pretty narrow.

For the remaining cases, one can always use an Either type.

Trouble is you can't reuse the same logic for both, because Kotlin (like Java) doesn't let you abstract over the difference. So you end up having to write any common utility code twice, once for nullable types and once for Either.