Update, Sep. 15, 2014 16:09 UTC: Much wordsmithing.
Update, same day: forgot to include how to run the thing. Now added!
Some recent work I’ve been doing led me to investigate the use of Wart Remover to catch bugs.
I’ve discovered that Scala has quite a few insiduous corner cases. For one, the combination of sub-typing with type inference leads to dangerous inferred types from time to time. Further, Scala provides various type-safety escape hatches that lead to reduced effectiveness of static analysis.
In this post, I’ll take a look at how Wart Remover addresses these issues, and why addressing all issues found by it can lead only to strictly better code.
Wart Remover is a linter that is aware of the escape hatches and inference corner cases in Scala’s type system. It works with the type system to improve the reliability of your code. It catches the following issues:
I’ll look into each of these in more detail in the following sections.
Wart Remover is pretty easy to set up for SBT-based build systems. I’ll cover the steps below, including a current corner case that the docs don’t cover. Three files are needed:
This line above in
build.sbt is needed because NoNeedForMonad is currently broken. It would normally warn about where Applicative-dependent code would suffice.
As to why you might prefer Applicative implementations over Monadic ones, I’ll quote a paper referenced in the footnotes:
The moral is this: if you’ve got an Applicative functor, that’s good; if you’ve also got a Monad, that’s even better!
…the dual of the moral is this: if you want a Monad, that’s good; if you only want an Applicative functor, that’s even better!
Applicative computations are composable, are more amenable to analysis, and are frequently more succinct than Monadic computations. The short of it is: if you don’t need dynamic decision making in your computation, e.g., (
if pred then path1 else path2), prefer Applicatives to Monads. 1 2
Given the set up above, running the thing is as simple as issuing a compile:
Scala’s type system, for better or worse, features subtyping available throughout. In more common terms, it means that inheritance is available as a tool for extending types. Consequentially, it is impossible for the type inference engine to arrive at safe conclusions in some cases. This matter is worsened by a design decision that let’s Scala infer
Any as a valid type for mixed constructs.
Any is much akin to C’s (
void *) - anything goes. Information is lost, and the confidence we can have in the compiler’s conclusions is reduced significantly.
Thankfully, Wart Remover catches these sorts of cases. If you see any of the warnings below, the fix is usually as simple as providing type annotations. In the case of
Nothing type inferred, this literally means your program will crash here if your code is left as-is.
Note: the let it crash (pg. 107) approach doesn’t work in Scala because of a lack of built-in process supervision. This is not Erlang. Do not take crashing paths for granted.
Partiality means that your functions don’t cover all values that could be passed to it. List functions are a good example of this. The
ListPartials below all crash when an empty list value is encountered.
The solution to partiality is often simple: use a function that returns an
Option or an
Either for values you’re not handling. If you want to be more explicit, a sum-type via
case classes could be even better, say, for a
Weekday data type.
Type evasion is my own terminology. I refer to the use of back doors to evade the benefits of a type system. An example of this would be to use
unsafePerformIO in Haskell land, and should be treated with the same level of suspicion. Whenever Wart Remover warns you of these sorts of type system evasions, heed the warning as soon as possible.
Run-time type coercion is occasionally a valid technique. Much research3 has gone into type-safe type coercion.
isInstanceOf are not the fruits of that research and should be considered suspect. Think of it as trying to force a Circle shape into a Triangle mold - you’ll have lost bits of the Circle and might not even have a full Triangle!
The remaining classes of warnings that Wart Remover provides are more subtle. Consider them as design flaws that could lead to maintenance troubles in the future. This is particularly true of the use of
Here are some more thoughts on why this is a questionable practice.
Unlike linters from languages like C, C++, and Java, Wart Remover does not give false positives. This means that if a particular construct is flagged by Wart Remover, you should think very carefully before choosing to ignore it. To understand why, read on. It all comes down to the promise of type safety within the realm of a sound type system.
Let’s look at the definition of type safety as given by Benjamin Pierce in Types and Programming Languages:
If t is a well-typed term (that is, t : T for some T), then either t is a value or else there is some t’ with t -> t’.
If t : T and t -> t’ then t’ : T
Progress and Preservation together mean that we can count on sections of code to behave as we wrote them.
Let’s look at each class of issues caught by Wart Remover in turn and reason why fixing them can’t make your code any worse.
In the best case, all we need to do is add a type signature to aid the compiler. Type signatures do not affect the run-time behavior of code. Therefore, this cannot hurt.
In the worst case, we’re changing something that would’ve crashed to something else (
Nothing inferred). Consider that the worst thing that can happen to your system is to crash. As a result, anything we do to address this warning cannot leave us in worse place.
If you are addressing partial
List methods, for example, the solution would typically be to use a method that returns an
Option. What you’ve done is removed the possibility of crashing at the cost of one more line of code.
In the shared common case, upon receiving a non-empty list value, the code behaves as before. Upon receiving an empty list value, the old code crashes and the new code does something. At least in the new code, that something is predictable and under your control.
The use of
get introduces partiality and I reason about it as above.
Here’s an example of coercion in action:
Note: this is a run-time error. Contrast this to a compile-time error:
It is a toy example, but what’s key here is that coercion leads to run-time errors. This means that it is not safe, in the sense defined earlier. Given that coercion in Scala is not guaranteed to be safe, consider the following -
When you employ coercion, you are choosing to take the burden of what a stretch of code does into your own (and your team’s) heads, rather than trusting the compiler. Case analysis is difficult here. Resolving the issue means you’ve found a way to encode what needs to be encoded in the type system. Choosing to ignore the issue means an implementation detail must now be kept in mind by those who will maintain your system. You may pass it off to a unit test, but now you have to maintain that test suite, as well.
These are more subtle to reason about and fix. I’ll pass on discussing why the use of
return, and default arguments can lead to maintenance problems.
I’ve written a lot at this point. I stewed over the subject for a few weeks. Most of what I’ve written in this post comes down to trusting a type system to prove that what you’ve implemented is consistent.
With Scala, you have to work with the quirks of the language to be able to trust the type system. It’s capable enough. It’s on par with Haskell, in many respects. Wart Remover knows about those quirks.
I wrote this post because I want to advocate for safer Scala. We have the tools. Let’s use them!