Open Credo

June 3, 2016 | Software Consultancy

The Destructor Pattern

Complexity warning

In this post, I’m going to take something extremely simple, unfold it into something disconcertingly complex, and then fold it back into something relatively simple again. The exercise isn’t entirely empty: in the process, we’ll derive a more powerful (because more generic) version of the extremely simple thing we started with. I’m describing the overall shape of the journey now, because programmers who don’t love complexity for its own sake often find the initial “unfolding” stage objectionable, and then have trouble regarding the eventual increase in fanciness as worth the struggle.

WRITTEN BY

Dominic Fox

Dominic Fox

The Destructor Pattern

In mathematics, the path to increased fanciness often leads through dense thickets of abstraction, but once you get there you can essentially forget about the complex machinery used to arrive at the result; in programming, we put the machinery into libraries and get on with using the APIs they provide. Compare the simplicity of what the Jackson JSON-mapping library for Java enables you to do – turn POJOs into JSON and vice versa – with the complexity of the implementation. (Actually, very few users of the library have any notion of how complex the implementation really is, because there’s seldom any need to look under the hood – and that’s the point.)

This is another Kotlin post, although there’s a bit of Scala as well. The pattern discussed here can also be applied in Java 8, but it’s slightly heavier there (you need to define a few interfaces).

Constructing and destructuring data classes

Here’s the simple thing. In Kotlin, we can define an immutable value type using a data class, like so:

data class Person(val name: String, val dateOfBirth: LocalDate, val favouriteColour: String)

The primary constructor of the Person class can be seen as a function of arity 3 (that’s a fancy way of saying “taking three arguments”), and can be accessed as such using the :: syntax:

val ctor: (String, LocalDate, String) -> Person = ::Person

// These are equivalent:
val person1 = Person("Tim", LocalDate(1984, 12, 30), "mauve")
val person2 = ctor("Tim", LocalDate(1984, 12, 30), "mauve")

The type signature of ctor, (String, LocalDate, String) -> Person tells us that given a String, a LocalDate, and another String, we can create a Person. It doesn’t tell us which properties of the Person each of these values is assigned to: we have to infer that from the position of the arguments in the declaration of Person‘s primary constructor. The ordering matters:

// These are equivalent
val person1 = Person(name="Tim", dateOfBirth=LocalDate(1984, 12, 30), favouriteColour="mauve")
val person2 = Person(favouriteColour="mauve", dateOfBirth=LocalDate(1984, 12, 30), name="Tim")

// These are not
val person3 = Person("Tim", LocalDate(1984, 12, 30), "mauve")
val person4 = Person("mauve", LocalDate(1984, 12, 30), "Tim")

On the flip side (or “dually”, in pretentious mathematical language), Kotlin allows us to destructure an instance of a data class into its component properties, like so:

val (name, dob, favColour) = person

// These are equivalent
println("$name, born on $dob, likes $favColour")
println("${person.name}, born on ${person.dateOfBirth}, likes ${person.favouriteColour}")

If you squint at it a bit, you can see that the destructuring assignment of person into its component parts looks a bit like a sort of “reverse function” (String, LocalDate, String) <- Person, the mirror image (or, pretentiously, "dual arrow") of Person -> (String, LocalDate, String). Wherever we have a Person, we can get a String, a LocalDate and a String; wherever we have a String, a LocalDate and a String, we can get a Person. What's more, we can "round-trip" a Person through destructuring and construction, like this:

val (name, dob, favColour) = person
val clone = Person(name, dob, favColour)
val (cloneName, cloneDob, cloneFavColour) = clone
// etc

Destructuring assignment in Kotlin is provided for any type that implements the functions component1(), component2()...componentN(), and every data class has these functions generated for it automatically. A "component" of a data class is a function projecting one of its properties, correlated by position index (rather than by name) with the primary constructor argument that sets that property.

A Spectre Is Haunting Kotlin

