Scala offers a number of advanced language features that are disabled by default. If you try to use any of them, the compiler will generate a warning informing you of their usage. There are a number of reasons that you might want to enable features (or keep them disabled). In this post, we will explore what each language feature is and under which circumstances you might want to enable them. (For a discussion of basic Scala Compiler options that we find useful at Threat Stack, see Part 1 of this series.)
Language Feature Warnings
To start with, you should always enable the -feature flag. This flag tells scalac to provide information about misused language features. It can be useful for discovering what advanced language features are being used, but have not been enabled. For example, if we have a simple definition with a higher kind:
We will see the following compiler warning:
warning: there was one feature warning; re-run with -feature for details
This warning tells us that we are using a language feature that has not been enabled, but it doesn't really give us any useful information about what is being used or where it is happening.
If we pass in the -feature flag, we will see a more verbose warning:
warning: higher-kinded type should be enabled by making the implicit value scala.language.higherKinds visible. This can be achieved by adding the import clause 'import scala.language.higherKinds' or by setting the compiler option -language:higherKinds.
This warning tells us that we have not enabled the higher kinded types feature for a particular piece of code.
Later we'll see all of the language features that can detected with this flag.
The Scala Compiler allows you to enable certain language features at the compiler level instead of at a per file level.
Usually if you want to use a language feature like scala.Dynamic or Implicit Conversions, you would have to import that feature like so:
Without this import, the compiler would generate a warning stating the implicit conversion method "should be enabled by making scala.language.implicitConversions visible." Adding the import to one file is all well and good, if it’s the only place in the project you are using that language feature.
But often this is not the case.
If you want to enable a language feature or features, you have several options:
-language:_-language:<feature name here>
The first line enables all language features. The second will enable a specific feature. Currently, seven language features (as discussed below) can be included in the compiler or imported into a file.
Implicit Conversions — implicitConversions
Many Scala programmers have strong feelings about implicit conversions. Implicits can easily be abused and can lead to some very confusing errors about missing implicits or multiple implicits of the same type being found. With that said, we've chosen to enable implicit conversions because they are used in many places in the core library, as well as a number of other libraries that we use.
Existential Types — existentials
The documentation notes several issues with enabling Existential types. Overly complex Existentials can lead to code that is difficult to maintain. Much like implicits, too much of a good thing can become a bad thing. Existentials can also be intimidating to programmers who are new to Scala, and can prove to be yet another roadblock to onboarding new Scala programmers. We enable existential types because they are great for dealing with wildcard types and type erasure in Java. Some may not find this especially valuable, but we often find ourselves bringing in established Java libraries to get the job done, and Existential types prove invaluable when dealing with the language integration.
Higher Kinds — higherKinds
Higher kinded types provide the ability to define general abstractions (e.g., Functors and Monads). Many libraries (such as Cats) use these constructs to make more expressive APIs. We've chosen to enable higher kinded types, because several of the libraries that we use expose higher kinded types. As a result, enabling higher kinded types becomes invaluable if not necessary. There are, however, some interesting ramifications to enabling higher kinded types. Like many advanced language features, they can be difficult to understand for newcomers, which can be a barrier if you are trying to ramp a team up on Scala as we are.
There's also a fun little blurb in the API documentation around higher kinded types:
Higher kinded types in Scala lead to a Turing-complete type system, where compiler termination is no longer guaranteed.
I've never seen this happen as a result of higher kinded types, but I'm sure someone has at some point. It’s probably fine.
Macros — experimental.macros
Macros are a particularly powerful feature in Scala, albeit somewhat arcane. They allow you to define code that is run at compile-time and can be used to implement static analysis and code generation, amongst other things. Generally speaking, you really only need to enable the macro feature if you are actually implementing macros. This can be useful for library maintainers who are looking to add some more advanced features to their libraries, but can be less useful in writing everyday applications. Macros generally have a steep learning curve, and can be very difficult for newcomers to Scala to learn. We tend to not enable this option because we don’t write macros.
Structural Types — reflectiveCalls
Structural Types effectively offers a form of Duck Typing in Scala. It allows users to define an interface without needing a predefined inheritance hierarchy. However, this feature relies on Reflection, which is not available on all platforms and confuses some tools and libraries. In addition, Reflection can lead to performance degradation where it is used. Due to these downsides, we generally don’t use them
Dynamics in Scala allows you to add functionality similar to the Missing Method functionality that is often found in dynamic languages. It is intended to improve the experience around dynamically typed DSLs and integration with dynamically typed languages. One of the major downsides of using dynamics, though, is a loss of type-safety. Dynamics also may not work because, in some cases, it relies on Reflection, which is not always available. Generally, these two situations are the only reason you want to use a dynamic in Scala. At Threat Stack, we neither integrate Scala with any dynamic languages nor have any dynamically typed DSLs, so we generally don't include it in our scalacOptions.
Postfix Operators — postfixOps
Postfix operators allow you to call nullary methods without the ‘.’ operator. For example, you could reverse a list as follows:
List(1, 2, 3) reverse
Many consider this code to be more readable. However, this comes at a price. If used incorrectly, postfix operators can lead to odd errors and can mess with semicolon insertion.
Using postfix operators can be a divisive issue amongst Scala programmers. Even at Threat Stack we aren’t in full agreement about whether or not to allow the use of postfix operators. Use of postfix operators varies from project to project, although many of our newer projects tend to omit this option.
Hopefully, this post has given you some insight into when you want to enable certain language features on a project-wide basis. Although it can vary based on the project, we tend to pass these options to the Scala Compiler:
scalacOptions ++= Seq( // See other posts in the series for other helpful options "-feature", "-language:existentials", "-language:higherKinds", "-language:implicitConversions" )
If anyone has any other thoughts on when to enable these language features, feel free to find me on Twitter.
For More Scala-Related Articles . . .
If you’re interested in other Scala-related articles based on the experiences of Threat Stack developers, have a look at these: