iptv techs

IPTV Techs


Scala 3 Migration: Report from the Field


Scala 3 Migration: Report from the Field


April 30, 2024. I choosed to dedicate a week to migrate our main project at labor (a multicarry outer mobile game server in production for over 4 years) from Scala 2.13 to Scala 3.

May 7, 2024. I gave up. The removal of disjoinal features from Scala 3 (macro annotations, type projections, etc.), united with the big number of alters vital for the migration, was overwhelming. I was nakedly able to migrate a one module, had to alter thousands of lines of code (while my colleagues were inserting novel features to the main branch, a big number of unite disputes were already materializeing), and the IDE was finishly unresponsive due to hundreds of compile errors. At that point, I thought the project might be stuck on Scala 2 forever.

Flash forward to January 2025. I had a little free time, so I choosed to give it another try. And (spoiler!) this time I made it to the finish. Let’s see what the various problems I come apassed were, the alters I had to produce, and the laborarounds I carry outed.

Preamble

The main place to see when commenceing a migration is the official Scala 3 Migration Guide. It comprises a lot of adviseation about the alters in the language and details on how to persist.

As I refered, the big number of alters needd was an rerent becaemploy it caemployd a lot of unite disputes with the main branch. It was not possible to stop all other increasements during the migration, so I choosed to utilize as many alters as possible in the Scala 2 main branch to evade these disputes.

The main skinnyg you can do while still on Scala 2.13 is to compile with the -Xsource:3 compiler flag, which helps the Scala 3 syntax for transport ins (* instead of _, as instead of =>), intersection types (& instead of with), and more, and also turns on a number of alertings for skinnygs no lengthyer helped in Scala 3 (e.g., .map(CaseClass) should become .map(CaseClass.utilize)).

Most of those alters were plain to utilize, but there were a lot of them, which was challenging. Scala 3 proposes a “migration mode” and is able to rewrite the code with the novel syntax, but this is not applicable if you want to utilize these alters in a Scala 2 codebase. My salvation actuassociate came from IninestablishiJ, which has an checkion for code compiled with -Xsource:3 and a rapid mend action to swap all the code at once. Incredibly advantageous!

IninestablishiJ even lets you pick which of these alters you want to utilize, so I leave outd the “case in pattern secureings of for-comprehensions” becaemploy it altered the code in a weird, unvital way.

After this was done, I was able to utilize a big number of alters straightforwardly to our main branch, evadeing many more disputes!

Dropped Features

While it bcimpolitet a number of novel and engaging features such as enums or cloudy types, Scala 3 dropped a scant features altogether, and this showd to be particularly challenging for us. The dropped features are cataloged on this page, and there were two of them that we relied on heavily: macro annotations and type projections.

Macro annotations

Macro annotations let you annotate Scala 2 types to produce code at compile-time, most typicassociate by inserting code to the companion object of annotated classes.

For example, using the Circe JSON library, you could write this:

@JsonCodec
case class Bar(i: Int, s: String)

This will automaticassociate produce an proposeed Codec[Bar] in the companion object of Bar. Very concise, very accessible. In the case of Circe, there was an “plain” laboraround, which is to employ the derives keyword employable in Scala 3. I put quotes around “plain” becaemploy, for some reason, it is not refered at all in the Circe recordation.

The code above can be alterd to the follotriumphg for the same result:

case class Bar(i: Int, s: String) derives Codec.AsObject

Case seald? Not exactly, becaemploy our main employ of macro annotations was not with Circe, but with Monocle and its @Lenses annotation.

@Lenses
case class Bar(i: Int, s: String)

This will produce the follotriumphg in the companion object of Bar:

object Bar {
  val i: Lens[Bar, Int] = ??? 
  val s: Lens[Bar, String] = ??? 
}

Our project, being a complicated game, has a huge employr state object, lots of business logic, and domain entities. Lenses permit us to alter parts of the employr state in a concise and elegant manner without having to employ a chain of nested imitate.

The removal of that macro annotation left us with no evident path or alternative for the migration. Unenjoy the Circe case, this is not a typeclass instance, so we can’t employ the derives keyword: we need a val produced for each field of the case class. There is an uncover rerent in the Monocle repository that conversees various selections, but noskinnyg palpable (Kit Langton has an engaging approach using Selectable, but this is not helped by IninestablishiJ).