Seen in the light of constructor functions and destructuring assignment, data classes behave like typed tuples of values, which happen to have been enriched with names. An earlier version of Kotlin actually had classes representing typed tuples, e.g. Tuple3, but these were removed in favour of the more explicit mechanism of declaring data classes. In most cases, code that consumed a Tuple3 would really be interested in dealing with a specific kind of tuple in which the first, second and third arguments had specific meanings, e.g. the first is the name of a person, the second is their date of birth, and the third is their favourite colour. It's clearer, then, to pass in a Person, where all of these things are clearly labelled and there's no danger of getting the name and favourite colour in the wrong order, or of accidentally passing in a tuple of three values of matching types that meant something completely different.

Even so, the ghost of tuples still haunts data classes: we can always choose to peel off the labels and deal with them in their naked tupley-ness. And sometimes - not typically, but in some special cases - that's precisely what we want to do.

The problem is that the (String, LocalDate, String) in the type signature of ctor isn't the type of a tuple, but a list of the types of three function arguments; and the (name, dob, favColour) in our destructuring assignment example also isn't a tuple: each of name, dob and favColour is assigned separately by that expression. We don't actually have a function from Person -> Tuple3 or from Tuple3 -> Person: we have two things that work a bit like those functions, but that can't be plugged directly into each other.

Meanwhile, In Scala

In Scala, the apply and unapply methods generated for case classes are very close to the functions we need. It turns out that Person.unapply(person).map(Person.tupled) == Some(person) and Person.unapply(Person.tupled(tuple)) == Some(tuple) - we can round-trip case classes through tuples and vice versa. This means that, with a little abuse of Option, we can treat case classes and their matching tuple types (pretentiously) as isomorphic, and so we can write generic functions that work on tuples and use them to manipulate case class instances. Here's an example, which uses a function that encrypts all the String elements in a 3-tuple to build a function that will encrypt all the fields of a case class instance with three properties:

object Crypto {

  def main(args: Array[String]): Unit = {
    case class Person(name: String, dateOfBirth: LocalDate, favouriteColour: String)

    def encryptValue[T](value: T): T =
      value match {
        case stringValue: String => s"CONFIDENTIAL: $stringValue".asInstanceOf[T]
        case _ => value
      }

    def encryptTuple3[A, B, C](tuple3: (A, B, C)): (A, B, C) = {
      val (a, b, c) = tuple3
      (encryptValue(a), encryptValue(b), encryptValue(c))
    }

    def tuple3ToCC[A, B, C, CC](apply: (A, B, C) => CC, unapply: CC => Option[(A, B, C)])
                               (f: ((A, B, C)) => (A, B, C)): CC => Option[CC] =
      caseClass => unapply(caseClass).map { tuple => apply.tupled(f(tuple)) }

    val personEncryptor = tuple3ToCC(Person.apply, Person.unapply)(encryptTuple3)

    val person = Person("Tim", LocalDate.of(1984, 12, 30), "mauve")
    val encryptedPerson = personEncryptor(person)

    encryptedPerson.map { println }
  }
}

The function tuple3ToCC enables us to plug the encryptTuple3 function into any class for which we have apply and unapply methods with appropriate signatures. We are effectively mapping over the Person case class's properties, in a type-safe way, without using reflection.

Kotlin Isn't Scala, Stupid

In Kotlin, we have half of what we'd need to do the same thing: the ::Person constructor function, which corresponds precisely to Scala's generated Person.apply. In this section, I'll demonstrate a tricksy way of implementing the other half, without introducing Tuple types back into the picture.

To begin with, we define a destructor function as an extension function which extracts values from its target and sends them to a receiver function which accepts them as arguments:

data class Person(val name: String, val dateOfBirth: LocalDate, val favouriteColour: String)

// Captures a destructor function for a type with three components
fun  dtor3(dtor: IN.(receiver: (A, B, C) -> Unit) -> Unit) = dtor

val ctor = ::Person

