Thursday, October 27, 2022

Kotlin: Functional Domain Modeling #5

 Kotlin: Functional Domain Modeling #5


In this blog we introduce the traversable pattern. Traversable structures are collections of elements that can be operated upon with an effectful visitor operation. The visitor function performs a side-effect on each element and composes those side effects whilst retaining the original structure.



Traversables

This is how our Book class appears:

data class Book(
    val title: String,
    val publisher: String,
    val isbn: ISBN,
    val authors: NonEmptyList<Author>
)

Using the smart constructor idiom we have (for the Book class):

fun create(
    title: String,
    publisher: String,
    isbn: String,
    vararg authors: ValidationNel<String, Author>
): ValidationNel<String, Book>

Note the authors parameter to function create. It is marked as a vararg so we can pass a variable number of actual parameters at the call site, including none. The type is ValidationNel<String, Author> and represents the effectful validation returned from calls to the smart constructor Author.create.

Here is the complete class declaration:

data class Book(
    val title: String,
    val publisher: String,
    val isbn: ISBN,
    val authors: NonEmptyList<Author>
) {

    companion object {

        fun create(title: String, publisher: String, isbn: String, vararg authors: ValidationNel<String, Author>): ValidationNel<String, Book> {
            val vISBN: ValidationNel<String, ISBN> = ISBN.create(isbn)
            val vAuthors: ValidationNel<String, Array<Author>> =
                if (authors.isEmpty())
                    failureNel("Book.create: must have one or more authors")
                else
                    authors.sequenceValidationNel()

            return fmap2(vISBN, vAuthors){isn: ISBN, auths: Array<Author> ->
                Book(title, publisher, isn, auths.toNonEmptyList())
            }
        }   // create

    }

}   // Book

Within the body of the create function of class Book the vararg parameter authors is considered an Array<ValidationNel<String, Author>>. The binding for vAuthors is a failure if this array is empty. If authors is non-empty then the binding for vAuthors is to:

authors.sequenceValidationNel()

We shall return to this shortly.



The iterator pattern

Perhaps the most familiar of the object oriented design patterns is the iterator pattern, which provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. Traditionally, this is achieved by identifying an iterator interface that presents operations to initialize an iteration, to access the current element, to advance to the next element, and to test for completion; collection objects are expected to implement this interface.

This traditional version of the pattern is sometimes called an external iterator. An alternative internal iterator approach assigns responsibility for managing the traversal to the collection instead of the client: the client needs only to provide an operation, which the collection applies to each of its elements.

Now recall the idea behind the functor function fmap. When we use fmap, we can take any functor structure (such as List or Option from the Dogs library) and transform all the underlying elements of that functor, returning a new object with the exact same structure, but different elements. The new elements might be of the same type or they can be entirely different.

Traversable structures are containers of elements that can be operated upon with an effectful visitor operation. The visitor function performs a side-effect on each element of the structure and composes these side effects with an applicative. The traversable abstracts this capability with the traverse function:

fun <A, B> F<A>.traverse(f: (A) -> G<B>): G<F<B>>

Once again F is the place-marker for the actual type for which it applies. The types Option, Either, Validation, ArrayList and Map from the Dogs library all support the traversable. The function parameter transforms an element from the context using an applicative. Thus G is expected to be some applicative type such as Option, Validation or List.

We have already been performing traversals with the functor function fmap and the foldable function foldMap. Function fmap walks across the collection, applies a transformer operation to each element and collects the results by rebuilding the collection. Similarly, function foldMap walks across the collection applies the transforming function and collects the results by combining them with the given monoid. Function traverse provides a further useful way for traversing a collection.

As a comparison consider iterating across a list using fmap and the Option-encoded test for negative Ints:

fun deleteIfNegative(n: Int): Option<Int> =
    if (n < 0) none() else some(n)

assertEquals(
    ListF.of(none(), some(3), some(2), none(), some(0)),
    ListF.of(-5, 3, 2, -1, 0).fmap(::deleteIfNegative)
)

By contrast, function traverse creates an applicative summary of the contexts within a structure, and then rebuilds the structure in the new context.

assertEquals(
    none(),
    ListF.of(-5, 3, 2, -1, 0).traverseOption(::deleteIfNegative)
)

assertEquals(
    some(ListF.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)),
    ListF.closedRange(1, 10).traverseOption(::deleteIfNegative)
)

Given deleteIfNegative, a function that returns an Option effect, function traverseOption threads this effect through the running of this function on all the values in the List, returning a List<Int> in an Option context.

Alongside function traverse, the traversable also includes the function sequence:

fun <A> F<G<A>>.sequence(): G<F<A>>

This function threads all the G effects through the F structure and inverts the structure from F<G<A>> to G<F<A>>. Two examples are:

assertEquals(some(ListF.of(1, 2, 3)), ListF.of(some(1), some(2), some(3)).sequenceOption())
assertEquals(none(), ListF.of(some(1), some(2), none()).sequenceOption())

Function sequence turns the traversable inside out.



The travesrsable Array

The Array extension function sequenceValidationNel has the signature:

fun <E, B> Array<ValidationNel<E, B>>.sequenceValidationNel(): ValidationNel<E, Array<B>>

The function threads all the ValidationNel effects through the Array structure to invert the structure into an ValidationNel<E, Array<B>>. So we turn an Array<ValidationNel<E, B>> into a ValidationNel<E, Array<B>>. The functional community consider sequence as a member of the traversable pattern. Function sequence is somewhat analogous to performing a fold operation - it creates an applicative summary of the contexts (the ValidationNel) within a structure (the Array), and then rebuilds the structure (the Array)  in the new context (the ValidationNel).

In function create of class Book we use function sequenceValidationNel to traverse the array of ValidationNel<String, Author> produced by the function Author.create. A simple test is:

assertEquals(
    successNel(
        Book(
            "Kotlin in Action", "Manning", ISBN("9781617293290"),
            NonEmptyListF.of(
                Author(Name(LastName("JEMEROV"), FirstName("Dmitry"), none()), 1992),
                Author(Name(LastName("ISAKOVA"), FirstName("Svetlana"), none()), 1995)
            )
        )
    ),
    Book.create("Kotlin in Action", "Manning", "9781617293290",
        Author.create("JEMEROV", "Dmitry", "", 1992),
        Author.create("ISAKOVA", "Svetlana", "", 1995)
    )
)



The code for the Dogs library can be found at:

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

No comments: