Monday, February 28, 2022

Kotlin: Validation type

Kotlin: Validation type

Any system will have errors and how we handle them is important. Consistent and transparent error handling is critical to any production ready system. Here we explore the functional approach to error handling, developing techniques to capture errors elegantly without contaminating your code with ugly conditionals and try/catch clauses. We consider the Validation class included in the custom library Dogs.



Motivation

Validation can be found in different forms when error(s) are detected. Validation can return immediately when the first error (or exception) has been encountered; the validation result may or may not contain the validation error or exception message. This scenario is called fast failing validation, in which the validation does not validate all the business rules and only zero or one message is returned, and the process shall be cut short upon first error. This simple form of validation is sometimes considered insufficient, as a full validation is not carried out with accumulated errors. See the Either type blog on fast failing.

Validating all the business rules, and accumulating errors, is very different from fast failing validation. Applicative functors are proposed, and they have effectively solved accumulation problems. The Validation data type has an instance of applicative that accumulates on the error side.

The Validation data type is isomorphic to Either. Validation is a container type with two type parameters: A Validation<E, A> instance can contain either an instance of E, or an instance of A (a disjoint type). Validation has exactly two sub types, Failure and Success. If a Validation<E, A> object contains an instance of E, then the Validation is a Failure. Otherwise it contains an instance of A and is a Success. The Validation sealed class is:

sealed class Validation<out E, out A> {

    class Failure<out E, out A>internal constructor(val value: E) : Validation<E, A>()
    class Success<out E, out A>internal constructor(val value: A) : Validation<E, A>()

}

As before, the constructors for Failure and Success are internal and we create instances with the factory functions:

val failed: Validation<String, Int> = failure("bug")
val succeeded: Validation<String, Int> = success(25)

assertEquals(true,      failed.isFailure())
assertEquals(false,     succeeded.isFailure())

Validation is a general purpose type for use with error handling:

fun parseInt(text: String): Validation<String, Int> =
    try {
        success(Integer.parseInt(text))
    } catch (ex: NumberFormatException) {
        failure("parseInt: bad number format: $text")
    }

assertEquals(success(123),                                  parseInt("123"))
assertEquals(failure("parseInt: bad number format: ken"),   parseInt("ken"))



Pattern matching

Like in the Option class the ValidationFailure and Success type names can be used in application code. The type names can be used in user-defined functions. The function swap supports mapping over both arguments at the same time. Its signature is shown in the following code.

fun <E, A> Validation<E, A>.swap(): Validation<A, E> {
    return when(this) {
        is Failure -> success(this.value)
        is Success -> failure(this.value)
    }
}   // swap

val failed: Validation<String, Int> = failure("bug")
val succeeded: Validation<String, Int> = success(25)

assertEquals(success("bug"),    failed.swap())
assertEquals(failure(25),       succeeded.swap())


The function swap flips the Failure/Success values.



Some Validation operations

The Validation class includes many of the functions we saw with the Either class. Validation class functions include exists, fold and map. The map function has the signature:

fun <B> map(f: (A) -> B): Validation<E, B>

and applies function if this is a Success.

val failed: Validation<String, Int> = failure("bug")
val succeeded: Validation<String, Int> = success(25)

assertEquals(failure("bug"),    failed.map(isEven))
assertEquals(success(false),    succeeded.map(isEven))
assertEquals(success(true),     succeeded.map(isOdd))


The Validation type is also an applicative functorThe Validation type as an applicative functor includes the extension function ap:

fun <E, A, B> Validation<E, A>.ap(se: Semigroup<E>, f: Validation<E, (A) -> B>): Validation<E, B>

which applies the wrapped function to the receiver Validation. If two or more errors are encountered they are combined using the semigroup instance.

val failed: Validation<String, Int> = failure("bug")
val succeeded: Validation<String, Int> = success(25)

assertEquals(failure("bug"),      failed.ap(stringSemigroup, success{n: Int -> isEven(n)}))
assertEquals(success(false),      succeeded.ap(stringSemigroup, success{n: Int -> isEven(n)}))


When mapping functions over the Validation functor with fmap/map we have provided a unary function for the mapping. What do we get if we provide a curried binary function? The answer is we get a unary function wrapped in a Validation as shown for the value binding vf and vs.

val failed: Validation<String, Int> = failure("bug")
val err: Validation<String, Int> = failure("error")
val succeeded: Validation<String, Int> = success(25)

