Ilja Tollu (12) [Avatar] Offline
#1
Hello, All!

I'm new to Kotlin and a fresh reader of this great book! But there is one question that I couldn't have found an answer to.

Suppose a sequence of computations, each of which could fail. Results could be typed as Try or Future. So, the whole sequence models a happy path of execution. But after it there is a recovery block, where all deviations from the happy path are handled.

In Scala you can use for-comprehension for this. What are the ways to accomplish the same in Kotlin, other than using try-catch blocks?

Another example is "with" block in Elixir. It works slightly differently: it pattern-matches the result of a step. It the match was successful - then the next step is called with all matched variables accessible. But if the match fails - it shortcuts to result. I believe, this behaviour is possible due to lack of static typing, but I may be wrong.

So, how this could be done in Kotlin?

Best regards,
Ilja
Pierre-Yves Saumont (181) [Avatar] Offline
#2
Hi Ilja,

Did you read chapter 7 yet? This chapter is about handling errors. In this chapter, I describe a type called Result which is more or less equivalent to Scala Try.

The for comprehension in Scala is only syntactic sugar over map/flatMap. Here is an example:

fun getFirstName(): Result<String> = ...

fun getLastName(): Result<String> = ...

fun getMail(): Result<String> = ...

data class Person(val firstName: String, val lastName: String, val mail: String)

val person = getFirstName()
        .flatMap { firstName ->
            getLastName()
                    .flatMap { lastName ->
                        getMail()
                                .map { mail -> Person(firstName, lastName, mail) }
                    }
        }


This is equivalent to something like:

for {
  firstName in getFirstName(),
  lastName in getLastName(),
  mail in getMail()
} return Person(firstName, lastName, mail)


although Kotlin does not have this syntax.

With this code, you get either a Result.Success<Person> if everything goes fine, or a Result.Error<RuntimeException> containing an exception if one of the functions produced one. You may then use this exception the way you want. You may even throw it!

Ilja Tollu (12) [Avatar] Offline
#3
Thank you!

Yes, I've looked through this chapter after I posted the question.

Sure, Scala's for-comprehension is a syntactic sugar (add 'withFilter' to it), but it makes all this way readable by eliminating the need for nested flatMaps. Actually, an example would not be complete without a recovery block.

So, not to fix myself on this for-comprehension style, I'm still to find an idiomatic way to accomplish a set of dependent steps and then recover from errors.

Best regards,
Ilja
Pierre-Yves Saumont (181) [Avatar] Offline
#4
One thing I perhaps made not clear eanough in the example is the type of the person variable resulting from the pattern:

val person: Result<Person> = getFirstName()
        .flatMap { firstName ->
            getLastName()
                    .flatMap { lastName ->
                        getMail()
                                .map { mail -> Person(firstName, lastName, mail) }
                    }
        }


Since you get a Result<Person>, you can easily handle the case when an error occurs using the Result.forEach function, which has the following signature:

fun forEach(onSuccess: (A) -> Unit = {},
            onFailure: (RuntimeException) -> Unit = {},
            onEmpty: () -> Unit = {})


So you could use it the following way:

fun main(args: Array<String>) {

    getFirstName()
            .flatMap { firstName ->
                getLastName()
                        .flatMap { lastName ->
                            getMail()
                                    .map { mail -> Person(firstName, lastName, mail) }
                        }
            }.forEach(::usePerson, ::handleException, ::handleEmpty)
}

fun usePerson(person: Person) {
    // ...
}

fun handleException(exception: RuntimeException) {
    // ...
}

fun handleEmpty() {
    // ...
}


Note that all parameters of forEach are optional. So, if you don't need to handle the "empty" case, just use:

fun main(args: Array<String>) {

    getFirstName()
            .flatMap { firstName ->
                getLastName()
                        .flatMap { lastName ->
                            getMail()
                                    .map { mail -> Person(firstName, lastName, mail) }
                        }
            }.forEach(::usePerson, ::handleException)
}