Thursday, March 31, 2022

Kotlin: Functional Domain Modeling #3

 Kotlin: Functional Domain Modeling #3


In this blog we introduce the applicative functor pattern. The central idea behind the pattern is the sequencing of effects. Validation is one use case for which applicative functors are useful. Given a series of validation effects, regardless of the result (failure or success) they guarantee all will be executed independently.



Applicative functor

Consider filling out a web form to sign up for an account. You enter your user name and password and the system responds by saying that a user name may not contain dashes. When you make the necessary changes and resubmit you might now be informed that the password must have at least one capital letter.

It would be nice to have all the errors reportedly simultaneously. We have seen that the monadic Either fails fast, reporting the first validation error it detects. Another approach is to use the Validation class that is comparable to Either:

sealed class Validation<E, A> {
    data class Failure<E, A>(val value: E) : Validation<E, A>()
    data class Success<E, A>(val value: A) : Validation<E, A>()
}

in which the type parameter E represents the error type and the type parameter A represents the obtained value.

The following shows validating the LastName with the Validation class replacing our earlier version with Either. Function create returns a Validation object with a String for its error type and LastName for its payload. The asserts show a successful creation and a failure creation.

data class LastName(val lastName: String) {

    companion object {

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

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

    }

}   // LastName


assertEquals(success(LastName("BARCLAY")), LastName.create("BARCLAY"))
assertEquals(failure("Last name malformed: Barclay"), LastName.create("Barclay"))

The Validation class is accompanied with the typealias:

typealias ValidationNel<E, A> = Validation<NonEmptyList<E>, A>

where the error type for ValidationNel is a NonEmptyList of errors of type E. The NonEmptyList reflects the fact that if there are validation errors then one or more will be reported. ValidationNel is supported with the factory functions failureNel and successNel, substitutes for the functions failure and success.

The next example repeats the above, this time using ValidationNel. Note how the failing assert packs the single error into a NonEmptyList.

data class LastName(val lastName: String) {

