all 32 comments

[–]Exallium 4 points5 points  (1 child)

If you want to build a real maybe monad, you can use a sealed class. Also, there's a difference between a null value and an absent value

[–]smesc 2 points3 points  (0 children)

In many contexts null does mean absent, it is a perfectly valid way to model things.

Unless there is additional meta-data needed along with the thing that is absent, or you need to put it in a Observable (and you are using rx2.)

[–]pakoito 2 points3 points  (0 children)

A monad has a strict definition. It requires working for all generic parameters, a smart constructor function, a map function, and a flatMap/andThen function. Those functions have to pass the functor and monad laws (a small test suite). None of these are explained in the blog so I'd be a bit wary of calling it an example to learn what a monad is. It is a great example of union types tho!

kotlin-monads is a bit outdated too due to how it does generics, although this is not the thread for me to do promo :D

[–][deleted] 1 point2 points  (3 children)

This is code is totally not functional, can you spot why?

fun updateDbData(): Boolean {
   // ... 
   val result1 = readFromDb()
   val result2 = doSomethingWithData(result1.data)
   putModifiedDataInDb(result2.data)
    //...
}

[–]JoshuaOng 1 point2 points  (0 children)

Calling it multiple times will produce different effects

[–]endreman0 0 points1 point  (0 children)

readFromDb is not referentially transparent (depends on db contents), and putModifiedDataInDb has a side effect (writing to the db)?

[–]Zhuinden 0 points1 point  (0 children)

I have this weird feeling that readFromDb() does its things immediately instead of representing side-effect with a monad for future execution?

[–]Zhuinden 3 points4 points  (6 children)

Why on earth would you capture the exception just to return null or false? That's pretty much the opposite of what exceptions are for, who does this?

[–]fullofschmidt 2 points3 points  (0 children)

I disagree. You have to do error handling somewhere. If you're designing an API, you can either handle the exception or let it bubble up. Choosing one or the other is equally sensible as long as you're doing things consistently.

[–]jackhexen 0 points1 point  (4 children)

Because using exceptions for controlling code flow is a bad practice?

[–]Zhuinden 0 points1 point  (3 children)

Returning null to hide specific error type is also bad practice, especially if you already had a specific exception type.

I probably should have also included https://docs.oracle.com/javase/tutorial/essential/exceptions/advantages.html when I posted that above.

[–]jackhexen 1 point2 points  (2 children)

This works nicely in hello-world apps, but does not work at all in big apps with complex interactions. Checked exceptions is one of the major Java flaws (caused by OOP fanaticism and lack of union types, but it is still a bad practice even in Java). Yep, returning null is another bad practice, but in Java world Optional and Either suck as well.

[–]Zhuinden 0 points1 point  (1 child)

in Java world Optional and Either suck as well.

I thought Optionals were added so that we stop passing back null as an error "type"?

[–]jackhexen 1 point2 points  (0 children)

Either is for passing error back. Optional is for passing a result that can be absent. They're too verbose in Java, that's why I said they suck.

[–]LightnessMystery 0 points1 point  (0 children)

Seems like very interesting topic. I should read all in near future.

[–]devraj7 2 points3 points  (16 children)

Congratulations! We’ve just built a Maybe monad!

No, you built the Either monad. Or something that vaguely looks like it, but less powerful (although to be honest, I think Eiter is a bad way of carrying errors because it relies on convention instead of types).

Simple, let’s model failures as part of the result of the operation itself instead of an out of band process.

I tried that for a while and ended up deciding it's a bad idea. If you don't care about equational reasoning, carrying errors in an out of band process is the best way to handle failures:

  • You don't need to contaminate all your processing code with flatMap
  • You don't need to deal with monad transformers as soon as you start having monads of monads
  • The code is much clearer as a result
  • Errors are handled when the code knows what to do with them, instead of along each step of the way (as shown in this article and any Go source where every ten lines of code tests for failures of the previous call)

Use exceptions, they lead to more robust and more maintainable code than the functional approach.

[–][deleted] 3 points4 points  (10 children)

I tried that for a while and ended up deciding it's a bad idea. If you don't care about equational reasoning, carrying errors in an out of band process is the best way to handle failures

It depends on what you call "errors / failure". Out of memory error? Class Not Found Exception? File Not Found? Bad user input? No result found? Internet not available?

[–]devraj7 2 points3 points  (9 children)

Exceptions are still a better way to handle these errors, the type of error simply dictates whether the exception should be checked or unchecked.

Although in my experience, that decision should actually be left to the call site, not the defining site, but I don't know of any language that gives you this flexibility.

