Introduction to writing a Scala object mapping library
The past few weeks I’ve been writing an object mapping library in Scala. Just for fun. So, what is it all about?
At Infi, we started our first Scala project mid-2016. When it became clear that Scala might be one of the technologies used in the project, I jumped at the chance to be part of it. I’m always eager to learn new tech, and doing a project in a functional programming language was already near the top of my professional wishlist.
As always, when learning a new technology, I like to push the envelope to see where things start to break down. I think it’s a nice way to get to know the limits of that technology. As it turns out, Scala is a powerful language, with a strong type system that lets you use many advanced concepts I won’t detail here (eg. type classes, high-level abstractions like the ones in the Typeclassopedia with the help of scalaz or Cats, generic programming with shapeless and much more).
The thing I want to discuss here is a common annoyance: mapping one type to another and the amount of boilerplate it takes to do so. How can we get rid of that silly, boring-to-write mapping code?
Some programming languages already have standard library functionality that limits the amount of boilerplate you need for type-mapping, especially the dynamically-typed languages (JavaScript and Python make it fairly easy). In other languages you may have to resort to libraries like AutoMapper or use runtime reflection.
Here’s how I tried to solve that problem in Scala.
The problem
It might be familiar: you have two unrelated composite data types that share some fields with the same name and type, and you need to map an instance of the first type to an instance of the second type. For example:
case class Foo(integerField: Int) case class Bar(integerField: Int)
Say you want to get a Bar instance from a Foo instance. This one is simple, right?
// using named arguments for explicitness val foo = Foo(integerField = 1) val bar = Bar(integerField = foo.integerField)
All clear, no doubt what’s happening here. But as your types get more fields, more mapping code needs to be written:
case class Foo(integerField: Int, stringField: String) case class Bar(integerField: Int, stringField: String) val foo = Foo(integerField = 1, stringField = "quux") val bar = Bar(integerField = foo.integerField, stringField = foo.stringField)
Also, when more types get added that you want to map to and from, you’ll get more lines that are a variation of the last one in the code above. Yuck.
In short: the amount of mapping code you need to write depends on two things:
- The size of your data types (let’s call this the quality axis)
- The number of data types (the quantity axis)
Possible solution #1: mapping code, concentrated at one place
So… In keeping things DRY, you don’t want to repeat the above kind of mapping code at each location you want to get a Bar from a Foo. You might create something like the Scala equivalent of a C# extension method that keeps all the boring boilerplate code in one place, separated from Foo, Bar and other future types to map:
// mappings.scala or some other generically-named file package nl.infi.thingamabob.data package object mappings { // map Foo to Bar, ooooh yeah implicit class MapFooToBarSyntax(val foo: Foo) extends AnyVal { def toBar: Bar = Bar(integerField = foo.integerField, stringField = foo.stringField) } } // DoodadService.scala trait DoodadService { def ohPleaseFooGiveMeAFlurpz(foo: Foo): Flurpz = { import nl.infi.thingamabob.data.mappings._ val bar = foo.toBar // wheee ??? // I'll get me a Flurpz, I just don't know how yet } }
You probably guessed it: as the number of classes that need mapping increases, the nl.infi.thingamabob.data.mappings package will grow over time, potentially becoming a maintenance burden. There might be a better way…
But what way is that? In my next blogpost I’ll explore the use of the shapeless library for a possible solution to this problem. Stay tuned!