If you have a background in Java like me you probably read Refactoring To Patterns.
It’s a very cool book about refactorings that shows you how to refactor Object Orientated code step by step and eventually reach full blown Gang of Four design patterns. It had a huge impact on me at the time. It left me with the feeling that code is alive and wants to be rearranged this way, and that patterns emerge naturally.
Fast forward 10 years, and I work in a very cool startup (Bigpanda) where we use Scala and Functional programming for our back end services. Working with FP is very different, and in my opinion far easier. The are no more complicated class hierarchies and no more complicated patterns, only functions, functions, functions. If GoF design patterns are no longer our destination, then the refactoring steps we must take are very different.
In this post I’ll introduce you to some approaches to refactoring functional code. I will build from simple refactorings to more advanced ones using the State and Writer monads — functional design patterns.
Make sure you have a full suite of tests with good coverage
Refactoring without tests is like jumping without a safety net. You can use sbt as a very useful continuous test runner:
Each time you save your file it will recompile it and rerun only the previously failing tests
Eliminate primitive types with value classes
We have a package called
types and we will put all our value classes in
values.scala file We will also add
Ordering implicits there.
Rewrite on the side and then switch the functions
Typically I do not start by deleting old code. First I write the new function on the side, make sure it compiles and then switch the old ones and make sure the tests pass. This is very handy trick to let you backtrack quickly if you have an error somewhere.
Align the types between functions
If your functions compose together in a natural way, it means that you have found the right level of abstraction.
Keep them small and focused on one thing, add type annotations for the return types to increase readability.
If you find that you need to work hard with type transformations to be able to compose your functions together then try this:
- Inline, inline, inline, and retry.
After a while you get that hang of it and your functions will be focused and compose together. You can also do some upfront design.
Personally I found A Type Driven Approach to Functional Design helpful. It’s in Haskell but it is still very relevant and will give you a sense of how to design functions that compose together.
Use State monad for functions that need previously computed values
Let’s define some types to work with:
In any meaningful service you will need previously computed data. You’ll also want to persist it in case you crash or restart your app. This lead to sateful functions.
In order to rate a stock we need the previous prices and rating, this usually leads to long ugly parameter lists. Because our data structures are immutable so we have to return new updated versions of them.
This is a very common pattern in FP. You can use
State monad to communicate to the reader that they are about to deal with stateful functions.
We use cat’s State.
We encapsulate the moving parts in our own defined type :
State to cleanup the parameter list and the return type :
We can improve the type readability with a type alias:
The function is far cleaner and it can compute and update the ratings from the previous state.
This gives us the ability to chain state functions as follows and be guaranteed that each function receive the correct latest updated state, very cool !!!
Writer monad to track state transitions when using
If you work with Event sourcing you will want to recreate your state from all the transitions you carried out. In order to keep track of state transitions without complicating your function you can use
Writer monad to log all the transitions in a List.
First let’s define some more types:
We want to use State and Writer together, so let’s use WriteT to combine them:
Use this function to log transitions and add it to the final transition list:
Boilerplate to wire up State and Writer together
Pure functions have simple return types that are not wrapped in
StateActionWithTransitions. This tells the the reader that this function does not change the state.
Stateful functions have the return type
StateActionWithTransitions. This tells the reader to pay special care because this function uses or updates the state:
Here is the final version of our function:
- Whenever the reader sees
<-he knows to pay special attention as it is a stateful function
- Whenever the reader sees
=he knows it's a pure function and nothing related to state happens there
- Before refactoring make sure you have good tests with decent coverage
- Strongly type as much as you can, use meaningful names and abstractions
- Design your functions so their types align and compose together
- Use cats’s
Statedata type to write functions that need state
- Use type aliases to cleanup boilerplate types
- Use cat’s
Writerdata type to log state transitions