Type safety

What is type safety

Type-safety is making use of what we know of our values at compile time to minimize the consequences of most mistakes.

Avoid null

  • Reason: null values can appear in any variable or value, be passed into functions, assign them to other variables, and store them in collections. So null values cause errors far away from where they initialized, and are difficult to track down.
  • Option[T]: used to represent a value that may or may not exist

For example, we need to write a class Address that has street2 is optional value:

  • val: used to declare and initialize a variable at one go

Avoid exception

  • Reason:
    • The compiler doesn’t complain if you don’t handle it.
    • Function signature doesn’t clearly explain kind of exception.
    • Exceptions interrupt the program flow by jumping back to the caller.
    • Exceptions are also a rather bad solution for applications with a lot of concurrency. For instance, if you need to deal with an exception thrown by an Actor that is executed on some other thread, you obviously cannot do that by catching that exception – you will want a possibility to receive a message denoting the error condition.
  • Option[T]: Use when know that values may be absent but we don’t really care about the reason why.
For example, when performing division, instead of returning an Int or fail with an exception when the divisor is zero, we return Option[Int]:

When indexing a list, instead of returning a value A for that index or fail with an exception when that index doesn’t exist, we return Option[A]:

  • Try[T]: If an instance of Try[A] represents a successful computation, it is an instance of Success[A], simply wrapping a value of type A. If, on the other hand, it represents a computation in which an error has occurred, it is an instance of Failure[A], wrapping a Throwable, i.e. an exception or other kind of error. If we know that a computation may result in an error, we can simply use Try[A] as the return type of our function. However, the signature of the function does not have information of exception type.

For example, let’s assume we want to write a function that parses the entered url and creates a java.net.URL from it:

As you can see, we return a value of type Try[URL]. If the given url is syntactically correct, this will be a Success[URL]. If the URL constructor throws a MalformedURLException, however, it will be a Failure[URL]. Hence, parseURL("http://danielwestheide.com") will result in a Success[URL] containing the created URL, whereas parseURL("garbage") will result in a Failure[URL] containing a MalformedURLException.

  • Either[L, R]: Represents a Left value or a Right value. Left is the exceptional case and Right is the success value.

  • Custom sealed trait: Represent results with more than one failure mode

Avoid side effect

As an example, let’s consider a piece of code like:

We initialize result to some placeholder value, and then use side-effects to modify result to get it ready for the makeUseOfResult function. If you leave out one of the mutations, the function makeUseOfResult gets invalid input and do the wrong thing.

In this case, we should eliminate the side effects, and giving the different “stages” of result different names:

As a result,  leaving out one stage in the computation results in a compile error:

Avoid Strings in favor of Structured data

For example, imagine we take in a string with user names and phone-numbers as comma-separated values, and want to find everyone whose phone number starts with 415 indicating they’re from California:

However,  when the text format changes, e.g. to tab-separated values, and your code silently starts returning nothing:

In such situations, the better thing to do would be to first parse the phoneBook into some structured data format (here a Seq[(String, String)]) you expect, before working with the structured data to get your answer:

In such a scenario, if the input data format changes unexpectedly, your code will fail when computing parsedPhoneBook.  Thus, data-format errors will only happen in one place, and after that the code is “safe”.

Choosing data structure

For example, we need to store the phone cook containing users and phone numbers and a lookup function to get a user’s phone-number based on their name.

We can use following data structure to store the phone cook

  • Seq[(String, String)]: use when allowing duplicate entries.

The lookup function will return Seq[String] or (String, Seq[String]) if there is at least a phone numbers corresponding to the name existed.

  • Set[(String, String)]: use when (name, phone-number) pair is unique.

The lookup function will return Set[String] or (String, Set[String]) if there is at least a phone numbers corresponding to the name existed.

  • Map[String, String]: use when duplicate phone-numbers but no duplicate names.

The lookup function will return Option[String] or String if there certainly is a phone numbers corresponding to the name existed.

Avoid Integer Constants

  • Advantage:
    • Int takes minimal memory to store or pass
    • More obvious than magic number
  • Disadvantage: Cause runtime error in some situations when you modify unexpectedly a ERROR_CODE constant or you pass pass in all sorts of Int into places where a ERROR_CODE constant is required:

  • Solution: Use case object

Avoid String Constants

  • This has the same problem as using integers; you can call .subString.length.toUpperCase and all other string methods on these ERROR constants, and they’re all meaningless and definitely not what you want. Similarly, you can pass in all sorts of Strings into places where a ERROR constant is required

  • Solution: Use case object

Box Integer IDs

For example, you have a function deploy a machine. If id is Int or String,  passing id of a user instead of id of a machine will not result into a compile error. Or worse, having no error at runtime and instead silently deploying the wrong machine.

Solution: We must box id into a case class





Add a Comment

Your email address will not be published.