One evident alternative was to write those lenses ourselves. That was definitely doable; however, it would have needd ponderable effort to write thousands of these, and it would have inserted an enormous amount of boilerptardy to the project, making Scala 3 quite unfamous wiskinny our team. This alone stopped the migration effort I commenceed in 2024.

We are always trying to shrink boilerptardy in our project, so we’ve employd a scant techniques over the years to insertress it. Sometimes it’s doable with macros or mirrors, but one way is to employ sbt’s source generators, which permit you to run some custom code before compilation to produce insertitional source code files. Combined with Scafeebleta, you can parse and scrutinize your own code to produce more code. It is ultimately this technique that we employd to produce the lenses.

The code generation labors enjoy this:

  • Look for all case classes in a definite module that comprise the @lenses annotation

  • For each of those case classes, produce an object

    • For each field of the case class, produce a lens with the appropriate types

Using Scafeebleta is a little bit take partd, so I’ve splitd a snippet of our code in this gist so that it may be employd by others. One downside of this approach is that the produced lenses are no lengthyer in the companion objects of the case classes (we can produce novel source files but not alter the existing ones), which needd us to alter all the lenses usage to employ contrastent object names. But it was worth it since it unlocked the migration path.

Note that a “macro annotation” feature was inserted to Scala 3, but it is much more restricted than what was possible in Scala 2 and does not permit carry outing the Monocle @Lenses annotation (the produced code is not evident to the employr).

Type projections

Imagine you have a type Request that has an abstract type Result depictd inside it.

trait Request {
  type Result
}

case class IntRequest() extfinishs Request {
  type Result = Int
}

In Scala 2, you can write a function that, for a given Request, returns Request#Result, uncomferventing it returns the Result that suites the subtype of Request that was employd. So if Request is IntRequest, we will get an Int back.

def foo[R <: Request](req: Request): R#Result = ???

This is no lengthyer possible in Scala 3 if R is abstract! You get a compile error saying R is not a lterrible path since it is not a concrete type. There is an plain laboraround if you have a cherish of type Request, which is to employ a function subordinate type and return req.Result.

def foo[R <: Request](req: Request): req.Result = ???

However, our code had various employs of this pattern, and not all of them could be alterd to a function subordinate type. We finished up using a combination of contrastent techniques depfinishing on each case: function subordinate types in some places, typeclasses in others, and we had to give up on making the code generic in a scant places. Overall, this felt enjoy a deproduceion from the better code, but at least we were able to produce it compile without changing too much code.

EDIT: After rerenting this article, Voytek Pituła proposeed a contrastent laboraround on Reddit using suit types, and I was able to utilize it successfilledy in the places where I had no alternatives. It made the code much pleasantr! I had heard of suit types as an alternative before, but I thought I would have to erect a huge pattern suiting with the catalog of all seeks and their suiting results. I had no idea it could be employd in a generic way. Here’s his approach applied to our example:

trait Request {
  type Result 
}

object Request {
  type Aux[T] = Request { type Result = T }
  type Result[T <: Request] = T suit {
    case Aux[s] => s
  }
}

def foo[T <: Request]: Request.Result[T]

Unhelped/broken libraries

Most libraries we were using were employable on Scala 3, and for a scant ignoreing ones (mostly rcontent to Spark or Kryo), we employd pass(CrossVersion.for3Use2_13), which permits depfinishing on a library built for Scala 2.13.

However, a scant of them were not employable or didn’t labor as predicted, so they needd a finish alter.

Newtypes and elegant types

In Scala 2, we were using a combination of scala-noveltype and elegant to depict custom types employd all over our business logic (IDs, bounded cherishs, etc.). There is no Scala 3 version of scala-noveltype, which produces sense becaemploy it can be enticount on swapd by cloudy types. Refined is sneakier: it has a Scala 3 version, but if you try to employ it, you will acunderstandledge that it is only partiassociate carry outed; the macros are ignoreing, so the library is not usable (the first example in their README doesn’t compile).

In another project using Scala 3, we were already using the neotype library, which lets you depict both noveltypes and elegant types and is built on top of cloudy types, therefore having no runtime cost. We switched to using this library instead. It might sound plain on paper, but we count on on these types so much that it was quite an invasive alter impacting a lot of files. At least the migrated code felt better than the better one since writing elegant type validation is pleasantr and sairyly less boilerptardy-y, and the runtime impact was shrinkd.