val vf: Validation<String, (Int) -> Int> = {m: Int -> {n: Int -> m + n}} dollar failed
val vs: Validation<String, (Int) -> Int> = {m: Int -> {n: Int -> m + n}} dollar succeeded

assertEquals(success(50),           succeeded.ap(stringSemigroup, vs))
assertEquals(failure("errorbug"),   err.ap(stringSemigroup, vf))

Suppose we need to validate a person's name and age where the values are supplied through two text values. The classes we wish to construct from the text are:

data class Name(val name: String)
data class Age(val age: Int)
data class Person(val name: Name, val age: Age)


A name is valid provided it is non-empty and an age is valid if it is non-negative. The checks are made with the functions makeName and makeAge:

fun makeName(name: String): Validation<String, Name> =
    if (name == "") failure("Name is empty") else success(Name(name))

fun makeAge(age: Int): Validation<String, Age> =
    if (age < 0) failure("Age out of range") else success(Age(age))


We are using Validation with the result of these two validation functions. To combine the results we use the applicative fmap2 function, a binary version of fmap:

fun makePerson(name: String, age: Int): Validation<String, Person> =
    fmap2(stringSemigroup, makeName(name), makeAge(age)){name: Name -> {age: Age -> Person(name, age)}}

In the first assert we have an invalid age. In the second assert we have an invalid name and an invalid age. The string semigroup concatenates the two failure messages. The final assertion makes a valid Person instance.

assertEquals(failure("Age out of range"),               makePerson("Ken", -5))
assertEquals(failure("Age out of rangeName is empty"),  makePerson("", -5))
assertEquals(success(Person(Name("Ken"), Age(25))),     makePerson("Ken", 25))



Case Study

We repeat here the second case study introduced in the Either type blog. In this a user form comprises text fields for the user last name, the first name and the email. The requirements are that the last name is fully capitalized, the first name is capitalized on the initial letter, and that the email be correctly structured. After accepting the three fields, objects for the classes LastNameFirstName and Email are constructed then used to create an instance of the Person class.

The standard technique for the construction of objects that need to honor a set of constraints is the smart constructor idiom. This is illustrated for the class LastName, using Validation for the error handling. Since it is meaningless to have a Failure with no errors then we use the type Failure<NonEmptyList<Error>, A> where Error is some error type. We start with:

typealias Error = String
typealias ValidationNelError<A> = ValidationNel<Error, A>

Typically, Error would be a sealed data class with sub-types for the various error types. The type name ValidationNel is provided by Dogs and is an alias for Validation<NonEmptyList<E>, A>. The application alias ValidationNelError provides a compact representation for Validation<NonEmptyList<Error>, A>.

The class LastName is:

data class LastName(val lastName: String) {

    companion object {

        fun create(lastName: String): ValidationNelError<LastName> {
            return if (lastName.length < 2)
                failureNel("Last name too short: $lastName")
            else if (!regex.matches(lastName))
                failureNel("Last name malformed: $lastName")
            else
                successNel(LastName(lastName))
        }   // create

        private val regex: Regex = Regex("[A-Z][A-Z]*")
    }

}   // LastName

The same scheme is used for the classes FirstName and Email.

Since the Either class is a monad we used the monadic bind to implement the smart constructor for the Person class. However, the Validation class is not a monad and we have no bind function. The issue is that the applicative functor implied by Validation being a monad does not equal the applicative functor defined on Validation.

We also know that the monad fails fast and can only identify the first error. Applicatives allow us to compose independent operations and evaluate each one. Even if an intermediate evaluation fails. This allows us to collect error messages instead of returning only the first error that occurred. A classic example where this is useful is the validation of user input. We would like to return a list of all invalid inputs rather than aborting the evaluation after the first error. We see this in the Person class:

data class Person(val lastName: LastName, val firstName: FirstName, val email: Email) {

    companion object {

        fun create(lastName: String, firstName: String, email: String): ValidationNelError<Person> {
            val vLastName: ValidationNelError<LastName> = LastName.create(lastName)
            val vFirstName: ValidationNelError<FirstName> = FirstName.create(firstName)
            val vEmail: ValidationNelError<Email> = Email.create(email)

            val ctor: (LastName) -> (FirstName) -> (Email) -> Person = C3(::Person)
            return ctor dollar vLastName appliedOver vFirstName appliedOver vEmail
        }   // create

    }

}   // Person

and in the following three examples:

assertEquals(
    success(Person(LastName("BARCLAY"), FirstName("Ken"), Email("me@gmail.com"))),
    Person.create("BARCLAY", "Ken", "me@gmail.com")
)
assertEquals(
    failure(NonEmptyListF.singleton("First name malformed: kenneth")),
    Person.create("BARCLAY", "kenneth", "me@gmail.com")
)
assertEquals(
    failure(NonEmptyListF.of("First name malformed: kenneth", "Last name malformed: Barclay")),
    Person.create("Barclay", "kenneth", "me@gmail.com")
)

Observe how the third example identifies the malformed first name and the malformed last name.



The code for the Dogs library can be found at:

https://github.com/KenBarclay/TBA


Sunday, February 27, 2022

Kotlin: Either type

Kotlin: Either type

Just as its english counterpart describes, Either can represent one value or another. Scenarios where this might be the return value from a function where you may get the successful result value or you might get an error value. The class is included in the custom library Dogs.



The Option type allows us to represent failures and exceptions with ordinary values and provide functions that abstract out common patterns of  error handling and recovery. The functions bind and ap over the Option type did just this. One issue with the Option type is that it does not report what is wrong in an exceptional case. All we have is None, indicating that there is no value that can be delivered.

Either is a container type with two type parameters: An Either<A, B> instance can contain either an instance of A, or an instance of B (a disjoint type)Either has exactly two sub types, Left and Right. If an Either<A, B> object contains an instance of A, then the Either is a Left. Otherwise it contains an instance of B and is a Right. The Either sealed class is:

sealed class Either<out A, out B> {

    class Left<out A, out B>internal constructor(val value: A) : Either<A, B>()

    class Right<out A, out B>internal constructor(val value: B) : Either<A, B>()

}

As before, the constructors for Left and Right are internal and we create instances with the factory functions:

assertEquals(true,      left<String, Int>("ken").isLeft())
assertEquals(false,     right<String, Int>(25).isLeft())

Either is a general purpose type for use whenever you need to deal with a result that can be one of two possible values. Nevertheless, error handling is a popular use case for it, and by convention Left represents the error case and Right the success value:

fun parseInt(text: String): Either<String, Int> =
    try {
        right(Integer.parseInt(text))
    } catch (ex: NumberFormatException) {
        left("parseInt: bad number format: $text")
    }

assertEquals(right(123),                                parseInt("123"))
assertEquals(left("parseInt: bad number format: ken"),  parseInt("ken"))



Pattern matching

Like the Option class the Either, Left and Right type names can be used in application code. The type names can be used in user-defined functions. The function bimap supports mapping over both arguments at the same time. Its signature is shown in the following code.

fun <A, B, C, D> bimap(either: Either<A, B>, f: (A) -> C, g: (B) -> D): Either<C, D> =
    when (either) {
        is Either.Left -> left(f(either.value))
        is Either.Right -> right(g(either.value))
    }   // bimap

class DomainError(val text: String?) : Exception(text)

val currentDate: Either<Exception, Calendar> =     // simple definition
    right(GregorianCalendar(2020, 4, 1))

val res: Either<Exception, Long> = bimap(currentDate,
    {ex -> DomainError(ex.message)}, 
    {date -> date.timeInMillis}
)
assertEquals(right(1588287600000),   res)


The function f is applied to the wrapped value if it is a Left instance and the function g is applied to the wrapped value if it is a Right instance. The val binding for currentDate wraps a calendar value in a Right. Applying the bimap function to it we make a DomainError if the first parameter is a Left. If the first parameter is a Right we convert the wrapped calendar to its milliseconds.



Some Either operations

The Either class includes many of the functions we saw with the Option class. Either class functions include exists, fold, getOrElse and map. The fold function has the signature:

fun <C> fold(fa: (A) -> C, fb: (B) -> C): C

and applies function fa if this is a Left or function fb if this is a Right. A consequence is that bimap is a special case of fold as in:

class DomainError(val text: String?) : Exception(text)

val currentDate: Either<Exception, Calendar> =     // simple definition
        right(GregorianCalendar(2020, 4, 1))

val res: Either<Exception, Long> = currentDate.fold(
    {ex -> left(DomainError(ex.message))},
    {date -> right(date.timeInMillis)}
)
assertEquals(right(1588287600000),   res)


The Either type is right-biased, so functions such as map and bind apply only to the Right case. This right-bias makes Either convenient in, for example, a monadic context.

assertEquals(left("Ken"),  left<String, Int>("Ken").map{n -> 2 * n})
assertEquals(right(4),     right<String, Int>(2).map{n -> 2 * n})




Case Study

