6 Months with Scala - An Experience Report

March 28, 2016
Scala

I have been working at Skedulo for 6 months now as a scala backend engineer. This is my first experience working professionally with Scala (and functional programming). Therefore I would like to take some time to reflect on my experience over the past 6 months, as well as my thoughts on the language and ecosystem.

Where I Started

At the very end of my university life I stumbled upon Haskell, which really inspired me for how different and expressive it is. I then proceed to spend the next few months studying and tinkering with Haskell. By the time I started chasing other shiny things, I was comfortable with the basic concepts (up to monads).

When I started my new job, armed with my Haskell and Java experiences, Scala felt like the sibling I never met. For the first month or so I was focused on finding out what parts of Hasekll/Java Scala has inherited, and applying them as I go.

This post is mostly based on my experience working on a project that performs scheduling, job allocations, notifications and various other CRUD operations.

The Language

The Good

Scala is a pragmatic, expressive and powerful language - You can produce some readable and correct code easily with just a few sprinkles of functional style:

List(1, 2, 3, 4)
  .map(_ * 4)            // List(4, 8, 12, 16)
  .filter(_ % 8 == 0)    // List (8, 16)
  .map(_.toString)       // List("8", "16")
  .mkString(",")         // "8,16"

Though the example above is just a toy example, I was often surprised at how succinct Scala can be at encoding business logic (and invariants) due to the following language features:

There are many other helpful features that are rare or not found in other mainstream languages such as implicits and type inference. On the ecosystem/standard library side, immutability by default and Option type instead of nulls makes reasoning about code simple. In general, Scala serves its purpose very, very well when you use it as a “Better Java”.

One of the practical benefits, as I have experienced first-hand, is that your code can be refactored and evolved very aggressively. For a codebase that was written by novice Scala developers making a MVP, there was certainly a lot of ugly code written in the name of tight deadlines. However, the strong static typing means I am able to evolve the code and be confident of its correctness. With just Immutability and no-nulls by default means that I already save who-knows-how-many hours compared to Java or Python.

The Bad

Type Inference

The Type Inference isn’t perfect - it mostly works until it breaks on more complex types (which normally involes higher-kinded types). There are often cases where IntelliJ would break but not scalac, which in turn breaks type inspection and autocomplete (not to mention code being highlighted red and make you doubt yourself). Here is an example of a code that scalac gladly compiles but IntelliJ chokes on:

Generally, just sprinkling some type annotations solves the problem. Though it certainly feels like spending half your paycheck on a full-course meal only to be told that you cannot have the dessert unless you do 20 push-ups and pay an extra 10 dollars.

The Bugs

Here is a simple example of a compiler bug I encountered while trying to destructure the tuple on the right side of a \/[SomeError, (Int, Int)]:

  sealed trait SomeError
  case class ErrorOne() extends SomeError
  case class ErrorTwo() extends SomeError

  // attempt 1: desugar the tuple of the either directly
  for {
    (first, second) <- (1, 1).right
  } yield first

  // [error] Main.scala: diverging implicit expansion for type scalaz.Monoid[A]
  // [error] starting with object stringInstance in trait StringInstances
  // [error]     (first, second) <- (1, 1).right
  //                                       ^

  // Huh? Ok Google tells me that diverging implicit normally means that we are trying to find an implicit
  // on a Nothing. 

  // attempt 2: Let's fix the type on the left side of \/

  for {
    (first, second) <- (1, 1).right[SomeError]
  } yield first

  // [error] Main.scala: could not find implicit value for parameter M: scalaz.Monoid[SomeError]
  // [error]     (first, second) <- (1, 1).right[SomeError]
  //                                       ^

  // Huh why is it trying to find the Monoid instance for SomeError?
  // Monoid instance shouldn't be involved?!

  // attempt 3: Let's bind the tuple to a name and destructure it on the next line

  for {
    result <- (1, 1).right
    (first, second) = result
  } yield first

  // it compiles now?! Wut?

So it turns out that there is some bug with for comprehension desugaring, and it has been around for a while. Yikes.

I probably only encountered 2~3 compiler bugs and they are mostly annoyances with ugly workarounds than correctness issues.