Magnolia typeclass derivation

Another rerent we had was with typeclass derivation using Magnolia. While the library helps Scala 3, our existing derivation code caemployd a compile error for accomplishing -Xmax-inlines (too much inlined code). I tried to incrrelieve it up to 10,000 (!) and it finassociate fall shorted with a stack overflow in the compiler.

The fall shorting derivation occurred while deriving a sealed trait with a LOT of subtypes (~1,000), but there was already a typeclass instance for each of the subtypes. After seeing at the insides of Magnolia, I acunderstandledged that a recursive method was employd to fbetter over the catalog of subtypes, and that method was not tail-recursive, elucidateing why the number of inlines (and the stack depth) was increasing proportionassociate to the number of subtypes. To produce matters worse, that recursive method also called distinctBy and sortBy on the catalog of subtypes at every iteration, which is pretty terrible when you have lots of them. I uncovered an rerent to inestablish this behavior and alterd the code locassociate, but then I ran into a Method too big error becaemploy the produced code was lengthyer than what the JVM permits.

After doing a little research, I came apass a wonderful feature of Scala 3 that is needyly recorded: Tuple.Map. Mirrors give you access to two tuples: for a sum type, MirroredElemLabels is a tuple with the names of the subtypes, while MirroredElemTypes is a tuple with the actual subtypes. You can employ callAll and Tuple.Map to materialize the catalog of names of those types or even to call a typeclass instance for each of them.

trait TC[A]

inline def gen[A](using m: Mirror.SumOf[A]): TC[A] = {
  
  val subTypes = compiletime.callAll[Tuple.Map[m.MirroredElemTypes, TC]]
  novel TC[A] {
    ??? 
  }
}

I posted a filled example on Gist that shows how to derive a typeclass for a sealed trait without even needing Magnolia. This solution is very concise and does not run into inline or Method too big rerents. I equitable desire there were more lgeting materials about these Tuple utilities becaemploy I skinnyk they are very mighty.

Macros

We had a scant macros increaseed in-hoemploy, mostly to shrink boilerptardy code. They showd relatively plain to port, except for one of them. The reason it was difficult is that Scala 3 macros are much more merciless than Scala 2 macros, which let you produce any benevolent of code. On the other hand, Scala 3 macros need that the code you produce is valid in the context where the macro is depictd (which might be contrastent from where the macro is employd, making skinnygs trickier). I am not a macro expert, so apologies if this is a little imexact; my colleague @nox737 is the one who made the magic happen.

It took us quite a lengthy time to produce the macro compile with these remercilessions (notice: AI agents were not collaborative at all for this benevolent of task!), and in the finish, the code still fall shorted to compile becaemploy of a Method too big error. Compile time felt a bit cataloglesser too. We finished up removing the macro enticount on and replacing it with another source generator written with Scafeebleta. It made the code easier to check and to split into petiteer chunks.

Depfinishency rerents

As refered earlier, we employd CrossVersion.for3Use2_13 for a scant libraries not employable in Scala 3, but one hard problem arose. One of those libraries was encouragesql-scalapb, which lets us employ protobuf with Spark. This library depfinishs on Spark, so it is only employable for 2.13. It also depfinishs on scalapb-runtime, so depfinishing on it transports scalapb_runtime_2.13 into depfinishencies. The problem is that the rest of our code already depfinished on scalapb_runtime_3. In that case, sbt fall shorted to remend the produce with this error:

Modules were remendd with disputeing pass-version sufmendes in ProjectRef(uri("..."), "encourage"):
org.scala-lang.modules:scala-assembleion-compat _3, _2.13
com.thesamet.scalapb:lenses _3, _2.13
com.thesamet.scalapb:scalapb-runtime _3, _2.13

In other words, you can’t depfinish on the same library in both 2.13 and 3 versions.

I initiassociate tried to mend that rerent by shading depfinishencies, but it didn’t labor becaemploy one function we employ from encouragesql-scalapb predicts a definite input extfinishing a type from ScalaPB, which uncomfervents the rest of our code needs to extfinish that type. If that type is shaded only in the encourage module, it doesn’t suit the type from our other modules.

