Typesafety 102: Using Types in your code

Saheb Motiani
disney-streaming
Published in
9 min readNov 22, 2017

--

Note: Aimed to guide inexperienced programmers and provide common sense for experienced programmers. Code Snippets and Data Types will be in Scala, but the concepts would be applicable to other languages as well.

In Typesafety 101, we discussed the basics of typesafety using the Option[T] Type, which can either be something (Some[T](value: T)) or nothing (None). We used the get(key) function of Map data structure, which modeled the problem perfectly. `None` could only mean that the key doesn’t exist and we didn’t need any more information, for example a message describing its absence.

If more than one causes are possible, and we need to return the cause (say the message wrapped in an Exception), then in Scala we can use Try[T] which can be either be Success[+T](value: T) or Failure[+T](exception: Throwable) (Ignore the + for now)

It’s a good idea to use Exception only for unknown/rare scenarios. Other than that, you would be better off creating your own custom type, which would be internal to your application code and return them using Either Type.

For ordinary errors (recoverable as per your application needs), you are better off using Optionor Either. Legacy Java Library calls throwing exceptions should be wrapped in Try and converted to other types using the helper methods.

scala> Try {throw new NullPointerException}.toEither
res26: scala.util.Either[Throwable,Nothing] = Left(java.lang.NullPointerException)
scala> Try {throw new NullPointerException}.toOption
res27: Option[Nothing] = None

In this post, we’ll discuss discuss some more types, especially nested types and analyse few of the function signatures commonly found in codebases.

If you’re already following the Principle of Least Power, you don’t need to read any further. This post has more to do with why diverse signatures exist, and why the general intuition of developers typically goes against that principle — it’s very tempting to use the most powerful type for all use cases, even though you don’t need most of their features.

Problem

Suppose you want to make a reservation for some resource using the following JSON. You have to return the reservation/reservationId or the error(s) if unsuccessful.

// Create a reservation for this resource
{
"resourceId" : "501109ee-bfdb-4c72-b200-f6e09a05f44a",
"startDate" : "2017-05-31T09:25:55Z",
"endDate" : "2017-05-31T11:25:55Z"
}

Let’s take a look at how the middle tier functions for application code (sits between the HTTP layer and the database/cache/queue writing classes) are designed — commonly referred as the Service layer by developers. Your HTTP layer takes the request and calls this method from the Service layer

createReservation(reservation: Reservation): ???

Lets try to come up with the return type of this function.

Precise function signatures create less room for human error. Both, function implementer and function caller, benefit from this information, but coming up with the right type involves a deeper thought than just picking up first thing which comes to your mind.

Possible Solutions

Refer to the Typesafety 101 post for key points you should keep in mind, but in summary:

  1. Keep the result contained, till you want to handle it.
  2. Be aware of what you are dealing with. (Results, Errors, and Exceptions)
  3. Acknowledge the error scenarios and program them into the Type System of the language to benefit from it.
  4. Be more precise with your function signatures.
  5. Find the right Data Type for your use case: use an existing one, or create a new one.

Here are versions of signature examples we’ve used here:

// Future is a Type used for asynchronous operations. 
// Subtypes for Future a. Success b. Failure
// Either is a Type used for capturing Errors or Valid results.
// Subtypes for Eitther a. Left b. Right (Left for Error, Right for (valid) Result)
// ErrorType is a custom type
// NonEmptyList is a Type used to prevent creation of an Empty List
1. Future[Reservation]
2. Future[Either[String, Reservation]]
3. Future[Either[ErrorType, Reservation]]
4. Future[Either[NonEmptyList[ErrorType], Reservation]]]

As you might have noticed, the signature becomes more and more specific from top (1) to bottom (4).

  1. Tells us that the function will return Reservation wrapped in a Future and is a non blocking asynchronous call. It doesn't tell if Known errors are possible or not!
  2. Tells us it can also return Error in form of Strings in addition to . . . (it doesn’t tell us anything about the Error except that it’s a String!).
  3. Tells us more about the Left side, it is going to be of type ErrorType (can it be a list of Errors?).
  4. Tells us it’s going to be a list of errors and also tells us it’s going to be a Non Empty List of Errors.

There is no right or wrong among these signatures, they provide different level of typesafety and teams will need to decide what works for them — based on developer experience and how productive they can be with these tools.

They all work, it just depends on how you handle it at the calling site. You get to decide where to draw the line, how type-safe you want to be and at what cost.

Ease of use, learning curve for third party Types, readability and new developer on-boarding should be considered while making a decision for your team.

Nested Types (say 4) might look scary at first, but with a little reading and some knowledge of Types, you might start to like them. Using them, you’ll get the benefits from typesafety without losing the readability. It’s a grey area though, so here are few tips on how precise to be:

Analysing Solutions

// 1. createReservation(reservation: Reservation): Future[Reservation]
createReservation(...).map {
reservation => Ok(reservation.toJson)
}.recover {
ex: DataException => BadRequest(ex.message)
}

Looks clean. Readable, but the recover block which you see there is the block of concern for a couple of reasons.

  1. It’s easy to miss and no warnings will be generated to help the developer. Something like the null check, Not Enforced.
  2. The DataException which has been handled here will contradict what I suggested during the start; many devs might start cursing the article (as Either is a Type meant to handle expected errors), because it's not an exception in the asychronous operation (Future which was returned); it returned perfectly as was expected but here the failed state of the Future type is used to communicate the known error; by wrapping it inside the custom DataException.

