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

Typesafety 101: Knowing your Types

Posted by Saheb Motiani on Mon, Oct 30, 2017

Note: I will try to keep the articles conceptual, language agnostic* and unbiased, but as Code Quality is subjective, there will not be any clean conclusions or clear recommendations. The goal is to help a beginner (or a less experienced type-safe programmer) to have a better understanding of Types and Typesafety

 

The concepts would make sense only for statically typed languages (Scala, Haskell, Rust…), which take help of a compiler to catch errors as early as possible (At compile time). I'd recommend knowing the basic difference between statically typed and dynamic typed before reading any futher.

Also note that statically typed languages might seem like dynamic languages because of their type inference capabilities, but they aren't. You should understand that being dynamic isn't just about lack of type annotations, but more about type checking happening on run time instead of compile time.

*snippets will be in Scala but that the points I wish to convey can be applied to any language. I will provide a brief explanation of the langauge specifc types which should be sufficient to understand the concept.

Context

Before we dive into the grey area and nested types, let's get on the same page to achieve a better understanding of the problem. I'm going to go slow so please be patient (Side note: During the start, you will need a lot of patience while writing type-safe programs)

val MyMap: Map[String, String] = Map("key" -> "value")
MyMap.get("key")  // returns Some("value")
MyMap.get("key2") // returns None

MyMap.get(key) returns the value wrapped in a Type in strongly typed languages like Scala (Option), Haskell (Maybe), Rust (Option), while less strict languages directly return the value which makes it easier to use.

This "easier to use" concept is not well defined and often leads to misunderstanding between two people who more or less believe in the same principle.

What happens if the value for that key doesn't exist? It's missing, null or undefined

It's possible to deal with that with a simple check ( if(MyMap.get(key) == null) ) before performing any further operations (e.g. toJson) on it, but it's also likely for a developer to miss the check, either ways, you can't be confident that it won't break on run time. This is where typesafety shines and gives you a helping hand in writing better software.

The goal is to prevent unhandled scenarios (e.g. missing values) and more importantly, enforcing the programmer to handle them instead of assuming, ignoring and forgetting about edge cases.

Typesafety is about being more specific about what you are dealing with. get(key) will return the value for the key but what is the type of the value, if you say String, than the next question should be:-

Is null included in type String? Is it possible for a String to be null? You need to know answers to these questions before using them at the calling site.

The more precise you are with your signatures, the better. Being aware of the missing value forces you to come up with a better Data Type to represent the result.

You want to seperate the null values which can't be processed further from the other valid values, hence for this use case, the precise, more specific return type would be Option / Optional/ Maybe

Type-safe programming often leads to frustration, especially for beginners because it's not trivial to see the benefit you are achieving.

MyMap.get(key) match {
    case Some(value) => value.toJson
    case None => JsNull
}

It's not impossible to write unsafe code in strongly typed languages, they accommodate it, but they don't want you to use it. It breaks their heart when you don't use the features languages provides by default.

map(key) or map.get(key).get will get you the value directly in less lines of code and you might think it's okay to do it but it isn't and you would be looked down by type-safe programmers, leave the pure functional programmers aside, but even the experienced Scala/Haskell/Java programmers would be annoyed too.

The above code snippet might be verbose for some, they would prefer this one line version

MyMap.get(key).map(value => value.toJson).getOrElse(JsNull)

Finding the right balance of readability without losing the compile time safety is a crucial necessity for a team to function well, especially if the developers are unequally experienced and have strong conflicting opinions (about libraries to use, how type-safe they want their code to be, nested types, readability)

I'll go a level deeper to clarify this concept of why wrapping the result in a Type is important because I have come to realise that many beginners in functional programming languages just assume it is the better way without never understanding why, which builds a blockage and prevents them for understanding other complex types.

The question always lingers in their head but they never ask: "What's so wrong with calling .get, instead of using flatMap or map or recover"

MyMap.get(key)                          // Option(value)
MyMap.get(key).map(...).map(...)        // Option(value), still contained
MyMap.get(key).get.toJson               // Value if good, Null Pointer Exception when not

The result is retained inside the Type and won't come out and break your code unless you explicitly do it yourself.

The expectation is to keep it contained and prevent unhandled cases which are possible (e.g. null, Throwable)

What's the difference? Enforcement, writing bad/unhandled code is not possible, it won't compile.

I will refer to this documentation to complete my point. Here you need to refer to documentation to know if you need to make a null check on the returned value or not.

* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.

While, enforcing means to make it impossible (at least difficult) for developer to miss it.

/** Optionally returns the value associated with a key.
*
*  @param  key    the key value
*  @return an option value containing the value associated with `key` in this map,
*          or `None` if none exists.
*/
def get(key: K): Option[V]

The next question is: How type-safe you want your code to be?

Specific function signatures help, but how specific to be and where to draw the line?

We will discuss that in the next post.

Topics: Scala, typesafety, types

Recent Posts

Posts by Topic

see all

Subscribe to Email Updates