In this article I will present the full process of defining an isomorphic relationship among two types and testing it. Don’t worry if it sounds tough, this is an extremely simple concept but really powerful and it can be useful on your day-to-day coding.
First of all, what is an isomorphism and why should you care? Well, without going through a full mathematical explanation (which I would probably do incorrectly) two types A and B are isomorphic if you can go from A to B and from B to A without information loss. A different explanation could be “the two types are two different ways of representing the same thing, and we know how to convert from one to the other”.
For example, String and List[Char] are isomorphic. You can transform from String to List[Char] using function toList, and from List[Char] to the same string with mkString(“”). On the contrary, Double and Int are not isomorphic. You can go from a Double to an Int with toInt and back with toDouble, but all the decimals from the Double will be gone so you don’t get the same number, there has been some loss of information.
In the real world you can find isomorphisms when transforming from input data to your internal representation. This is why you should care! If you change one of the types, you want to make sure that the other is changed too in a consistent way.
For example, imagine that you receive information about a client from a form and want to work with it. This how you input case class and your internal representation could look like.
In order to go from one type to the other, the transformation functions could look like
But hey! this is Scala, so there is a library to do everything you want to do. In this case we are using Monocle, a library for Optics in Scala to define both types as isomorphic. To do so, we define a Iso relationship from Person to PersonFlat specifying how to convert from one type to the other.
And then, creating our transformation functions is trivial
Up to now, this is not so useful, I agree. But now is when things get interesting.
What we want to do now is to fully test this. Not doing a couple of trivial tests, but a real demonstration that our converter works as expected. This is of outmost importance, because every piece of software must be fully reliable or it will ruin your life when you less expect it. So we are using Scalacheck for some Property Testing.
First, we define how to generate random instances of our types. Note that, as Gen is a Monad, we can flatMap it and compose. This way, defining a complex Person object is done by using both the existing generators and by defining a new one, addressGenerator
Now that we have generators, we can demonstrate that our Iso transformation does what we expect to. This means demonstrating that, for every element that our generator can create, the transformation works correctly.
But hey! As I said, Scala has a library for everything you want to do. In this case, Monocle comes with some tools for this kind of tests. It includes the two I have already coded plus some extra ones for you math geeks out there.
Now we KNOW that our transformation is correct, and if we change anything in the future, some tests will detect it and let us know that we inserted a bug.
This example show how to use this technique with Scala scripts, but you probably want to use classes in your code. Moreover, you probably don’t want to code all this tests for every converter that you create. What is the solution? Put all the tests into a trait with ̶F̶u̶n̶S̶u̶i̶t̶e̶ PropSpec (part of Scalacheck), make it generic on the types, and receive the data generator as parameters. This way, only by creating a test class for each converter and specifying the generators, you get all the tests :)
This is how it looks in action
Altogether, this approach creates reliable and easy to tests converters. And it is cool.