Not ideal, I agree, not typesafe, I agree, but works reasonably well if handled properly on the calling side.

You might see this snippet in many codebases, and might be a good first step if you want to improve Code Quality.

A new developer might start with such signatures, and an experienced developer might come and change all signatures to one of next (type-safe) ones, but they also need to explain and have the conversation discussing reasons for the change.

// 2. createReservation(reservation: Reservation): Future[Either[String, Reservation]]
createReservation(...).map {
case Left(errorMessage) => BadRequest(errorMessage.toJson)
case Right(reservation) => Ok(reservation.toJson)
}.recover {
ex => InternalServerError(s"Some unexpected exception, ${ex}")
}

Some more lines, equally readable and better typesafety. Observe, we are using recover only for dealing with unexpected exceptional scenarios raised during the asynchronous call.

You can’t get the return value without taking it out, by either matching, mapping or some other operation.

So, if you want to enforce the error is handled by the caller in a very simple way, this is your way to go.

But, notice: Stringis the typefor left hand side of Either; this ends your type safety and might not be ideal if you want further processing of the Errors (say you wish to process Error returned and transfer them using different HTTP Status Codes).

If you want to keep it clean, and the Service layer has all the information it needs to generate the relevant error which should be reported to the client of the application, then this should be good enough for you.

Note:

  1. You can still check the contents of the String to give appropriate error codes, but we don’t want to go that error prone route.
  2. You can also remove the pattern match for Left(_), it will compile and emit a warning

scala warning: match may not be exhaustive. It would fail on the following input: Left(_))

but that won’t be able to stop you, that’s the maximum you can do. As I said earlier, it’s possible to write bad code, despite all the enforcement.

// 3. createReservation(reservation: Reservation): Future[Either[ErrorType, Reservation]]
createReservation(...).map {
case Left(error) => error match {
case err: ValidationError => BadRequest(err.toJson)
case err: PartialFailureError => Ok(err.toJson)
}
case Right(reservation) => Ok(reservation.toJson)
}.recover {
ex => InternalServerError(s"Some unexpected exception, ${ex}")
}

Observe: the return type no longer returns error as String, but it’s another custom type ErrorType created to deal with known errors. If the error hierarchy is correctly implemented using sealed traits, it might help you catch new ErrorType in case you missed to handle any of them. But, that would still be an exhaustive matching Warning and not an Error. (Note: You can convert these warnings to error with proper compiler flags)

Once you have developed a production ready application, wrote a lot of error/exception handling code, you get a fair idea about these requirements even before you have think about the implementation.

So, you directly start with a type-safe signature (like the #3 above) as it worked well for you in the past. You might have learnt it the hard way or the easy way, but it wouldn’t be safe to assume that new beginners in the functional world would know about the rationale behind using that signature, which has kind of become the defacto Type used all around your codebase.

Observe: You can return one Error, not a list of them!

// 4. createReservation(...): Future[Either[NonEmptyList[ErrorType], Reservation]]
createReservation(...).map {
case Left(errors) => BadRequest(errors.toList.toJson) // It can never be empty!
case Right(reservation) => Ok(reservation.toJson)
}.recover {
ex => InternalServerError(s"Some unexpected exception, ${ex}")
}

Moving on, let’s upgrade the requirement, like it usually happens in software development, requirements keep on changing.

We want to return all the errors wrong with the JSON received, so the client knows them in one go. An example you will find everywhere is a web form with validation failures…

For our reservation object, let’s assume you want to report that resourceId doesn’t exist, and also report that the dates are in incorrect format. Ergo, you want to return List of Errors.

We can use List, but it’s possible to miss handling empty list and just pass along after converting to json, showing client a Bad Request with an empty body of errors. You see where I am going with this, the goal is to prevent such things from happening.

How do you do it? Make it impossible/difficult to create an empty list of errors. You can create your own Type to enforce this behaviour or use existing solutions from libraries.

def f: Either[List[ErrorType], ...]]        = Left(List())            // Compiles
def f: Either[NonEmptyList[ErrorType], ...] = Left(NonEmptyList.of()) // Won't compile
/**
* A data type which represents a non empty list of A, with
* single element (head) and optional structure (tail).
*/

final case class NonEmptyList[+A](head: A, tail: List[A])

Checking a list for emptiness manually is easy to handle in your application code, however it is easy to miss that check or assume the list will always have data. Types to the rescue and you now know how to model this problem and create a more type safe solution.

Only thing I wish to suggest here is to settle with one level which is comfortable for majority of your team. Say #1 and then gradually move to improving the typesafety of your code base instead of directly using the most type-safe signature across the codebase without understanding the benefit.

We have discussed only the calling site of some nested Types in this post, but the implementing side, the function returning that Type also becomes unreadable with all the boiler plate code and conversion from one Type to another. That’s one more thing you need to consider before moving from one level to another.

Miscellaneous Points:

  1. It’s not a good idea to have every function return Any or Object; this is known to most developers, so the entire point about typesafety and being more specific should be obvious, but somehow it isn't. For instance: The Actor's Receive Method (in Akka) has type ParitalFunction[Any, Unit], which is something many developers hate equally. (Typed Actors are coming in later release of Akka.)
  2. Either wasn’t right biased for a long time in Scala, which was a good reason for many to not use it. (Scala 2.12 changed that)

— Saheb Motiani

Photo by Markus Spiske on Unsplash

--

--

Saheb Motiani
disney-streaming

A writer in progress | A programmer for a living | And an amateur poker player