A Project represents some work hosted on a repository service such as GitHub. Each Project records its URL and the list of contributors. Our purpose is to identify those projects with no contributors and in need of support, and at the same time a list of all contributors. Here is how we process our project list:

class Project(val url: URL, val contributors: List<String>)

val git: List<Project> = ListF.of(
    Project(URL("http://github.com/project/ai"),           ListF.of()),
    Project(URL("http://github.com/project/algol"),        ListF.of("EdsgerD", "JohnB", "PeterN", "KenB")),
    Project(URL("http://github.com/project/antlr"),        ListF.of("TerranceP")),
    Project(URL("http://github.com/project/data"),         ListF.of("KenB", "JohnS")),
    Project(URL("http://github.com/project/ml"),           ListF.of()),
    Project(URL("http://github.com/project/system"),       ListF.of("BrianK", "DennisR"))
)

val checked: List<Either<URL, List<String>>> =
    git.map{project ->
        if (project.contributors.isEmpty())
            left<URL, List<String>>(project.url)
        else
            right<URL, List<String>>(project.contributors)
    }

val support: List<Option<URL>> = checked.bind{either -> ListF.singleton(either.fold({ url -> some(url)}, {none()}))}
val supportReq: List<Option<URL>> = support.filter{option: Option<URL> -> (option.isDefined())}
val supportRequired: Option<List<URL>> = supportReq.sequenceOption()

assertEquals(
    ListF.of(URL("http://github.com/project/ai"), URL("http://github.com/project/ml")),
    supportRequired.getOrElse(ListF.empty())
)

val supporters: List<String> = checked.bind{either -> either.fold({ListF.empty<String>()}, {names -> names})}.removeDuplicates()

assertEquals(
    ListF.of("EdsgerD", "JohnB", "PeterN", "KenB", "TerranceP", "JohnS", "BrianK", "DennisR"),
    supporters
)


We create in checked a list of Either values, with the Left instances representing unsupported projects and the Right instances containing the contributors. The sequence of val bindings support is a List of Options wrapping the URL; supportReq is also a List of Options but without any None instances; supportRequired is a List of URLs wrapped in an Option. The val binding supporters is the List of contributors.



Case Study

A user input form comprises text fields for the user last name, the first name and the email. The requirements are that the last name is fully capitalized, the first name is capitalized on the initial letter, and that the email be correctly structured. After accepting the three text fields for the classes, LastNameFirstName and Email are constructed then used to create an instance of the Person class.

The standard technique for the construction of objects that need to honor a set of constraints is the smart constructor idiom. This is illustrated for the class LastName, using Either for the error handling. We start with:

typealias Error = String

data class LastName(val lastName: String) {

    companion object {

        fun create(lastName: String): Either<Error, LastName> {
            return if (lastName.length < 2)
                left("Last name too short: $lastName")
            else if (!regex.matches(lastName))
                left("Last name malformed: $lastName")
            else
                right(LastName(lastName))
        }   // create

        private val regex: Regex = Regex("[A-Z][A-Z]*")
    }

}   // LastName

Typically, Error would be a sealed class with sub-types for the various error types. Also we might make the constructor for LastName private so that users must use function create.

The same scheme is used for the classes FirstName and Email.

Since the Either class is a monad we used the monadic bind to implement the smart constructor for the Person class:

data class Person(val lastName: LastName, val firstName: FirstName, val email: Email) {

    companion object {

        fun create(lastName: String, firstName: String, email: String): Either<Error, Person> {
            val eLastName: Either<Error, LastName> = LastName.create(lastName)
            val eFirstName: Either<Error, FirstName> = FirstName.create(firstName)
            val eEmail: Either<Error, Email> = Email.create(email)

            return eLastName.bind{lastName ->
                eFirstName.bind{firstName ->
                    eEmail.bind{email ->
                        inject(Person(lastName, firstName, email))
                    }
                }
            }
        }   // create

    }

}   // Person

We know that the monad fails fast and so can only identify the first error. We see this in the second and third assert:

assertEquals(
    right(Person(LastName("BARCLAY"), FirstName("Ken"), Email("me@gmail.com"))),
    Person.create("BARCLAY", "Ken", "me@gmail.com")
)
assertEquals(
    left("First name malformed: kenneth"),
    Person.create("BARCLAY", "kenneth", "me@gmail.com")
)
assertEquals(
    left("Last name malformed: Barclay"),
    Person.create("Barclay", "kenneth", "me@gmail.com")
)



The code for the Dogs library can be found at:

https://github.com/KenBarclay/TBA