Skip to content

Commit decdf22

Browse files
authored
Merge pull request #1675 from bracevac/given-priority
Blog post about given prioritization changes
2 parents 8ea5690 + fd42c36 commit decdf22

File tree

1 file changed

+256
-0
lines changed

1 file changed

+256
-0
lines changed
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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

Comments
 (0)