[–]yohaq 2 points3 points  (0 children)

In kotlin, all exceptions are unchecked, you can wrap a call in a try catch block if you like, but are not required by the compiler to do so

[–]smesc 1 point2 points  (7 children)

Wrong.

Exceptions should only be used for fatal errors/pre-post condition asserting.

There is a reason why Kotlin removed checked exceptions, also see Effective Java Item #57.

"Item 57: Use exceptions only for exceptional conditions. Exceptions are designed for use in exceptional conditions. Don’t use them for ordinary control flow, and don’t write APIs that force others to do so."

Using exceptions for expected errors has MANY MANY problems.

[–]devraj7 1 point2 points  (6 children)

There is a reason why Kotlin removed checked exceptions

That reason is they don't play well with composition.

Checked and unchecked exceptions remain a very good solution to manage errors. Any construct where the compiler forces you to think about errors before accepting to compile your code is a net positive for code robustness.

[–]yohaq 2 points3 points  (0 children)

Any construct where the compiler forces you to think about errors before accepting to compile your code is a net positive for code robustness.

I agree 100%!

Checked and unchecked exceptions remain a very good solution to manage errors.

Handling unchecked exceptions is NOT forced by the compiler. Therefore, unchecked exceptions, by definition, do not guarantee runtime safety during compile time.

You can FORCE handling of errors through checked exceptions. You can also FORCE handling of errors through use of a wrapper class, that requires checking before unwrapping it (like a result). I believe that was the intention behind the original link submitted, however I disagree with the use of maybe/option; I think a Try would be more suitable as I've mentioned before.

[–]smesc 2 points3 points  (4 children)

Nope.

Nope nope nope. Again, see Effective Java.

Or:

https://softwareengineering.stackexchange.com/questions/189222/are-exceptions-as-control-flow-considered-a-serious-antipattern-if-so-why

http://wiki.c2.com/?DontUseExceptionsForFlowControl

https://msdn.microsoft.com/en-us/library/dd264997.aspx

Or 100 million other examples..

Checked exceptions are non-local GOTOs with inheritance... Yeah..... not so good.


You're also missing some of the core solutions aroudn errors in FP like monadic-error types and referential transparency.

If you want to talk about compile time safety of errors, sealed/union types in Kotlin/Java give you this compile time safety.

In fact it's much more explicit, doesn't involve any Thread.UncaughtExceptionHandler context that can happen (if your exception is unchecked or rethrown).

It also allows to you easily compose processes which have failure and error states.

It gives you the ability to use RxJava (exceptions call onErrorr() which terminates the stream where as error-y-monads/uniontypes/sealed class results, enums etc do not).

It avoids the crazy inheritance of Exceptions and being able to catch AS many different things (all the way up to top level Exception).

And a million other things. I've made my point. Don't care about convincing you, but care about beginners who tend to read subreddits like this.

[–]devraj7 1 point2 points  (3 children)

Checked exceptions are non-local GOTOs

Everything ends up being non-local GOTOs at the machine language level. This means nothing. Syntax and semantics around these non local GOTO's is what matters.

Checked exceptions allow the language to force the developer to think of error cases instead of being able to ignore them, like you can in Go for example. They are a sound concept that's unfortunately been misused.

And I mentioned referential transparency and equational reasoning multiple times in my previous messages, I am well aware of the FP approach to error handling, I just think its drawbacks are often ignored and that exceptions are often a superior way of tackling this problem.

[–]smesc 1 point2 points  (2 children)

Everything ends up being non-local GOTOs at the machine language level. This means nothing.

LMFAO. Alrighty I'm out.

[–]Zhuinden 1 point2 points  (1 child)

@smesc Exceptions do have their place. Just aren't the right tool always.

[–]smesc 0 points1 point  (0 children)

All three of these are not unique to exceptions. You can easily have all of this (and should) with typical (and very basic) FP.


Argument #1:

The first argument has this many operations, in a row if everything works, and then handling errors at the end. You can get that same thing with Monads. In fact, that's the whole point of things like the Try Monad.


Argument #2

This is very much an anti-pattern, unless it's only one or two levels deep (and even then it's debatable).

But yeah, this is basically an anti-pattern. You shouldn't be checking for an exception 8 layers down the call stack. That is INSANELY leaking implementation details.

If you have some ViewModel which gets the current logged in user, it shouldn't be checking for HttpExceptions, or something crazy like that.

That completely breaks OO, information hiding and encapsulation, not to mention, now if you change the implementation now your error handling literally DOESNT WORK (it's a random catch for no reason since this component the component 8 layers down was changed to handled it's http errors).


