Why I Love Scala, Part I
by April Hyacinth, Protenus on March 10, 2020
While looking for an internship in late 2015, I stumbled across Scala, and was intrigued. Why would I want to use it over Java, especially since Java 8 added lambdas and streams? What made it better? I decided to try it out the following spring break, and I was instantly in love, so much so that I started contributing to the language the next year.
In 2018, I joined Protenus so that I could work full-time on Scala-based backend engineering projects. At Protenus, we use Scala for a variety of reasons that mostly boil down to reducing development time and the number of bugs in code. While many languages share some of Scala's features, few share the majority of them, making Scala unique in ease of development.
Scala is a flexible language with the benefit of compile-time type safety, the strengths of both object-oriented and functional programming paradigms, but without much of the boilerplate required by many other languages with compile-time type safety. It also has features (such as higher-kinded types) that allow users to write more powerful abstractions and reuse more code, but those features are in some ways more advanced, and the topic of another blog post.
Scala is strongly typed, and its types are checked by a compiler before the code is executed. The compiler's type checker prevents you from passing the wrong type of argument to a function, or performing an operation on the wrong type of object, before any code is even run. While Python is touted for its simplicity, its lack of type checking before runtime (at least by default) means that many mistakes that Scala prevents can make it into production code.
Functional and Object-Oriented
Scala's design is a fusion of functional and object-oriented programming. The object-oriented aspect gives Scala the strengths of inheritance and encapsulation, as well as the ability to use the vast ecosystem of Java libraries, at least when running Scala on the JVM.
Scala's functional aspect means that most data is immutable (unchangeable once created), and operated on using functions that don't modify existing data structures. Many of the Scala library's core data types do not allow data modifications and have APIs that return new structures. This design philosophy allows developers to reason about smaller pieces of code, making development easier. Additionally, immutability allows data to be shared safely without copying it, which can reduce memory use.
Scala has many features that reduce the amount of boilerplate code you need to write, speeding up development and reducing the tedium experienced by engineers. These features range from generating redundant methods to removing the need for excessive type annotations to implicitly calling code for you.
Case classes are a way of representing immutable data or state. A case class consists of a fixed number of fields, and the Scala compiler automatically generates several methods methods for it, including:
- accessors with the same names as the fields
- equality and hash methods
- a sensible string representation
- a method to create a copy of an instance of the class, changing one or more values
The Scala compiler will automatically infer semicolons anywhere it is clear (typically at the ends of lines), removing the need to use semicolons in all but the rarest situations. It's a small nicety, but one of many examples of the compiler doing tedious work so you don't have to.
Local Type Inference
Unlike Java (before Java 10), Scala can automatically infer the types of fields, methods and variables, and allows you to omit them (Java 10 only supports this for variables). While it is generally good practice to annotate the types of all public fields and methods, it can save a lot of time and significantly reduce visual clutter to leave them out for variables and private members. Instead of having to type out long types such as Map[User,Seq[HttpConnection]], you can leave them out; the compiler will figure them out and still prevent you from using incompatible types.
Sometimes you need to annotate long types, but even then Scala can help reduce the boilerplate and clutter. You can create a short alias for a long type, and then use the short alias everywhere instead. For example, you could create a type alias such as
type Connections = Map[User,Seq[HttpConnection]
and then use Connections as a type where needed instead of the longer Map type.
Scala allows you to effectively add methods to (or "extend") classes you don't control using extension methods. Extension methods are invoked using static dispatch (unlike regular methods, which use virtual dispatch), but they are called using the same syntax as regular instance methods. The Scala compiler checks if the class has a method by the given name, and if not, looks for extension methods to use instead. It lets you act as if the extension method is actually a member of the class, and handles the behavioral differences transparently in the background.
In Scala, method parameters can be marked as implicit, which means that they don't need to be passed explicitly when calling the method. Implicit parameters significantly help when there is a context that gets passed around into all methods, such as an injector for dependency injection, or a request in web server code. Additionally, it allows contextual type bounds (such as needing an Ordering to performing sorting operations) to be filled in implicitly by the compiler rather than specified explicitly.
One of Scala's more controversial features is implicit conversions—the ability to have the compiler automatically transform or convert one type to another, without you having to explicitly specify it. While it can make code difficult to understand when used irresponsibly, it can remove lots of boilerplate when used judiciously. For example, if you frequently need to convert a parser for a single line in a file to a parser for an entire file, performing that conversion implicitly will likely not obfuscate what is happening in the code, and will remove the need to manually call a method like asFileParser(...) all the time. Implicit conversions should not be used everywhere, but can be used in some particular situations as another boilerplate reduction tool.
Scala has many other powerful features, including higher-kinded types, type-level computation, string interpolation and much more, but they are topics of future blog posts.