Functional constructs for better readability of Kotlin code
I happened to see this type being returned from long function call today, that set me down a rabbit hole
Try<CustomerInquiryResponse?>?
But first, what is a Try
?
It is a construct from the Arrow library, which is used to wrap the result of an operation to be either a value (if the operation was successful) or an Exception, if the operation raised an exception. Have a look at the documentation here for more information.
All good, but what caught my eyes was the fact that the Try
was wrapping a nullable, and the Try
object was itself nullable.
Let’s have a look at the code around this type to get a better idea
This, to me is more complicated than it needs to be. Much so because when I have a look at this type, I have no idea as to the different conditions under which
i) CustomerInquiryResponse
is going to be null, and
ii) even less of an idea as to when Try<CustomerInquiryResponse?>
itself is going to be null.
Code should scream intent. This one doesn’t.
What would I have liked better? A different Arrow construct, for a start
Either<Problem, CustomerInquiryResponse>
An Either is another datatype provided by the Arrow library. It is a monad, that represents two possibilities, designated by a left and right side.
In the example above, on the right hand side, we have a CustomerInquiryResponse
object, which is non-nullable in our case. This will be returned by the monad if the computation was successful.
On the left, we have a Problem
which is a sealed class that would describe all the various issues that could happen when the customer inquiry response is computed.
This makes the contract extremely clear, which was lacking in the previous example, in my opinion.
But do we even have to use an external library?
Let’s first take a step back to Try
with the nullables, let us see how the value stored in is unwrapped in, say the offer()
method:
This makes a good revelation, which is that the author did not intend to differentiate between the cases where:
i) the customerInquiryResponse
(the Try<>
object) is null
ii) the inquiryResponse
is null
iii) if the Try
was wrapping a failure
In all three cases, customerId
will be null, and the code will chug along. There is no separate logic for each of the three conditions.
Considering this, you could just written the code like this
Advice: Maybe you don’t always require to use a library, plain and simple Kotlin can sometimes do the job.
But let’s play along the route of having all the various Arrow constructs available to us.
Step one:
Remove nullability of the Try<>
type. For this, I would need to know under what condition this type would be made null.
I saw that the Try<>
was a nullable, because the method that created it
CustomerInquiryRequest.from(customerDetails) : Request?
returns a null, if customerDetails
was a null value.
I would first, turn this around, and make this method return a non-nullable Request
instead of a Request?
.
This implies that the check for the nullability of customerDetails
must be extracted out of the method to the caller. The code will now look like this:
IMO, this looks better to me because the next reader of the code, can deduce that customerInquiryResponse
will be null if the customerDetails
just by the small amount of code above. That knowledge is no longer hidden away from view in the CustomerInquiryRequest.from(it)
method.
This reduces cognitive load.
What if that is not always possible?
Taking the example above further, what if I have an intermediate method in the one of the .let{} blocks that simply had to return a null? The reader then cannot figure out if it was the request or any of the intermediate functions causes the return value to be null. In that case, I usually follow the paradigm that the Kotlin stdlib folk do by appending any method that returns a null with a
orNull()
in the method name. I would choose to be explicit about this.
Also, I prefer to handle all null conditions at the extremities and have all my other functions to take only non-nullable parameters, especially if they cannot handle nullables in the first place.
A variant of fail early, I would reckon.
Step 2
Now lets remove the Try
and replace it with an Either
Changes for this needs to be done in two places:
i. Computation of customerInquiryResponse
should respond with an Either.Left<Problem>
if customerDetails
is null
ii. The customerInquiryService.doInquiry()
method should return an Either
instead of a Try
This is how the final code looks
In conclusion
- Sometimes, changing the code structure will improve readability. Check if the next reader of the code has to hop through multiple methods to understand flow. Don’t pass a null to your functions if they cannot handle them.
- Use sealed classes to represent problems. In the case there are multiple actions that need to be taken for different problems, a sealed class ensures compile time checks that all conditions are handled.
- Be explicit about your code. Constructs like
Either
help you for this, instead of exception/null-checks.