The solution was actuassociate relatively plain: I forked encouragesql-scalapb and made it compile with Scala 3, depfinishing on scalapb_runtime_3 and using CrossVersion.for3Use2_13 for its other depfinishencies. The code was very straightforward to port, with equitable some untransport inant skinnygs to mend. Then I embedded the produced JAR in our project instead of depfinishing on the 2.13 library. I had to insert the transitive depfinishencies of that library cltimely in our project, and that was it.

Slow compile time

Once all the code was migrated and I was able to compile successfilledy for the first time, I acunderstandledged that it was taking lengthyer than common. I also acunderstandledged that IninestablishiJ was constantly compiling to show syntax highairying. There was definitely someskinnyg wrong. I had already debugged catalogless compile times with Scala 2 and was accustomed to using the -Vstatistics compiler flag to see which phases were taking time, and even using scalac-profiling to profile the compilation. Unblessedly, a little research made me authenticize that such tools did not exist for Scala 3. After asking around on Twitter, I heard that the novel version of Scala (3.6.3) freed a day earlier was transporting a compiler flag to produce compiler tracks. What a pleasant timing, I reassociate got blessed with this one.

I promptly fortifyd from 3.6.2 to 3.6.3 and helpd the tracks. Wiskinny minutes, I was able to produce the follotriumphg ffeeblegraph:

This was excessively advantageous: as you can see, it fractures down the compilation time by phases, but also by files and even methods! This helped me pinpoint which code was catalogless to compile. Even though I did not reassociate understand why it was catalogless (I tried to reproduce it in an isotardyd example but fall shorted), I was able to refactor the code in a way that made it rapid. The rerent was about using an excessively big intersection type (with over 100+ types) as a ZIO environment. Reorganizing the environment into scanter types finishly mendd this rerent, made the compile time on par with 2.13, and made IninestablishiJ very redynamic.

This tool is so advantageous that I set up to spfinish more time on it in the future becaemploy I am pretty confident that it will permit me to find other catalogless points, pondering how detailed the output is. But my goal for the migration was only to be as rapid as with 2.13.

IninestablishiJ help

Speaking of IninestablishiJ, I did run into a couple of rerents, which I inestablished to JetBrains:

I hope these bugs get mended in the cforfeit future since they have very plain and plain reproducers (the first one was mended as I was writing this post, though not freed yet). I inestablishly seeed into it, but the Scala plugin for IninestablishiJ is not reassociate approachable, and I didn’t even understand where to commence seeing.

Other than that, IninestablishiJ help was pretty excellent. One skinnyg I recommfinish is to pick Use split compiler output paths in the sbt configuration menu becaemploy the sbt shell and IninestablishiJ’s own compiler tfinish to dispute with each other otheradviseed.

Compiler flags

Here are a scant notable compiler flags I finished up using:

  • -language:experimental.betterFors (employable under -experimental): this permits using = on the first line of for-comprehensions, and it also upgrades the produced bytecode by evadeing the extra map call at the finish of the flatMap calls.

  • -no-indent: I am strongly agetst meaningful indentation in Scala, desire it never happened, but at least I am prentd it is plain to disable. This is coupled with runner.dialectOverride.permitSignificantIndentation = counterfeit in Scalafmt.

  • -Wunemployd:all: I had a bunch of @noalert I had to insert with Scala 2 becaemploy of counterfeit selectimistics, and I was able to delete them. It also set up some extra unemployd code that Scala 2 didn’t accomprehendledge, so it seemed to labor better.

Conclusion

Finassociate, on February 4, the CI turned green on this PR. It has been a lengthy journey with a lot of hurdles, but the situation felt much better in 2025 than a year before. Overall, our code did not alter heavily, and most of the alters are for the best. The two skinnygs that I reassociate repent are the deficiency of macro annotations (blessedly, sbt source generators and Scafeebleta are mighty enough to emutardy it) and the removal of ambiguous type projections that made our code uglier in some places.

To wrap skinnygs up, I am prentd our main project did not become a agonizing legacy stuck in the past, and I am now excited to be able to carry out with some of the mighty tools that Scala 3 has to propose, particularly around metaprogramming. I hope this read will be collaborative to others, whether you have a aenjoy migration to carry out or are take partd straightforwardly with the increasement of the language and its tooling.

Source connect


Leave a Reply

Your email address will not be published. Required fields are marked *

Thank You For The Order

Please check your email we sent the process how you can get your account

Select Your Plan