Scala Saturday: Static vs dynamic typing
Type inference makes static typing easy in Scala
Scala is statically typed. “Boo, types!” you might be thinking. “So restricting and tedious! Jeeves, drive me back to my scripting language!” Before you write off static types completely, let me share a bit about my background with types.
A Tale of Two Types
I wasn’t always Mr. Monad. I didn’t always “push my errors into the compiler”. Nope, I would just let it fly. “Ship it and see if it survives!” I’d write and run. I’d write some more and then I’d run again. But little did I know what I was running from… was types.
In my formative years I was a user, a big time user of dynamic typing. For more years than I care to admit, I was a professional Perl programmer with some JavaScript on the side. I knew my Perl Best Practices. I bled Modern Perl. And I slept soundly with my copy of Higher Order Perl tucked beneath my pillow. I’d look around at my peers, fellow Perl folk, PHP-ers, Pythonistas, Rubyists, and I‘d think, “Aren’t we all a productive pod of programmers that mostly start with the letter P?”
“Static typing is for academics,” we’d trumpet. “We have no time for types. We’re too busy being more productive by not writing them!” Sound familiar? At some point a dichotomy arises of “static languages are boring because you have to specify the types and dynamic languages are funner because you never have to specify any”.
But the code eventually catches up to you. Things get more complex. The code base grows, teammates change, and you can’t hold it all in your head anymore. “Was that an array or an object?” What was once an integer is now a string, or sometimes nothing at all. Have you ever woken up in a cold sweat thinking, “Was that a real class? Or just a hash dictionary? Did I spell those keys correctly?”
Living a professional life of second-guessing my own work had taken its toll. I was looking for something else, something with consistency.
Duly noted
Here comes the big bad, static typing in Scala. Contrast with Java and C#, the types go on the right of the identifier.
Every value, parameter, and method has a type associated with it known at compile time. Without running the program, the compiler can tell if your program is correct based on the types that flow through it. This handles basic, logical mistakes like “you gave me a String
but I needed an Int
".
One of the chief complaints with static typing is the need to explicitly specify types for everything. Right away, we can see a bit of redundancy. The type annotation on the left is often the same as the value being constructed on the right. It becomes even more cumbersome when using a type like Map
which takes two type parameters. Twice the duplication and twice the friction!
Type inference to the rescue
Many languages nowadays feature some form of type inference where the compiler can guess your types at compile time. Often, the type of your value will be based on the expression that it is being assigned. In the case below, we can tell that age
will be of type Int
since 13
is an Int
literal. Likewise, we can infer that the type of pet
will be Dog
since we are constructing one on the right side.
Scala can even infer the two type parameters of Map
. Recall that Map
is a type that takes two additional types within it, one for keys and one for values. In this case, the full type of dictionary
is Map[String, Int]
. In this case as well, the Scala compiler is smart enough to infer the kind of Map
we are building based on the literals passed into its constructor. Likewise, when we want to case/match against the individual elements in dictionary
, we can relax the annotations there because the compiler can infer from context.
Even the describePets
method has its type inferred. In Scala, the last expression in a block will be that block’s return value and type. Since descriptions
is a Seq[String]
and is the last line of the method, the entire method describePets
must also return Seq[String]
.
Having type inference of this level of power is extremely convenient. We can depend on the compiler to help us during development. Firstly, we don’t need to annotate everything. If the type of some domain concept changes, that new type will propagate to all of the areas where its type is left to be inferred. The compiler will automatically infer that new type without us having to change any annotations. Secondly, if we happen to miss any spots or leave the code in an illogical state, the code wouldn’t compile, preventing any surprising runtime errors.
Let’s revisit that dichotomy from earlier. Is it still true that all languages with static typing come with a high usability cost?
Newer languages have increasingly powerful forms of type inference, relieving you of the burden of annotations while enabling you to reap the benefits of compile time safety.
Dynamic dud
We can take a look at dynamic types through the lens of a static type system. We can describe a dynamic program as having all values, variables, and functions belonging to one type whose behavior isn’t resolved until runtime. In Scala, we call this type Any
. Dynamically typed code can take in Any
type as an input and return Any
type as an output. This amount of power could be a blessing or a curse, but more often curse than not.
In my entire career as a Perl and JavaScript programmer, I haven’t found a strong enough or frequent enough use case for the ability to take in arbitrary input or return arbitrary values. In 99% of the cases, functions would take in one type and always return some other type. And because we didn’t have explicit types written anywhere, I would need to double check the documentation to make sure. In the first example function increment
we’re always expecting an Int
and returning anInt
, but our annotations are under-specified. In theory increment
could take anything and return anything. Is that true in practice? Is that expectation ever violated? What would our code do if that wasn’t the case? Why leave any of these suspicions to chance? Why not say this up front and have the spec match our expectations? It’s this constant stream of doubt that has made me shy away from dynamic types in favor of typed code that has much more consistent, targeted behavior.
There may be some cases where you do want to match on the type of some input. But these cases are much more limited in scope than the ability to match on anything at all. There is almost always a way to accomplish what you want using a static workflow and types. For example, if you were dead set on having a function that could take in or return either a String
or an Int
but no other types, there’s actually another mechanism you could use called Either
. Your output type would be Either[String, Int]
. With this container type, we’ve narrowed the scope down from Any
to two specific types can be analyzed during compile type, free of any ambiguities or problems at runtime.
Level up
Having been on both sides of the static/dynamic fence and building a career on both, I’m highly skeptical of ever returning to dynamically typed languages. Newer languages and stronger type inference systems have made the path to static typing much more palatable and fun.
Special shout out to Ryan LeCompte who detailed a similar experience with Scala in his 2013 Scala Days presentation Confessions of a Ruby Developer Whose Heart Was Stolen By Scala.
The debate rages on but in a statically typed way. Have you worked with both type systems before? Was there something I missed? Or something you disagreed with? Comment below!
Until next time, happy hacking and see you on the next Scala Saturday!