    companion object {

        fun create(lastName: String): ValidationNel<String, 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


assertEquals(successNel(LastName("BARCLAY")), LastName.create("BARCLAY"))
assertEquals(failureNel("Last name malformed: Barclay"), LastName.create("Barclay"))
typealias ValidationNelError<A> = ValidationNel<ValidationError, A>

Rebuilding our LastName, FirstName, MiddleName and Name using ValidationNel we have:

data class Name(val lastName: LastName, val firstName: FirstName, val middleName: Option<MiddleName> = none()) {

    companion object {

        fun create(lastName: String, firstName: String, middleName: String = ""): ValidationNel<String, Name> {
            val vLastName: ValidationNel<String, LastName> = LastName.create(lastName)
            val vFirstName: ValidationNel<String, FirstName> = FirstName.create(firstName)

            return if (middleName == "")
                fmap2(vLastName, vFirstName){ln, fn -> Name(ln, fn) }
            else {
                val vMiddleName: ValidationNel<String, MiddleName> = MiddleName.create(middleName)
                fmap3(vLastName, vFirstName, vMiddleName){ln, fn, mn -> Name(ln, fn, some(mn)) }
            }
        }   // create

        fun create(name: String): ValidationNel<String, Name> {
            val names: List<String> = name.split(" ")
            val size: Int = names.size

            return if (size == 0)
                failureNel("Name.create: empty name not supported")
            else if (size == 1)
                failureNel("Name.create: name must exceed 1 part")
            else if (size > 3)
                failureNel("Name.create: name must not exceed 3 parts")
            else if (size == 2) {
                val vLastName: ValidationNel<String, LastName> = LastName.create(names[0])
                val vFirstName: ValidationNel<String, FirstName> = FirstName.create(names[1])
                fmap2(vLastName, vFirstName){ln, fn -> Name(ln, fn)}
            } else {
                val vLastName: ValidationNel<String, LastName> = LastName.create(names[0])
                val vFirstName: ValidationNel<String, FirstName> = FirstName.create(names[1])
                val vMiddleName: ValidationNel<String, MiddleName> = MiddleName.create(names[2])
                fmap3(vLastName, vFirstName, vMiddleName){ln, fn, mn -> Name(ln, fn, some(mn)) }
            }
        }   // create

    }

}   // Name





If all three ValidationNel<String, ...> instances indicate a successful validation we extract the validated arguments and pass them to a pure function f that constructs the validated result object. In case of errors they are reported to the caller of Name.create. The functions that we use are fmap2 and fmap3 with the signature for the former:

fun <E, A, B, C> fmap2(
    vea: ValidationNel<E, A>, veb: ValidationNel<E, B>, f: (A, B) -> C
): ValidationNel<E, C>

This is an example of the applicative functor pattern of functional programming. The pattern is used when you deal with effects in functional programming. These applicative effects refer to the way the effects are applied. Our application of fmap3 demonstrates that the effects are sequenced through all the validation functions regardless of the failure or success they produce. This is shown in the following asserts:

assertEquals(
    successNel(Name(LastName("BARCLAY"), FirstName("Ken"), some(MiddleName("Andrew")))),
    Name.create("BARCLAY", "Ken", "Andrew")
)
assertEquals(
    failureNel(NonEmptyListF.of("Last name malformed: Barclay")),
    Name.create("Barclay", "Ken", "Andrew")
)
assertEquals(
    failureNel(NonEmptyListF.of("First name malformed: ken")),
    Name.create("BARCLAY", "ken", "Andrew")
)
assertEquals(
    failureNel(NonEmptyListF.of("Middle name malformed: andrew")),
    Name.create("BARCLAY", "Ken", "andrew")
)
assertEquals(
    failureNel(NonEmptyListF.of("First name malformed: ken", "Middle name malformed: andrew")),
    Name.create("BARCLAY", "ken", "andrew")
)

The final assert demonstrates how the applicative pattern fails slow, identifying two separate validation failures.



The applicative functor

The functions fmap2 and fmap3 are generalizations of fmap, to functions with more than one parameter. With F the actual data type place-marker, the overloaded function fmap2 has the signatures:

fun <A, B, C> fmap2(fa: F<A>, fb: F<B>, f: (A) -> (B) -> C): F<C>
fun <A, B, C> fmap2(fa: F<A>, fb: F<B>, f: (A, B) -> C): F<C> = fmap2(fa, fb, C2(f))

Note how the second version with the function f presented in its uncurried form can be defined in terms of the first version using the Dogs function C2 to curry its function parameter. The Dogs library includes the currying functions C2, C3, C4 and C5.

Whilst the Dogs library includes functions fmap2 and fmap3 across all types that are applicative functors, the question is whether we should also include fmap4, fmap5, etc, making the code very tedious.

The solution gives rise to the notion of an applicative functor. With F the place-marker for the actual type for which it applies, the applicative functor is described with the ap extension function (representing sequential application) and the function pure:

fun <A, B> F<A>.ap(f: F<(A) -> B>): F<B>
fun <A> pure(a: A): F<A>

With these two basic functions we can construct any mapping function fmapN. The function pure converts a value of type A into the context F<A>. If the context is, say List, then function pure creates a singleton List with that single value. Function ap is a generalized form of function application. We have a function (A) -> B, a value of type A, and a result of type B, all wrapped in a context F.

A typical use of these functions is shown in the following asserts:

assertEquals(some(3), some(2).ap(pure{m: Int -> m + 1}))
assertEquals(some(3), pure{m: Int -> m + 1} appliedOver some(2))
assertEquals(some(5), pure{m: Int -> {n: Int -> m + n}} appliedOver some(2) appliedOver some(3))

The function appliedOver is an infix version of function ap with the function given first. The final assert is often known as the applicative style because of its similarity to normal function application.

This final example informs us how we can now define fmap2:

fun <A, B, C> fmap2(fa: F<A>, fb: F<B>, f: (A) -> (B) -> C): F<C> =
    pure(f) appliedOver fa appliedOver fb

We see that whilst we could continue with this to define fmap3, fmap4, etc, we can simply employ the applicative style to get what we require.

Here it is defined for the Option type:

fun <A, B, C> fmap2(oa: Option<A>, ob: Option<B>, f: (A) -> (B) -> C): Option<C> =
    pure(f) appliedOver oa appliedOver ob

assertEquals(some(5), fmap2(some(2), some(3)){m -> {n -> m + n}})



The code for the Dogs library can be found at:

https://github.com/KenBarclay/TBA
https://github.com/KenBarclay/TBA


No comments: