The original inspiration for writing the article was this blog post by Daniel Ciocîrlan. I was somewhat flabbergasted: are we really still discussing Scala’s underscore ambiguity in 2020? That can only mean one thing — Scala 2’s language definition problems are so fundamental we still haven’t gotten around to mitigating them after so many years.
Luckily, Scala 3 is on the horizon. It introduces a multitude of changes to the language, and promises to make it more usable than ever. Will it do good on this commitment, especially when removing the common pain points?
This article represents an attempt of investigating just that. I have subjectively selected several language features in Scala 2:
- that are at least passingly familiar for Scala programmers through all levels of expertise,
- make understanding and using the language more difficult, or simply less “fun”,
and explored the equivalent features of Scala 3/Dotty.
We’re starting, of course, with a look into Scala 3/Dotty’s handling of the “underscore problem”. Well, the “wildcard” and “placeholder” semantics are still in place (I would personally classify “Lambda Sugars” referred in the aforementioned blogpost to one of the two). However, the ambiguity has been reduced on other fronts.
First of all, using
_ as a value definition:
now results in:
Unbound placeholder parameter; incorrect use of _
Fortunately, not many coders were willing to use this obscure feature (and not that many were aware of it to begin with). Still, it’s nice to see it go — it really had no practical use other than nerd bragging rights.
More significantly, eta-expansion is much more intuitive now. When reifying a method such as:
all you need to do is:
As the doc examples demonstrate, this even works for multi-argument lists:
Compare the above to Scala 2:
Admittedly, you still have to use the “full” syntax for partial applications:
this is not a problem in practice: the first example follows the “placeholder” intuition for underscore, and the second is arguably a sign of bad signature specification.
Finally, an interesting development are the changes in type parameters, going from:
This brings Scala’s type parameters syntactically closer to Java’s generics, which ever-so-slightly reduces the learning curve of the former language.
More advanced readers will note that, in Scala 2,
? is used by the kind-projector plugin. And good news on that front – not only is the kind projector concept included into core Scala 3, there’s also a dedicated symbol for it:
*, which means now we’ll be writing:
Since the change has significant implications for existing code bases, and Scala 3 aims for as much interoperability as possible, a detailed migration plan has been declared — it’s available here, along with some additional context on the type wildcard changes.
The other semantic overload hell in Scala 2.x.
implicit can mean, among others:
- implicit conversions,
- typeclass and ADT definition/derivation,
- extension methods,
- polymorphic methods,
- method/constructor dependencies in the implicit scope (like
- context bounds for generic parameters.
And these are just the semantic variants of
implicit – syntactically, it could be virtually anywhere: class definitions, value definitions, method declarations, method signatures etc. etc. etc. Overall, understanding
implicit is probably the greatest barrier to entry for mastering Scala 2, certainly more significant than the ambiguity of
Indeed, it becomes apparent that a significant amount of work went into cutting through that tangle. On the syntactic front, the overloaded
implicit keyword has been phased out, and replaced by several more localized ones:
given … as,
extension. But this is really more of a symptom of the more fundamental change – the semantic split.
Let’s look at the “original” meaning of implicits — the scope, and the members supplied within that scope. These are now Context Parameters. To supply a context parameter, you use the
Note that, unlike implicitly scoped values in Scala 2, an identifier is optional. Although, if you want or need it, you can always provide it:
On the other side of the equation, you declare demand for a context parameter with the keyword
using clause identifier – in this case,
enc – is also optional. This has the same applications as in Scala 2, namely context bounds (although the shorthand for that is still there) and direct invocation of extension methods.
For implicit scope dependencies/”defaults”, there’s a concept of Alias Givens:
which are pretty much like the “old” implicit-scope lazy vals.
This leaves us with actually gluing the usage and declaration together, which, like in Scala 2, is done through bog-standard imports. However, unlike Scala 2, there’s been some usage improvements. While you can still import by identifiers directly:
givens now have an additional explicitly separate import scope:
Take a closer look at the snippet. For
givens, we are no longer focusing on the identifier, but on the type – quite a bit more intuitive in the context of
If you’re so inclined, you can even go with a wildcard import:
which will import only
givens in the given (heheh) scope.
More information on
given imports, including the migration plan, can be found here.
Let us now consider another popular use case for Scala 2’s
implicit , extension methods. In previous Scala versions, extension methods didn’t really exist as a language-level concept – rather, they were more of a design pattern, exploiting several features of implicits (and the features used changed across Scala 2.x versions). In Scala 3, extension methods are a first-class syntax citizen, and defining them is as easy as using the appropriate keyword:
For the nontrivial cases, using implicit conversions is pretty similar to the previous versions of the language: either available for import through some identifier, or inclusion in the relevant implicit scope. More information, including import rules, and writing parametrized extensions, can be found here.
Typeclass definitions are simply a combination of the concepts outlined above — parametrized
givens providing extension methods. So, no more
TypeclassOps, and no more being tripped up by multiple implicit scopes.
Dotty also includes some powerful auxiliary facilities for deriving typeclasses. In fact, a proper in-depth treatment of typeclasses in Dotty deserves a separate blogpost of its own (if only due to the required length). Until that’s written, I recommend reading the substantial documentation.
Now for implicit conversions. While they are still a part of the broader
given (so, implicit) functional concept, they were bestowed with their own little niche. An implicit conversion is now simply a
given subclass of
As always, implicit conversions are to be employed in very limited circumstances, such as cumbersome interop with legacy libraries.
NOTE: another identically-titled documentation page appears to be an out-of-date artifact, not representing the final direction in which Scala 3 is going with conversions.
Overall, Scala 3 appears to deliver what most Scala developers wished for in terms of implicits — the detanglement of disparate language features from a huge conceptual blob into a series of small mental “compartments”.
For those interested in more detail, the Dotty documentation page includes both a thorough Motivation section, and a glossary of correspondence between Scala 2’s implicits, and Scala 3’s language constructs.
Ah, Scala 2’s enums. Most mid-and-up Scala programmers are acquainted with at least three enum abstractions — the standard library
Enumeration class, sealed traits, and enumeratum (and there’s more!). While quite a number of coders argue that enumeratum is the most comprehensive treatment of enums in Scala 2 , no clearly dominant "winner" has emerged so far.
of course, fields and methods are also fully supported:
(and yes, there’s also proper Java interop)
Joking aside, this appears to be the way to go. Scala 2 tried to make enums a second-class citizen of the language, and it clearly didn’t take.
Weirdly enough at first glance,
enum is not only for enums in Scala 3. It’s the endorsed abstraction for (G)ADTs:
This works, because, like
sealed trait families, enums form fully self-contained hierarchies – implying identical advantages for compile-time checks, serialization, etc. The construct is flexible enough for the main example in the docs to be… an
A word of concern, ‘though.
enum, to my mind, does not immediately speak "ADT" – this is something that can blindside new Scala 3 developers. The chosen approach evokes the conceptual overload for
implicit in Scala 2, with all of its problems (although admittedly at a potentially smaller scale). Time will tell whether this will be the case.
Regardless, it’s safe to say we can reach an encouraging conclusion — removing harmful and confusing quirks was on the forefront of those involved in Dotty/Scala 3s development. The vast majority of the outlined changes render the new version easier to understand, and easier to learn piece-by-piece.
Such sweeping changes in a language require careful planning (remember the case of Python 3?). As already noted in this post, each potentially disruptive alteration is treated with either a detailed migration plan or preserving the old syntax with future rewriting facilities.
Many, many more modifications and new features have been introduced in Scala 3 than we’ve covered here. While this post focused on applicability to most Scala devs’ work, a future article will concentrate on more obscure (and/or controversial), but no less revolutionary adjustments to the language.