|
| 1 | +--- |
| 2 | +layout: blog-detail |
| 3 | +post-type: blog |
| 4 | +by: Oliver Bračevac, EPFL |
| 5 | +title: "Upcoming Changes to Givens in Scala 3.7" |
| 6 | +--- |
| 7 | + |
| 8 | +## New Prioritization of Givens in Scala 3.7 |
| 9 | + |
| 10 | +Scala 3.7 will introduce changes to how `given`s are resolved, which |
| 11 | +may affect program behavior when multiple `given`s are present. The |
| 12 | +aim of this change is to make `given` resolution more predictable, but |
| 13 | +it could lead to problems during migration to Scala 3.7 or later |
| 14 | +versions. In this article, we’ll explore the motivation behind these |
| 15 | +changes, potential issues, and provide migration guides to help |
| 16 | +developers prepare for the transition. |
| 17 | + |
| 18 | +### Motivation: Better Handling of Inheritance Triangles & Typeclasses |
| 19 | + |
| 20 | +The motivation for changing the prioritization of `given`s stems from |
| 21 | +the need to make interactions within inheritance hierarchies, |
| 22 | +particularly inheritance triangles, more intuitive. This adjustment |
| 23 | +addresses a common issue where the compiler struggles with ambiguity |
| 24 | +in complex typeclass hierarchies. |
| 25 | + |
| 26 | +For example, functional programmers will recognize the following |
| 27 | +inheritance triangle of common typeclasses: |
| 28 | + |
| 29 | +```scala |
| 30 | +trait Functor[F[_]]: |
| 31 | + extension [A, B](x: F[A]) def map(f: A => B): F[B] |
| 32 | +trait Monad[F[_]] extends Functor[F] { ... } |
| 33 | +trait Traverse[F[_]] extends Functor[F] { ... } |
| 34 | +``` |
| 35 | +Now, suppose we have corresponding instances of these typeclasses for `List`: |
| 36 | +```scala |
| 37 | +given Functor[List] = ... |
| 38 | +given Monad[List] = ... |
| 39 | +given Traverse[List] = ... |
| 40 | +``` |
| 41 | +Let’s use these in the following context: |
| 42 | +```scala |
| 43 | +def fmap[F[_] : Functor, A, B](c: F[A])(f: A => B): F[B] = c.map(f) |
| 44 | + |
| 45 | +fmap(List(1,2,3))(_.toString) |
| 46 | +// ^ rejected by Scala < 3.7, now accepted by Scala 3.7 |
| 47 | +``` |
| 48 | + |
| 49 | +Before Scala 3.7, the compiler would reject the `fmap` call due to |
| 50 | +ambiguity. Since it prioritized the `given` instance with the _most |
| 51 | +specific_ subtype of the context bound `Functor`, both `Monad[List]` |
| 52 | +and `Traverse[List]` were valid candidates for `Functor[List]`, but |
| 53 | +neither was more specific than the other. However, all that’s required |
| 54 | +is the functionality of `Functor[List]`, the instance with the _most |
| 55 | +general_ subtype, which Scala 3.7 correctly picks. |
| 56 | + |
| 57 | +This change aligns the behavior of the compiler with the practical |
| 58 | +needs of developers, making the handling of common triangle |
| 59 | +inheritance patterns more predictable. |
| 60 | + |
| 61 | +### Source Incompatibility of the New Givens Prioritization |
| 62 | + |
| 63 | +While the new `given` prioritization improves predictability, it may |
| 64 | +affect source compatibility in existing Scala codebases. Let’s |
| 65 | +consider an example where a library provides a default `given` for a |
| 66 | +component: |
| 67 | + |
| 68 | +```scala |
| 69 | +// library code |
| 70 | +class LibComponent: |
| 71 | + def msg = "library-defined" |
| 72 | + |
| 73 | +// default provided by library |
| 74 | +given libComponent: LibComponent = LibComponent() |
| 75 | + |
| 76 | +def printComponent(using c: LibComponent) = println(c.msg) |
| 77 | +``` |
| 78 | + |
| 79 | +Up until Scala 3.6, clients of the library could override |
| 80 | +`libComponent` with a user-defined one through subtyping: |
| 81 | + |
| 82 | +```scala |
| 83 | +// client code |
| 84 | +class UserComponent extends LibComponent: |
| 85 | + override def msg = "user-defined" |
| 86 | + |
| 87 | +given userComponent: UserComponent = UserComponent() |
| 88 | + |
| 89 | +@main def run = printComponent |
| 90 | +``` |
| 91 | + |
| 92 | +Now, let’s run the example: |
| 93 | + |
| 94 | +```scala |
| 95 | +run // Scala <= 3.6: prints "user-defined" |
| 96 | + // Scala 3.7: prints "library-defined" |
| 97 | +``` |
| 98 | + |
| 99 | +What happened? In Scala 3.6 and earlier, the compiler prioritized the |
| 100 | +`given` with the _most specific_ compatible subtype |
| 101 | +(`userComponent`). However, in Scala 3.7, it selects the value with |
| 102 | +the _most general_ subtype instead (`libComponent`). |
| 103 | + |
| 104 | +This shift in prioritization can lead to unexpected changes in |
| 105 | +behavior when migrating to Scala 3.7, requiring developers to review |
| 106 | +and potentially adjust their codebases to ensure compatibility with |
| 107 | +the new `given` resolution logic. Below, we provide some tips to help |
| 108 | +with the migration process. |
| 109 | + |
| 110 | +## Migrating to the New Prioritization |
| 111 | + |
| 112 | +### Community Impact |
| 113 | + |
| 114 | +We have conducted experiments on the [open community |
| 115 | +build](https://github.com/VirtusLab/community-build3) that showed that |
| 116 | +the proposed scheme will result in a more intuitive and predictable |
| 117 | +`given` resolution. The negative impact on the existing projects is very |
| 118 | +small. We have tested 1500 open-source libraries, and new rules are |
| 119 | +causing problems for less than a dozen of them. |
| 120 | + |
| 121 | +### Roadmap |
| 122 | + |
| 123 | +The new `given` resolution scheme, which will be the default in Scala |
| 124 | +3.7, can already be explored in Scala 3.5. This early access allows |
| 125 | +the community ample time to test and adapt to the upcoming changes. |
| 126 | + |
| 127 | +**Scala 3.5** |
| 128 | + |
| 129 | +Starting with Scala 3.5, you can compile with `-source 3.6` to receive |
| 130 | +warnings if the new `given` resolution scheme would affect your |
| 131 | +code. This is how the warning might look: |
| 132 | + |
| 133 | +```scala |
| 134 | +-- Warning: client.scala:11:30 ------------------------------------------ |
| 135 | +11 |@main def run = printComponent |
| 136 | + | ^ |
| 137 | + | Given search preference for LibComponent between alternatives |
| 138 | + | (userComponent : UserComponent) |
| 139 | + | and |
| 140 | + | (libComponent : LibComponent) |
| 141 | + | has changed. |
| 142 | + | Previous choice : the first alternative |
| 143 | + | New choice from Scala 3.7: the second alternative |
| 144 | +``` |
| 145 | + |
| 146 | +Additionally, you can compile with `-source 3.7` or `-source future` |
| 147 | +to fully enable the new prioritization and start experiencing its |
| 148 | +effects. |
| 149 | + |
| 150 | +**Scala 3.6** |
| 151 | + |
| 152 | +In Scala 3.6, these warnings will be on by default. |
| 153 | + |
| 154 | +**Scala 3.7** |
| 155 | + |
| 156 | +Scala 3.7 will finalize the transition, making the new `given` |
| 157 | +prioritization the standard behavior. |
| 158 | + |
| 159 | +#### Suppressing Warnings |
| 160 | + |
| 161 | +If you need to suppress the new warning related to changes in `given` |
| 162 | +search preference, you can use Scala’s facilities for configuring |
| 163 | +warnings. For example, you can suppress the warning globally via the |
| 164 | +command line: |
| 165 | + |
| 166 | +```bash |
| 167 | +scalac file.scala "-Wconf:msg=Given search preference:s" |
| 168 | +``` |
| 169 | + |
| 170 | +It is also possible to selectively suppress the warning |
| 171 | +using the [`@nowarn` annotation](https://www.scala-lang.org/api/current/scala/annotation/nowarn.html): |
| 172 | + |
| 173 | +```scala |
| 174 | +import scala.annotation.nowarn |
| 175 | + |
| 176 | +class A |
| 177 | +class B extends A |
| 178 | + |
| 179 | +given A() |
| 180 | +given B() |
| 181 | + |
| 182 | +@nowarn("msg=Given search preference") |
| 183 | +val x = summon[A] |
| 184 | +``` |
| 185 | + |
| 186 | +For more details, you can consult the guide on [configuring and suppressing warnings]({{ site.baseurl }}/2021/01/12/configuring-and-suppressing-warnings.html). |
| 187 | + |
| 188 | +**Caution**: Suppressing warnings should be viewed as a temporary |
| 189 | +workaround, not a long-term solution. While it can help address rare |
| 190 | +false positives from the compiler, it merely postpones the inevitable |
| 191 | +need to update your codebase or the libraries your project depends |
| 192 | +on. Relying on suppressed warnings may lead to unexpected issues when |
| 193 | +upgrading to future versions of the Scala compiler. |
| 194 | + |
| 195 | +### Workarounds |
| 196 | + |
| 197 | +Here are some practical strategies to help you smoothly adapt to the |
| 198 | +new `given` resolution scheme: |
| 199 | + |
| 200 | +#### Resorting to Explicit Parameters |
| 201 | + |
| 202 | +If the pre-3.7 behavior is preferred, you can explicitly pass the |
| 203 | +desired `given`: |
| 204 | +```scala |
| 205 | +@main def run = printComponent(using userComponent) |
| 206 | +``` |
| 207 | + |
| 208 | +To determine the correct explicit parameter (which could involve a |
| 209 | +complex expression), it can be helpful to compile with an earlier |
| 210 | +Scala version using the `-Xprint:typer` flag: |
| 211 | +```scala |
| 212 | +scalac client.scala -Xprint:typer |
| 213 | +``` |
| 214 | +This will output all parameters explicitly: |
| 215 | +```scala |
| 216 | +... |
| 217 | +@main def run: Unit = printComponent(userComponent) |
| 218 | +... |
| 219 | +``` |
| 220 | + |
| 221 | +#### Explicit Prioritization by Owner |
| 222 | + |
| 223 | +One effective way to ensure that the most specific `given` instance is |
| 224 | +selected -— particularly useful when migrating libraries to Scala 3.7 -— |
| 225 | +is to leverage the inheritance rules as outlined in point 8 of [the |
| 226 | +language |
| 227 | +reference](https://docs.scala-lang.org/scala3/reference/changed-features/implicit-resolution.html): |
| 228 | + |
| 229 | +```scala |
| 230 | +class General |
| 231 | +class Specific extends General |
| 232 | + |
| 233 | +class LowPriority: |
| 234 | + given a:General() |
| 235 | + |
| 236 | +object NormalPriority extends LowPriority: |
| 237 | + given b:Specific() |
| 238 | + |
| 239 | +def run = |
| 240 | + import NormalPriority.given |
| 241 | + val x = summon[General] |
| 242 | + val _: Specific = x // <- b was picked |
| 243 | +``` |
| 244 | + |
| 245 | +The idea is to enforce prioritization through the inheritance |
| 246 | +hierarchies of classes that provide `given` instances. By importing the |
| 247 | +`given` instances from the object with the highest priority, you can |
| 248 | +control which instance is selected by the compiler. |
| 249 | + |
| 250 | +### Outlook |
| 251 | + |
| 252 | +We are considering adding `-rewrite` rules that automatically insert |
| 253 | +explicit parameters when a change in choice is detected. |
| 254 | + |
| 255 | + |
| 256 | + |
0 commit comments