Type System Limitations

Maybe due to Scala’s focus on uniting OO and FP, encoding FP concepts are way more verbose than its Haskell counterpart. Two examples of this are Typeclasses (using implicits/context bounds) and Abstract Data Types (with sealed traits).

An example of this is attempting to use multiple-level monad transformers involving EitherT. Since Scala type constructors cannot be partially applied, You will need to employ Type Lambdas hacks to get it to work. which is both verbose, confusing and killing type inference.

This is quite scary for me as someone who is constantly learning and applying functional concepts at work, because it feels like if I try any advanced functional constructs, I am trading away maintainability, simplicity and productivity.

While I am being really critical in this section, I am by no means implying that the people involved in Scala tooling or compilers are incompetant or lazy. No other language has tried doing what Scala is doing, and the people working on Scala has done a great job. However it is a bit sad that some style of programming is much more painful (and probably not worth it because you are trading off conciseness and maintainability).

This is probably the only real fundamental issue with Scala that discourages me from exploring advanced functional techniques in Scala.

Tools and Ecosystem

The Good

Using Java libraries in Scala is a total breeze. I have never had any problem (and expectely so since Java-interoperability is one of the main selling points of Scala). In addition to that, there are an abundance of high quality libraries and frameworks you can choose to solve your problems in Scala, with many of them being backed commercially.

At work we use Play Framework as our backend, Slick as our SQL access layer, Akka for concurrent processing, ScalaTest / ScalaCheck for testing and Scalaz for functional-goodiness.

On the tooling front, Play Frameworks’s various SBT plugins makes the development experience relatively smooth. Due to incremental compilation, compile time is generally not a problem but could be improved (0 - 5 seconds typically).

I spend most of my coding time in Intellij with vim emulation (Ideavim). While I do not make use of many of its more advanced features, the navigation, refactoring, autocomplete and code analysis is invaluable for getting work done.

The Bad

Similar to the ecosystems of other statically typed functional languages, documentation seems to be quite weak, maybe because it is considered not necessary (which to a degree is true because you can figure most things out by just looking at the types). However, I do think this is gradually changing, as many newer projects have excellent documentation and examples.

Scala’s Current and Future

There are a lot of awesome projects in Scala that excites me:

On Productivity, Correctness and Maintainability

Like most developers, I am constantly on the lookout for better ways to program. There are many facets as to what consitutes as “better” - Productivity? Expressiveness? Correctness? Maintainability? The relationship between these facets are very complex and the right balance for each person is different, hence the constant static vs dynamic typing debate.

For me personally it has definitely been a constant struggle between all of these factors. There are times I spend days making sure my code is “correct”, when simple due diligence and documentation can probably prevent mistakes made by future edits. (And that type-safe solution you spent working on? It is invalidated by a new business requirement). There is a little voice that constantly questions myself whether the next person that comes in will be able to understand my code and maintain it, which has been a major consideration when I am deciding how “type-safe” I want to be.

As for now, I have settled on being “Simply Correct”. This means to make full use of the language’s first class features to maximize Correctness and Productivity without sacrificing Maintainability. Beyond that, every extra bit of correctness or expressiveness constructs I implement will need to founded by real requirments.
It turns out YAGNI (“You Aint Gonna Need It”) principle applies nicely here too.

Please don’t leave behind a trail of single character variable names and undocumented mess because I should “just follow the types”.

Conclusion

Scala is a great language for Getting Things Done, whether you are fresh off the boat from Javaland or an experienced FP programmer.

At this moment, if I were to be asked to deliver a project ASAP while making it maintainable, Scala will be the language I choose for its pragmatism, expressiveness and comprehensive ecosystem. However, Scala is definitely far from perfect; The compiler bugs you will find when attempting to write advanced, type-safe code, as well some language design trade-offs means that it is siginificantly harder to reach the expressive level of other “purer” languages like Haskell.

Personally, I aim to explore Haskell more and attempt to write it for real business applications, so I can learn more about how to design and implement purely functional architecture. (instead of the functional-on-the-outside, imperative-on-the-inside design I always fallback to because Scala makes it easy)

comments powered by Disqus