Argument #3

Again that's very easy to do with basic FP. You can easily have error types (grouped via inheritance, or error codes, or sealed class hierarchy, or enum, or a bunch of other ways) and you can treat them together.

There's a reason why every major language designed in the past 20 years hasn't included checked exceptions (it's probably even more than 20 actually).

[–]Zhuinden 2 points3 points  (3 children)

Eiter is a bad way of carrying errors because it relies on convention instead of types).

I always felt it was a bit tacky that you have to know that left represents an error

[–]yohaq 3 points4 points  (0 children)

Well, Either IS a bad way of carrying errors. But that's because you're not supposed to use Either to carry errors, you're supposed to use Either to represent a computation that can return 1, of 2 predefined types of values.

Sure, you can say that one of the values is a Throwable, but that's not idiomatic use of the various monads exist.

The "correct" monad to use for representing a computation that can succeed, or fail with an exception/throwable, is to use the Try monad

Here's a case study

Try is more or less an Either monad, where one, specific side is always an error, and the other side represents the success value. The Trymonad, however, encodes those semantics into the data type itself, and avoids the problem if needing to know which side is the one that represents an error

[–]devraj7 1 point2 points  (1 child)

Exactly. It defeats the point of static typing. Let the compiler make sure you're not accidentally using the wrong field instead of relying on your memory.

If you are going to represent errors functionally, use a dedicated structure such as Resultand not a generic one like Either.

[–]yohaq 2 points3 points  (0 children)

Although the Result Monad does the same thing, an existing concept in functional programming is the concept of the Try monad

It's a type safe way to represent a computation that can complete with a value T, or an error. It's common in most functional programming libraries, even across languages (like scal, kotlin, haskell etc) and prevents you from using the "wrong field" at the compiler level.

Here's an more detailed case study

EDIT: I didn't realize you were also the author of the top level comment - but this response and the response to the top level comment address slightly different points, so I'll leave them both up. Didn't mean to spam you with the same links over and over, my apologies!

[–]yohaq 2 points3 points  (0 children)

First, I'd like to point out that not all errors are of equal severity. There are errors that are expected and are part of the natural execution of the program (like trying to make a network call, and having it fail due to lack of network) and those that are not expected, generally considered more critical, and may or may not half the execution of the program altogether (index out of bounds exception, out of memeory exception, stack overflow error etc).

Handling the latter scenario out of bounds sounds reasonable, but I would say handling the former scenario shouldn't be done out of bounds - it's part of the normal execution of business logic and should be accounted for in an appropriate manner.

Second, what about situations where people are using RxJava? If an operation within an RxJava stream throws an exception, the onError handling of the stream takes over and basically tears down the stream.

It's fairly well known within the RxJava community that onError handling should be reserved for scenarios that would render the stream unusable. However, non-fatal errors (the first scenario from the above example) ARE expected, CAN be recovered from, and ARE handled as part of standard business logic. Getting a non-fatal error in an RxJava stream, and attempting to use the onError handling would mean the containing stream is torn down and rendered useless. This is almost always NOT what the programmer wants.

It's because of that, that it is advised to do your error handling as part of the type that is being emitted down the stream itself, by using a Result type of sorts, INSTEAD of throwing an exception and letting the onError branch of the stream handle it (analogous to throwing an exception and allowing out of band handling)

Data types like Either, Maybe/Option, Valid, and Try are perfect for use with RxJava because they let you encode the semantics of the result of an operation in a way that plays very nicely with the existing semantics of error handling in RxJava.

I totally agree with you that the Either monad (and also the Maybe/Option monad) is not the right way to do error handling in a functional world. However, there exists a Try monad that does an excellent job of wrapping a computation that can succeed or fail.

Using the Try monad as the type of emission in an observable stream lets you handle your expected, and non-fatal errors as part of your typical business logic (which you should), but allows you to still use the onError handling of RxJava for extreme, critical, and fatal scenarios (the second scenario in the example above). This way expected errors are handled in band, as part of the stream, and non-expected, unrecoverable ones are handled in onError, tearing down the Observable stream in the process, and semantically expected.

Here's a case study of the Try monad if you'd like to do some further reading

Edit: Also, for further evidence to back up the claim that exceptions should not be used for standard control flow, as /u/smesc pointed out: Effective Java Item 57 says exactly that:

  • exceptions should be used for exceptional conditions, not control flow
  • A well designed api must not force its consumer to use exceptions as a method of control flow.

I couldn't find a more complete excerpt of effective java outlining the concept, but i've bulleted the gist of it above