June 3, 2016 | Software Consultancy
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
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).
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.
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<A, B, C>
, but these were removed in favour of the more explicit mechanism of declaring data classes. In most cases, code that consumed a Tuple3<String, LocalDate, String>
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<String, LocalDate, String>
or from Tuple3<String, LocalDate, String> -> Person
: we have two things that work a bit like those functions, but that can’t be plugged directly into each other.
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.
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 <IN, A, B, C> dtor3(dtor: IN.(receiver: (A, B, C) -> Unit) -> Unit) = dtor
val ctor = ::Person
// Create a destructor function for Person
val dtor = dtor3<Person, String, LocalDate, String> { 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 <IN, OUT, A, B, C> 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 <A, B, C, OUT> 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.
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 <DC, A, B, C> 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!
This blog is written exclusively by the OpenCredo team. We do not accept external contributions.
Agile India 2022 – Systems Thinking for Happy Staff and Elated Customers
Watch Simon Copsey’s talk from the Agile India Conference on “Systems Thinking for Happy Staff and Elated Customers.”Lean-Agile Delivery & Coaching Network and Digital Transformation Meetup
Watch Simon Copsey’s talk from the Lean-Agile Delivery & Coaching Network and Digital Transformation Meetup on “Seeing Clearly in Complexity” where he explores the Current…When Your Product Teams Should Aim to be Inefficient – Part 2
Many businesses advocate for efficiency, but this is not always the right goal. In part one of this article, we explored how product teams can…