|
| 1 | +--- |
| 2 | +layout: blog-detail |
| 3 | +post-type: blog |
| 4 | +by: Jamie Thompson, Scala Center |
| 5 | +title: Forward Compatibility for the Scala 3 Transition |
| 6 | +--- |
| 7 | +# Forward Compatibility for the Scala 3 Transition |
| 8 | +Scala 3 is coming, with a release candidate slated for the end of 2020. |
| 9 | +With that knowledge comes the inevitable question: |
| 10 | +should I migrate, and what is the potential cost? |
| 11 | + |
| 12 | +With this blog we would like to outline some scenarios for maintainers of projects who |
| 13 | +want to move to Scala 3, and how these |
| 14 | +scenarios are made simpler by the ability to read Scala 3 compiled dependencies |
| 15 | +from Scala 2.13. There is also a [troubleshooting](#troubleshooting) section |
| 16 | +in case you encounter issues following the steps described in the scenarios. |
| 17 | + |
| 18 | +>As a quick aside, If you would like to know how it is possible to mix |
| 19 | +>dependencies between the two compilers, watch the talk |
| 20 | +>[Taste the difference with Scala 3](https://www.youtube.com/watch?v=YQmVrUdx8TU). |
| 21 | +> |
| 22 | +>To summarise: Scala 3 stores meta information about the code it compiles in |
| 23 | +>TASTy files, and Scala 2.13 can read these files to learn, for example, |
| 24 | +>which terms, types and implicits are defined in a given dependency, |
| 25 | +>and what code needs to be generated to use them correctly. |
| 26 | +
|
| 27 | +## Scenarios |
| 28 | + |
| 29 | +1. You have an application consisting of many subprojects that may depend on |
| 30 | +each other, built with Scala 2.13, and would like to transition each one |
| 31 | +independently to Scala 3.0 |
| 32 | +2. You have an application, built with Scala 2.13, and want to use some |
| 33 | +new features of a library that has migrated to Scala 3.0 |
| 34 | + |
| 35 | +Let's take a look at each scenario in turn: |
| 36 | + |
| 37 | +## Migrating a multi module project |
| 38 | + |
| 39 | +If you want to migrate a multi module project to Scala 3, |
| 40 | +it does not matter in which order you migrate the modules, they will be |
| 41 | +able to depend on each other regardless of whether they are compiled with |
| 42 | +Scala 2.13 or Scala 3. |
| 43 | + |
| 44 | +To illustrate this scenario, we will start with an empty directory and |
| 45 | +build an sbt project with Scala 2.13 that has two modules, `shared`, |
| 46 | +which has some common domain model data structures, and `app`, |
| 47 | +which uses those data structures. |
| 48 | + |
| 49 | +For this project, we will pick |
| 50 | +[sbt 1.4.0](https://github.com/sbt/sbt/releases/tag/v1.4.0), |
| 51 | +this allows you to mix projects of different Scala versions with very |
| 52 | +little extra effort, (thanks to Eugene Yokota). |
| 53 | + |
| 54 | +This tutorial also assumes the release of Scala `2.13.4`, but if you would |
| 55 | +like to follow along today, you can [modify the build file](#early-access) |
| 56 | +to use an early access snapshot release. |
| 57 | + |
| 58 | +To begin, our project looks like the following: |
| 59 | +```scala |
| 60 | +// project/build.properties |
| 61 | +sbt.version=1.4.0 |
| 62 | +``` |
| 63 | + |
| 64 | +```scala |
| 65 | +// build.sbt |
| 66 | + |
| 67 | +ThisBuild / scalaVersion := "2.13.4" |
| 68 | + |
| 69 | +lazy val shared = project |
| 70 | + |
| 71 | +lazy val app = project |
| 72 | + .dependsOn(shared) |
| 73 | +``` |
| 74 | +```scala |
| 75 | +// shared/src/main/scala/example/Cat.scala |
| 76 | + |
| 77 | +package example |
| 78 | + |
| 79 | +sealed trait Cat |
| 80 | +object Cat { |
| 81 | + case object Lion extends Cat |
| 82 | + case object Tiger extends Cat |
| 83 | + case object Cheetah extends Cat |
| 84 | +} |
| 85 | +``` |
| 86 | +```scala |
| 87 | +// app/src/main/scala/example/Main.scala |
| 88 | + |
| 89 | +package example |
| 90 | + |
| 91 | +object Main extends App { |
| 92 | + println(Cat.Tiger) |
| 93 | +} |
| 94 | +``` |
| 95 | +We can run this application with `sbt app/run` which will output the following: |
| 96 | +``` |
| 97 | +[info] Compiling 1 Scala source to blog/shared/target/scala-2.13/classes ... |
| 98 | +[info] Compiling 1 Scala source to blog/app/target/scala-2.13/classes ... |
| 99 | +[info] running example.Main |
| 100 | +Tiger |
| 101 | +``` |
| 102 | + |
| 103 | +At this point we can try something exciting, compile either subproject with |
| 104 | +Scala 3 and see if it works. At the time of writing, the Scala version |
| 105 | +`0.27.0-RC1` is the latest preview release of Scala 3. |
| 106 | + |
| 107 | +First, add the dotty plugin (this helps with managing and inspecting the |
| 108 | +`scalaVersion` setting with Scala 3) |
| 109 | + |
| 110 | +```scala |
| 111 | +// project/plugins.sbt |
| 112 | + |
| 113 | +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.2") |
| 114 | +``` |
| 115 | + |
| 116 | +Then we can change the `scalaVersion` of `app`: |
| 117 | + |
| 118 | +```diff |
| 119 | +// build.sbt |
| 120 | + |
| 121 | + ThisBuild / scalaVersion := "2.13.4" |
| 122 | + |
| 123 | + lazy val shared = project |
| 124 | + |
| 125 | + lazy val app = project |
| 126 | + .dependsOn(shared) |
| 127 | ++ .settings(scalaVersion := "0.27.0-RC1") |
| 128 | +``` |
| 129 | + |
| 130 | +To recap, `app` is now compiled by Scala 3 and depends on `shared`, |
| 131 | +which is compiled by Scala 2.13. |
| 132 | + |
| 133 | +If we do `sbt app/run` we should see the `app` project recompile |
| 134 | +and it will run as before. |
| 135 | + |
| 136 | +Now lets try the other way around: |
| 137 | + |
| 138 | +```diff |
| 139 | +// build.sbt |
| 140 | + |
| 141 | + ThisBuild / scalaVersion := "2.13.4" |
| 142 | + |
| 143 | + lazy val shared = project |
| 144 | ++ .settings(scalaVersion := "0.27.0-RC1") |
| 145 | + |
| 146 | + lazy val app = project |
| 147 | + .dependsOn(shared) |
| 148 | +- .settings(scalaVersion := "0.27.0-RC1") |
| 149 | +``` |
| 150 | + |
| 151 | +Here we have the opposite, `app` is compiled by Scala 2.13 and depends |
| 152 | +on `shared`, which is compiled by Scala 3. We can see this more clearly using |
| 153 | +the command `sbt 'show app/dependencyTree'`, which outputs the following: |
| 154 | +``` |
| 155 | +app:app_2.13:0.1.0-SNAPSHOT [S] |
| 156 | + +-shared:shared_0.27:0.1.0-SNAPSHOT |
| 157 | + +-ch.epfl.lamp:dotty-library_0.27:0.27.0-RC1 [S] |
| 158 | +``` |
| 159 | + |
| 160 | +If we then try `sbt app/run` both `shared` and `app` subprojects will recompile |
| 161 | +and it should work as it always has. If in your own project there are issues |
| 162 | +in changing the Scala versions, check out the |
| 163 | +[troubleshooting](#troubleshooting) section. |
| 164 | + |
| 165 | +To summarise this part, we have shown that it is possible to migrate subprojects |
| 166 | +to Scala 3 from Scala 2.13 in any order, gradually, and continue to build and |
| 167 | +run them if they mix versions. |
| 168 | + |
| 169 | +## Using a Scala 3 Library Dependency |
| 170 | + |
| 171 | +In the section above, we have seen how it is possible for a subproject compiled |
| 172 | +with Scala 2.13 to depend on a subproject compiled with Scala 3. The same |
| 173 | +relationship extends to library dependencies. |
| 174 | + |
| 175 | +To demonstrate using a third party library dependency, we will add a dependency |
| 176 | +on the fansi library to `app` and try to change our output to be colored blue: |
| 177 | + |
| 178 | +```diff |
| 179 | +// build.sbt |
| 180 | + |
| 181 | + ThisBuild / scalaVersion := "2.13.4" |
| 182 | + |
| 183 | + lazy val shared = project |
| 184 | + .settings(scalaVersion := "0.27.0-RC1") |
| 185 | + |
| 186 | + lazy val app = project |
| 187 | + .dependsOn(shared) |
| 188 | ++ .settings( |
| 189 | ++ libraryDependencies += "com.lihaoyi" % "fansi_0.27" % "0.2.9" |
| 190 | ++ ) |
| 191 | +``` |
| 192 | +Notice that here, we are using the Scala 3 version of fansi, to force this, |
| 193 | +we take a standard module id and change it as so: |
| 194 | +```diff |
| 195 | +-"com.lihaoyi" %% "fansi" % "0.2.9" |
| 196 | ++"com.lihaoyi" % "fansi_0.27" % "0.2.9" |
| 197 | +``` |
| 198 | +By replacing `%%` with `%`, we can then manually specify the binary version, |
| 199 | +leading us to add `_0.27` to the name of the module. |
| 200 | + |
| 201 | +We now update `Main.scala` to use the fansi library: |
| 202 | + |
| 203 | +```diff |
| 204 | +// app/src/main/scala/example/Main.scala |
| 205 | + |
| 206 | + package example |
| 207 | + |
| 208 | + object Main extends App { |
| 209 | +- println(Cat.Tiger) |
| 210 | ++ println(fansi.Color.Blue(Cat.Tiger.toString)) |
| 211 | + } |
| 212 | +``` |
| 213 | +If we then use the command `sbt app/run` we should notice that "Tiger" |
| 214 | +is now blue when printed. |
| 215 | + |
| 216 | +If we then run again `sbt 'show app/dependencyTree'` we see the following: |
| 217 | +``` |
| 218 | +app:app_2.13:0.1.0-SNAPSHOT [S] |
| 219 | + +-com.lihaoyi:fansi_0.27:0.2.9 |
| 220 | + | +-com.lihaoyi:sourcecode_0.27:0.2.1 |
| 221 | + | |
| 222 | + +-shared:shared_0.27:0.1.0-SNAPSHOT |
| 223 | + +-ch.epfl.lamp:dotty-library_0.27:0.27.0-RC1 [S] |
| 224 | +``` |
| 225 | +To summarise, in this section we have shown how it is possible to use a |
| 226 | +third party library dependency, compiled with Scala 3, from Scala 2.13. |
| 227 | +If there are issues with changing the binary version of a particular |
| 228 | +dependency you have, check out the [troubleshooting](#troubleshooting) section. |
| 229 | + |
| 230 | +## Troubleshooting |
| 231 | +There are some situations where the steps described in the sections above do |
| 232 | +not work out of the box for your own project, and some other considerations |
| 233 | +need to be made. |
| 234 | + |
| 235 | +For the multi module project scenario, let's say you have two subprojects |
| 236 | +`A` and `B`, where `B` depends on `A`, both compiled with Scala 2.13. |
| 237 | + |
| 238 | +### Source Incompatibilities When Changing to Scala 3 |
| 239 | +If you change the `scalaVersion` of `A` to Scala 3 and `A` fails to build, |
| 240 | +and the error is not due to a missing dependency, the source code of `A` may |
| 241 | +be incompatible with Scala 3 and you will have to make some changes. |
| 242 | +You can consult the [Scala 3 Migration Guide](https://scalacenter.github.io/scala-3-migration-guide/) |
| 243 | +to see what changes may be required. |
| 244 | + |
| 245 | +### Missing Scala 3 Dependencies |
| 246 | +If you change the `scalaVersion` of `A` to Scala 3 and `A` fails to build |
| 247 | +due to a missing dependency, you can try to use the Scala 2.13 version of |
| 248 | +the dependency. The sbt-dotty plugin provides |
| 249 | +[helper methods](https://github.com/lampepfl/dotty-example-project#getting-your-project-to-compile-with-dotty) |
| 250 | +for this. |
| 251 | + |
| 252 | +### Scala 3 Dependencies That Break Compilation |
| 253 | + |
| 254 | +If you successfully migrate subproject `A` to Scala 3, but now `B` fails |
| 255 | +to compile, there are several likely possibilities: |
| 256 | + |
| 257 | +1. `A` has made a deliberate but [forward compatible](#forward-compatibility) |
| 258 | +change to its API, and `B` needs to update to use the new API. |
| 259 | +2. `A`, or a transitive dependency of `A`, uses a feature of Scala 3 that |
| 260 | +is not [forwards compatible](#forward-compatibility) with Scala 2.13. |
| 261 | +2. `A`, or a transitive dependency of `A`, defines a Scala 3 |
| 262 | +macro but does not provide an equivalent Scala 2 macro (see an |
| 263 | +[example project](https://github.com/scalacenter/mix-macros-scala-2-and-3) |
| 264 | +here for how to provide macros to Scala 2.13 from Scala 3.) |
| 265 | +4. There is a bug in the implementation of reading Scala 3 dependencies |
| 266 | +in Scala 2.13. [Please report here](https://github.com/scala/bug). |
| 267 | + |
| 268 | +### Scala 3 Dependencies that Break Runtime Behaviour |
| 269 | + |
| 270 | +If you successfully migrate subproject `A` to Scala 3, and `B` compiles |
| 271 | +successfully, but now fails at runtime, there are two likely possibilities: |
| 272 | + |
| 273 | +1. The definitions in the Scala 3 dependency were correctly read, but |
| 274 | +perhaps the API changed subtly, so the code of `B` must adapt. |
| 275 | +For example, perhaps a default implicit value has been replaced, or other |
| 276 | +code has changed its implementation while using the same signature. |
| 277 | +2. There is a bug in the implementation of reading Scala 3 dependencies |
| 278 | +in Scala 2.13. [Please report here](https://github.com/scala/bug). |
| 279 | + |
| 280 | +### Third Party Scala 3 Dependencies that Break Compilation/Runtime |
| 281 | +If depending on a third party library, compiled with Scala 3, breaks |
| 282 | +compilation, or disrupts runtime behaviour, please consider the above |
| 283 | +troubleshooting topics, but instead assume that `A` is the third party |
| 284 | +library, and `B` is your Scala 2.13 project that depends on it. |
| 285 | + |
| 286 | +## Forward Compatibility |
| 287 | +When migrating a subproject to Scala 3, where downstream consuming subprojects are likely to be on Scala 2.13, we would recommend restricting the usage of new features in Scala 3. This will maximise compatibility as you migrate each subproject. However, it is possible to start using some features of Scala 3 without issue, this is due to a limited forward compatibility in Scala 2.13 with some new Scala 3 features. |
| 288 | + |
| 289 | +Forward compatibility means that many definitions created by using new Scala 3 features |
| 290 | +can be used from Scala 2.13, however they will be remapped to features |
| 291 | +that exist in Scala 2.13. For example, |
| 292 | +[extension methods](http://dotty.epfl.ch/docs/reference/contextual/extension-methods.html) |
| 293 | +can only be used as ordinary methods, using their expanded name. |
| 294 | + |
| 295 | +On the other hand, some features of Scala 3 are not mappable to features in Scala 2.13, |
| 296 | +and will cause a compile-time error when using them. A longer list can be seen in the |
| 297 | +migration guide, describing [how Scala 2 reacts to different Scala 3 features](https://scalacenter.github.io/scala-3-migration-guide/docs/compatibility.html#the-scala-2-tasty-reader). |
| 298 | + |
| 299 | +For unsupported features, a best effort is made |
| 300 | +to report errors at the use-site that is problematic. For example, |
| 301 | +[match types](http://dotty.epfl.ch/docs/reference/new-types/match-types.html) |
| 302 | +are not supported. If we define in the `shared` project the type `Elem`: |
| 303 | + |
| 304 | +```scala |
| 305 | +// shared/src/main/scala/example/MatchTypes.scala |
| 306 | +package example |
| 307 | + |
| 308 | +object MatchTypes { |
| 309 | + type Elem[X] = X match { |
| 310 | + case List[t] => t |
| 311 | + case Array[t] => t |
| 312 | + } |
| 313 | +} |
| 314 | +``` |
| 315 | + |
| 316 | +and then try to use it in the `app` project: |
| 317 | +```scala |
| 318 | +// app/src/main/scala/example/TestMatchTypes.scala |
| 319 | +package example |
| 320 | + |
| 321 | +object TestMatchTypes { |
| 322 | + def test: MatchTypes.Elem[List[String]] = "hello" |
| 323 | +} |
| 324 | +``` |
| 325 | +we get the following error when calling `sbt app/run`: |
| 326 | + |
| 327 | +``` |
| 328 | +[error] TestMatchTypes.scala:5:25: Unsupported Scala 3 match type in bounds of type Elem; found in object example.MatchTypes. |
| 329 | +[error] def test: MatchTypes.Elem[List[String]] = "hello" |
| 330 | +[error] ^ |
| 331 | +[error] one error found |
| 332 | +``` |
| 333 | + |
| 334 | +The error is standard for all unsupported Scala 3 features, naming the feature, |
| 335 | +the location of the definition and the location where it is used. |
| 336 | + |
| 337 | +## Contributing |
| 338 | + |
| 339 | +If you have found an issue with Scala 3 dependencies in Scala 2.13 and want to try |
| 340 | +fixing it in the Scala 2 compiler, you can see a summary of how to work on the |
| 341 | +implementation here: |
| 342 | +[Working with the Tasty Reader](https://github.com/scala/scala/blob/2.13.x/doc/internal/tastyreader.md#working-with-the-code) |
| 343 | + |
| 344 | +## Summary |
| 345 | + |
| 346 | +In this blog, we have outlined how it is possible to incrementally migrate a |
| 347 | +project to Scala 3 while continuing to use all the parts together during the |
| 348 | +transition. We encourage you to try out this process and let us know of any |
| 349 | +issues with using Scala 3 dependencies from Scala 2.13. |
| 350 | + |
| 351 | +## Early Access |
| 352 | + |
| 353 | +If you would like to try out Scala 3 dependencies early, you can use a |
| 354 | +snapshot version of Scala 2.13.4 by adding the Scala integration resolver |
| 355 | +to your build: |
| 356 | +```diff |
| 357 | +// build.sbt |
| 358 | + |
| 359 | +-ThisBuild / scalaVersion := "2.13.4" |
| 360 | ++ThisBuild / scalaVersion := "2.13.4-bin-8891679" |
| 361 | + |
| 362 | ++Global / resolvers += "scala-integration".at( |
| 363 | ++ "https://scala-ci.typesafe.com/artifactory/scala-integration/") |
| 364 | + |
| 365 | + ... |
| 366 | +``` |
| 367 | + |
| 368 | +## Important Links |
| 369 | + |
| 370 | +- [Scala 3 Migration Guide](https://scalacenter.github.io/scala-3-migration-guide/) |
| 371 | +- [Doc: Working with the Tasty Reader](https://github.com/scala/scala/blob/2.13.x/doc/internal/tastyreader.md#working-with-the-code) |
| 372 | +- [Talk: Taste the difference with Scala 3](https://www.youtube.com/watch?v=YQmVrUdx8TU) |
| 373 | +- [Scala 2 issue tracker](https://github.com/scala/bug) |
0 commit comments