// Create a destructor function for Person
val dtor = dtor3 { f -> f(name, dateOfBirth, favouriteColour) }

// Use the destructor to send a Person's components to a receiver
val person = Person("Arthur Putey", LocalDate.of(1984, 12, 30), "mauve")
person.dtor { a, b, c -> println("$a, $b, $c") }

Now, we would like to be able to demonstrate round-tripping here by writing something like val clone = person.dtor(ctor), making the constructor function the receiver for the components extracted by the destructor. The problem is that our destructor function returns Unit - it can "do something" with the values it's given, but not return a result. We need a way to patch a transformer function of type (A, B, C) -> OUT into a receiver function, and retrieve the result. Here it is:

fun  patch3(
        dtor: IN.(receiver: (A, B, C) -> Unit) -> Unit,
        f: (A, B, C) -> OUT): (IN) -> OUT =
        fun(value: IN): OUT {
            var result: OUT? = null
            value.dtor { a, b, c -> result = f(a, b, c) }
            return result!!
        }

Yuck, right? Just pretend it's in a library and you'll never have to look at it again. Here's how we can use it to patch ctor and dtor together:

val makeClone = patch3(dtor, ctor)
val clonedPerson = makeClone(person)

That may seem like a lot of work to replace person.copy(). But now that we have patch3, we can also write

fun  encryptValue(value: T): T = when(value) {
    is String -> "CONFIDENTIAL: $value" as T
    else -> value
}

fun  encrypt3(f: (A, B, C) -> OUT): (A, B, C) -> OUT
        = { a, b, c -> f(encryptValue(a), encryptValue(b), encryptValue(c)) }

val encryptor = patch3(dtor, encrypt3(ctor))
val encryptedPerson = encryptor(person)

The point I want to emphasise here is that this is about genericity and re-use. It's much, much simpler just to write

fun encryptPerson(person: Person): Person =
  Person(
    encryptValue(person.name),
    encryptValue(person.dateOfBirth),
    encryptValue(person.favouriteColour))

and that is almost always what you should actually do. However, what we can't do is write

fun  encrypt3(value: GENERIC_DATA_CLASS): GENERIC_DATA_CLASS =
  GENERIC_DATA_CLASS(
    encryptValue(value.component1()),
    encryptValue(value.component2()),
    encryptValue(value.component3()))

because we have no way of knowing, and no way of specifying in the type signature, that GENERIC_DATA_CLASS must have three components, and three componentN() functions. The functions patch3 and dtor3, as ugly as they are, provide a mechanism for applying generic logic (expressed through transformer functions) to any class for which we can provide a 3-component destructor function.

The payoff

What's this really useful for? In a word, mapping. Suppose we want to map the components of some object into parameters of a PreparedStatement, in order to perform a SQL insert.

fun  inserter3(dtor: DC.(f: (A, B, C) -> Unit) -> Unit): (PreparedStatement, DC) -> Boolean =
    { ps, value ->
        (patch3(dtor, { a, b, c ->
            with(ps) {
                setObject(0, a)
                setObject(1, b)
                setObject(2, c)
            }
            ps.execute()
        }))(value)
    }

val insertPerson = inserter3(dtor)
val succeeded = insertPerson(
  connection.prepareStatement("INSERT INTO Person(name, dob, favouriteColour) VALUES (?, ?, ?)"),
  person)

Again, this is absolutely generic across all types for which we can provide a 3-component destructor function, and makes no use whatsoever of reflection.

In summary, in the hopefully very rare cases where you are writing truly generic logic that works over types that can be broken down into a small, known number of component values, you should consider using the "destructor pattern" (or "Gozer pattern") outlined above. Use dtorN to create destructor functions to destructure specific types, and either use them directly with receiver functions or plug in transformer functions using patchN. I hope that, even if you never find a use for this, you've nevertheless found this discussion illuminating!

RETURN TO BLOG

SHARE

Twitter LinkedIn Facebook Email

SIMILAR POSTS

Blog