Recently I was playing around with Rapture, an awesome utility library created by Jon Pretty. One thing that really stuck with me was the concept of modes. Modes are classes that allow us to modify the type returned by a given library call. You probably wonder how we can do that in Scala. I was curious too, so I dig a bit into the code and this is my answer.
Here’s a typical use case for Modes in Rapture - parsing Json string. Let’s define few parsing functions
As you can see the only difference between them are the imports. Those imports have a special meaning in Rapture - they are used to bring specific modes into scope. When imported it gets passed as an implicit param into the parse function to modify it’s behavior. Let’s take look on a example
and the output is
The example above doesn’t do much. It shows parsing two Json strings - one correct and one badly formatted, with different modes in scope. As you can see although we are calling the same
Json.parse function the results we are getting have different types. Depending on the imported mode we can get either
Try or an unwrapped value. In the case of
Future mode the result is asynchronous! This gives us a lot of flexibility on how to handle Json. But how does it work? It’s clearly not returning
Any - the
Future example shows that we can use
onFailure without a problem. Is
Json.parse overloaded to handle different cases? No. That would be too cumbersome to maintain and extend. So how it’s implemented?
In order to understand how Rapture achieves this level of modularity, we need to take a step back and try implement something similar, but simpler. When we grasp the idea on a tiny example we will be able to apply our knowledge to a bigger example in Rapture. Our example will cover a simple example: safe division - we want to be able to divide two numbers and get their results. As dividing by 0 is usually an error, we want to catch that.
That’s easy. Here’s a “un-modded” example:
Since it will be an utility class we want to allow our end users to decide on how the calculated value will be returned. We will use modes for that. Division using modes is slightly more complex, but not hard to understand. First let’s define a mode. A trait will work fine for that
Let’s stop here for a second and discuss this code a bit. First we define
ResultWrapper type. As the name suggests it wraps our possible results. It’s generic and parametrized with two types. The first one, being a covariant(
+ in the definition) anything(expressed with
_) will be used as to hold the correct result. The second type parameter (
_ <: Exception will be used to handle errors). Based on that we can define an abstract
Wrap has two type params
ErrorResult. It takes a function, which we will refer to as body, that returns the generic
Result. The final return type from
ResultWrapper[Result, ErrorResult]. Nothing new here.
On it’s own a trait won’t do much. Let’s implement concrete classes - modes for
Try example is slightly easier, so let’s discuss it first. We define
ResultWrapper as simple
Try. We keep it generic, so we can use
tryMode with every
body we want to. The wrap function is also very straightforward - we simple invoke the body inside a
Try to catch the output - both successful divisions and exceptions. The second example does exactly the same thing, but it’s slightly longer due to the explicit
try-catch. Nothing fancy.
Now that we have all the pieces in place we can move to solve the problem at hand. Here’s the upgraded
This time it takes a third param, being the mode. We could pass it as an implicit as Rapture does it, but implicit isn’t really required here. The code block with division is passed to
mode.wrap for evaluation and from that we get the final result. You might be surprised but this is all it takes to write a moddable function from scratch.
One thing to notice in the example above is the (explicit for clarity) return type -
mode.ResultWrapper[Int, ArithmeticException]. It’s parametrized with
ArithmeticException to handle the possible divide by zero error and with
Int which is the expected type for
Int division. If you compare it with our modes you will quickly understand how the return type is modified … in fact it stays the same! The only difference that for
mode.ResultWrapper[Int, ArithmeticException] is defined as
Try[Int] and for
mode.ResultWrapper[Int, ArithmeticException] is a
Option[Int]. So we keep it typesafe.
(Question for the curious: In this example we are using
Int as the type for division although
Double would be more useful, do you know why? If so, leave us a comment below.)
Now let’s move back to Rapture. For a better understanding of modes we will write ourselves one. It won’t do anything sensible, just return a one or zero element
As you can see it’s not very different from our previous code. We made it
implicit for easier passing and we implement the interface
Mode[ParseMethods] required by Rapture. Turns out it’s all it takes.
As we established earlier you can use modes in Rapture and with custom code, but what about all the code you’ve written before and you would like to “mode-ify”? When working with Rapture you can do that in no-time. Just use the convenient
In this case we transform the return type from
Option[T], but you could easily pass the mode as parameter.
As you can see implementing modes is surprisingly easy and comes a with few benefits. The biggest one being the fact, that they don’t force the end users of our API to tie closely to a single return type. With the moddable approach the decision on how to serve the data is made by end users, which know exactly what is the best fit for their needs. Since they aren’t tied they are also free to experiment and change their code without having to wonder if the API will work fine with their improved codebase.
I hope I managed to convince you that modes are a very useful tool for writing flexible code. Especially when you don’t know how your API will be used by client code. Again Scala’s type system proved to be powerful enough to express even the most difficult type requirements. Modes are not the only cool thing that you can find in Rapture. I encourage you to check it out.
PS. Huge thanks to Rapture committers for creating this library, especially Jon Pretty for creating Rapture and giving a talk that inspired me to write this post.