<img height="1" width="1" src="https://www.facebook.com/tr?id=1076094119157733&amp;ev=PageView &amp;noscript=1">

Typesafety 102: How safe, type-safe you want (your code) to be?

Posted by Saheb Motiani on Wed, Nov 22, 2017

Note: Aimed to guide inexperienced programmers, 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 suited it perfectly. None could mean only one thing, the key doesn't exist, and we didn't need any more information. We didn't need the reason--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 Option or 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. The post has more to do why diverse signatures exist, and why general intuition of developer goes against that principle, because 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 if successful or the error(s) otherwise.

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

I am more interested in how we design the middle tier functions for our application code which sits in between the HTTP layer, and the database/cache/queue writing classes; many developers prefer to call it the Service layer. 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 makes it harder to fuck up. Both, the 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

I have already mentioned the key points in the previous post, we just need to keep them in mind going forward.

  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.

On inspecting several projects at Cake, I found these are the possible signatures different teams use (or some version of it).

// 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 more, 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 more, it is going to be a list of errors, and also tells us it is 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 a team decides what works for them--based on developer experience and how productive they can be with these tools.

They all work, as it all 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 onboarding 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 can keep benefit from typesafety without losing the readabilty. But, that's the grey area! 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 type-safe, I agree, but works reasonably well if handled properly on the calling site.

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: String is the type for 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 recevied, 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 exisiting 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]) 

It's easy to handle it on your own. Check list for emptiness, but as you realize it is possible to miss! I have repeated the words more than often now.

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. (Actors works great, but not type-safe )
  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)
  3. I wrote about Play Framework's Web Service Client's method which used String for Url, making it error prone, and they didn't use type-safe constructor to prevent the creation of an invalid Url; it throws a RunTimeException instead. Their concern was performance, so I can't argue more about typesafety, but your application code might not have similar concerns. (Read more about Total Data-Types)

Topics: Scala, typesafety, types

Recent Posts

Posts by Topic

see all

Subscribe to Email Updates