Language Evolution: SIPs, Preview & Experimental
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and every program on it was compiled and run with Scala CLI 1.17.1 on JDK 26, checked on 2026-10-16. Where the answer differs on the 3.9 LTS line (3.9.0, 2026-09-03) the same file was also run with Scala 3.9.0 from Maven Central. Scala changes through a public process and through a three-stage lifecycle for unfinished features; this page explains both, shows how to switch an unfinished feature on, how far the label experimental or preview reaches into your own build, and lists what is in each state today. The language features themselves are covered on their own pages and are linked from the tables. The release lines are introduced in Getting Started & Versions, and what to do when you move a project from one version to the next is the subject of Upgrading Between Scala 3 Versions; the compiler flags are covered in Compiler Options, Linting & Errors.
The examples use the trailhead scenario: a toolkit that plans hikes on a network of mountain trails.
The Scala Improvement Process
The language and the core of the standard library are changed through the Scala Improvement Process, SIP for short. A
change to the syntax, the type system or the semantics, or to anything the language specification refers to (primitive
types, Seq), needs a design document, a Scala Improvement Proposal. Compiler work that does not touch the language,
such as lint warnings, optimizations and error-message quality, is outside the process. The committee consists of a
chairperson, a secretary and voting members, meets monthly and votes on proposals. The chairperson is an appointed employee
of the Scala Center and does not vote.
A SIP moves through four stages. The stage names are the ones the process specification uses and are the labels you see on a proposal’s pull request.
| Stage | Purpose | What has to be true to enter it |
|---|---|---|
Pre-SIP |
gather community feedback and support, in a "Pre-SIP:" post on the Scala Contributors forum |
nothing; anyone can open one at any time |
Design |
make the case, make the design precise, compare alternatives; major changes are expected |
community support shown in the Pre-SIP thread, and a pull request on the |
Implementation |
an experimental implementation in the compiler, to learn how it holds up in practice; minor changes only |
a precise specification, and a committee vote to accept the proposal for implementation |
Completed |
the feature ships as stable in the next minor release; no more changes |
a complete, tested implementation already shipped as experimental, and a second committee vote to accept it |
The link between the two systems is the important part: a SIP reaches the Implementation stage with an experimental implementation, and the committee’s final vote takes place only after that implementation has shipped as experimental and has been tried. So "experimental" in the compiler is the implementation-stage label of the process, and a feature that is marked experimental has not been accepted yet. Scala 3.10 adds the middle label, preview, for features that the committee has accepted. Pending proposals are tracked as pull requests on GitHub; the completed list below is the official one, and the rejected and withdrawn proposals are kept on GitHub as well.
The completed SIPs and where they are covered
The official list (checked 2026-10-16) marks every completed SIP as Shipped, including the three previews of 3.10. The
table covers the Scala 3-era proposals; the older ones (SIP-11 string interpolation, SIP-13 implicit classes, SIP-14
futures and promises, SIP-15 value classes, SIP-17 Dynamic, SIP-18 modularizing language features, SIP-23 literal types,
SIP-25 trait parameters, SIP-27 trailing commas, SIP-30 @static, SIP-31 by-name implicit arguments, SIP-33 infix type
precedence, SIP-35 opaque types and the historical SID documents) describe features that were already part of Scala 2 or
of the first Scala 3 releases. The release in the second column comes from the release notes of each version.
| SIP | Title | Stable in | Covered on |
|---|---|---|---|
Binary integer literals |
3.5 |
||
Fewer braces (the |
3.3 LTS |
||
Scala CLI as the default |
3.5 |
||
Clause interleaving |
3.6 |
||
Binary APIs ( |
3.7 |
||
Explicit type variable syntax in quote patterns |
3.4 |
||
Multi-source extension overloads |
3.4 |
||
Proper specification for match types |
3.4 |
||
|
3.8 |
||
Named tuples |
3.7 |
||
For-comprehension improvements ("better fors") |
3.8 |
||
New syntax for context bounds and givens |
3.6 |
||
Strict-equality pattern matching |
3.10 (experimental in 3.9) |
||
Fully implicit conversions with |
3.9 (preview in 3.8) |
||
Unroll default arguments for binary compatibility |
preview in 3.10 |
||
Reference-able package objects |
preview in 3.10 |
||
Partial function literals with parentheses |
preview in 3.10 |
||
Single-line lambdas after |
preview in 3.10 (experimental in 3.9) |
Four SIPs in that list show the lifecycle at work. SIP-61, SIP-68, SIP-74 and SIP-75 are accepted and shipped as previews in 3.10, and will become standard in a later minor release; SIP-74 even states that it "will be activated by default in the next minor release". The sibling pages Incompatibilities & Dropped Features and Upgrading Between Scala 3 Versions cover the removals that travel with the same releases.
Source: List of all SIPs
Experimental, preview, stable
A feature that is not finished lives in one of three states. The reference documentation defines the two unfinished ones as follows. An experimental feature is not guaranteed binary or source compatibility; it may change or be dropped at any time. A preview feature has been accepted by the committee and is "guaranteed to be standardized in some next Scala minor release", but the compiler team may still make small, possibly binary incompatible, changes based on feedback. A stable feature is part of the language and follows the normal compatibility rules.
The transitions are not automatic or on a schedule. Experimental to Preview and Preview to Stable happen in minor
releases (3.9, 3.10, …), never in patch releases of an LTS line. A feature can also skip the middle state: the
3.10 notes list SIP-67, strict-equality pattern matching, as going from experimental in 3.9 straight to stable in 3.10,
while the three SIPs that were experimental in 3.9 and are still being finalized went to preview.
The two labels differ in more than a promise. The practical differences, all of which were checked with the examples below, are these:
| Question | Experimental | Preview |
|---|---|---|
How do I switch it on? |
a |
the |
Does the label spread to my code? |
yes: code that uses an experimental definition must itself be experimental, and |
no: a definition compiled with |
Can a library use it? |
only if the library accepts being unusable to non-experimental consumers |
discouraged for public libraries, because a later compiler may change the binary form, but not blocked |
Which releases? |
any, including patch releases of LTS |
minor releases only; 3.10 is the first release with a populated preview set |
The 3.9 LTS line has the -preview flag, which arrived in 3.8, but none of the features that became previews in 3.10 is
a preview there: they are still experimental on 3.9, as the sections below show.
@experimental and experimental scopes
scala.annotation.experimental marks a term or type definition as one without compatibility guarantees. The rule is that
an experimental definition may be referenced only from an experimental scope: another definition marked @experimental
(its right-hand side, its signature, its extends clause, its body, its annotations), a package that imports an
experimental language feature, or a project compiled with -experimental. This is why the mark is called viral: the user of
an experimental API is itself forced to carry the mark, and the compatibility warning therefore travels up the call chain
until it reaches code whose author has accepted it.
The first example defines an experimental route finder and uses it from a normal and from an experimental definition:
//> using scala 3.10.0
import scala.annotation.experimental
@experimental
def fastRoute(from: String, to: String): List[String] = List(from, to)
def plan(): List[String] = fastRoute("hut", "peak")
@experimental
def planExperimental(): List[String] = fastRoute("hut", "peak")
@main def run(): Unit =
println("ok")
-- Error: ExpDef.scala:7:27
7 |def plan(): List[String] = fastRoute("hut", "peak")
| ^^^^^^^^^
| method fastRoute is marked @experimental
|
| Experimental definition may only be used under experimental mode:
| 1. in a definition marked as @experimental, or
| 2. an experimental feature is imported at the package level, or
| 3. compiling with the -experimental compiler flag.
1 error found
Compilation failed
Only plan is rejected; planExperimental is fine. The message names the three ways out. Moving the mark up the chain
until the entry point compiles is the first of them; marking @main too makes the whole program run:
//> using scala 3.10.0
import scala.annotation.experimental
@experimental
def fastRoute(from: String, to: String): List[String] = List(from, to)
@experimental
def planExperimental(): List[String] = fastRoute("hut", "peak")
@experimental
@main def run(): Unit =
println(planExperimental())
List(hut, peak)
Overriding and compatibility
An experimental member may not override a non-experimental one, because that would make an experimental signature visible through a stable type and break binary compatibility by accident:
//> using scala 3.10.0
import scala.annotation.experimental
class Route:
def length: Any = 1
class FastRoute extends Route:
@experimental override def length: Int = 2
@main def run(): Unit = println(FastRoute().length)
-- [E164] Declaration Error: ExpOverride.scala:8:29
8 | @experimental override def length: Int = 2
| ^
| error overriding method length in class Route of type => Any;
| method length of type => Int may not override non-experimental member
-- Error: ExpOverride.scala:10:44
10 |@main def run(): Unit = println(FastRoute().length)
| ^^^^^^^^^^^^^^^^^^
| method length is marked @experimental
...
2 errors found
Compilation failed
Test code is the one place where the rule is relaxed deliberately: a test framework finds tests by reflection, so
tests can be marked @experimental without making the test class experimental to its callers.
The -experimental flag is contagious
-experimental allows every experimental feature in the whole project, and it does so by adding @experimental to every
top-level definition. The consequence for library authors was checked with two Scala CLI projects. A library with one
function was compiled with //> using option -experimental and packaged as a JAR; an application that does not use the
flag then calls it:
//> using scala 3.10.0
//> using jar ../lib.jar
import trailhead.distanceKm
@main def run(): Unit = println(distanceKm(3, 10))
-- Error: Main.scala:5:32
5 |@main def run(): Unit = println(distanceKm(3, 10))
| ^^^^^^^^^^
| method distanceKm is marked @experimental: Added by -experimental
|
| Experimental definition may only be used under experimental mode:
| 1. in a definition marked as @experimental, or
| 2. an experimental feature is imported at the package level, or
| 3. compiling with the -experimental compiler flag.
1 error found
Compilation failed
The function in the library does not use any experimental feature; the flag alone made it experimental. So a library
published with -experimental can be used only by projects that are also experimental. Applications can use the flag
freely; libraries should not.
Language imports and options
An experimental language feature is enabled in one of three ways, which differ only in scope. The import form takes effect for the rest of the file, the option form for the whole compilation, and the annotation form for one definition.
//> using scala 3.10.0
def pair[A, B](a: A, b: B): (A, B) = (a, b)
@main def run(): Unit =
println(pair[B = String, A = Int](1, "x"))
-- Error: NtaNo.scala:5:15
5 | println(pair[B = String, A = Int](1, "x"))
| ^^^^^^^^^^
|Named type arguments are experimental,
|they must be enabled with a `experimental.namedTypeArguments` language import or setting
-- Error: NtaNo.scala:5:27
5 | println(pair[B = String, A = Int](1, "x"))
| ^^^^^^^
|Named type arguments are experimental,
|they must be enabled with a `experimental.namedTypeArguments` language import or setting
2 errors found
Compilation failed
The message names the import. With the import the same program runs, and so does it with the option form
-language:experimental.namedTypeArguments, passed on the command line or as
//> using option -language:experimental.namedTypeArguments:
//> using scala 3.10.0
import scala.language.experimental.namedTypeArguments
def pair[A, B](a: A, b: B): (A, B) = (a, b)
@main def run(): Unit =
println(pair[B = String, A = Int](1, "x"))
(1,x)
Source: Scala 3 Reference: Named Type Arguments and Scala 3 Reference: Experimental language imports
Preview features use no import: -preview is the only switch. In sbt the option goes in scalacOptions, for example
scalacOptions += "-preview" or scalacOptions += "-language:experimental.namedTypeArguments"; see
sbt & Build Tools.
The imports that exist in 3.10.0
The object scala.language.experimental holds one entry per feature that has ever been experimental. The table lists the
26 nested members, plus the plain value macros, of the 3.10.0 standard library, read by reflection, and what the compiler does when each is imported. The
"graduated" members stay in the library so that old code keeps compiling; importing them now produces a deprecation
warning. On Scala 3.9.0 the same imports compile without warnings, which is a quick way to tell which line you are on.
| Status on 3.10.0 | Imports | What the compiler says when imported |
|---|---|---|
Standard (no import needed) |
|
a deprecation warning: "`experimental.betterFors` is now standard, no language import is needed" |
Preview (use |
|
a deprecation warning: "the |
Experimental |
|
accepted without a warning; using the feature in a non-experimental scope is an error |
Four of the experimental imports are new in 3.10 compared with 3.9.0, found by comparing the nested members of the two
standard libraries: dedentedStringLiterals, inlineTraits, magic and specializedTraits. The remaining 22 are present
on both lines. One of them, safe, switches on safe mode, which rejects code that calls methods tagged @rejectSafe. A
program that imports it and then calls println is rejected at its first call, which is exactly its purpose:
-- Error: ImpSafe.scala:3:22
3 |@main def m(): Unit = println(1)
| ^^^^^^^
|Cannot refer to method println in object Predef from safe code since it is tagged @rejectSafe
1 error found
Compilation failed
Experimental features on 3.10
The reference documentation lists the experimental features below. The five that are easy to run in isolation were run; the others are listed with their status and a link, and are labelled "not run here" because they are large enough to deserve their own project, or in the case of explicit nulls and capture checking, are covered on the page that owns them.
| Feature | Enabled with | Notes |
|---|---|---|
Match expressions with sub cases |
|
run below; nested |
Dedented string literals (SIP-72) |
|
run below; |
Numeric literals |
|
run below; literals for |
Flexible varargs (multiple spreads) |
|
run below |
Modularity improvements and better support for type classes |
|
run below with |
Named type arguments |
|
run above |
Checked exceptions ( |
|
not run here; see CanThrow capabilities |
Erased definitions |
|
not run here; see Erased definitions |
Capture checking and separation checking |
|
not run here; a large research feature with its own reference section |
Pure function syntax |
|
not run here; see the reference |
Explicit nulls |
|
covered in Option & Null Safety |
Inline traits and specialized traits |
|
not run here; see inline traits and specialized traits |
Quoted patterns with polymorphic functions |
|
not run here; related to Quoted Pattern Matching |
|
annotations and APIs marked |
not run here |
Sub cases
A case can be followed by a nested match introduced by if. The nested cases are tested only if the outer pattern
matches, they can use variables bound by it, and they need not be exhaustive: when none matches, matching continues with
the next outer case. That removes the usual duplicated default case.
//> using scala 3.10.0
import scala.language.experimental.subCases
enum Grade:
case Easy
case Hard(km: Int, ascent: Int)
case class Route(name: String, grade: Grade)
def label(r: Option[Route]): String = r match
case Some(x) if x.grade match
case Grade.Hard(km, up) if up > 800 => s"${x.name}: $km km, steep"
case Grade.Easy => s"${x.name}: easy"
case _ => "unsupported"
@main def run(): Unit =
println(label(Some(Route("Ridge", Grade.Hard(12, 900)))))
println(label(Some(Route("Lake", Grade.Easy))))
println(label(Some(Route("Pass", Grade.Hard(8, 300)))))
println(label(None))
Ridge: 12 km, steep
Lake: easy
unsupported
unsupported
The third call shows the fall-through: Pass matches Some(x), but no nested case accepts a hard grade with a 300 m
ascent, so the match continues with case _. Without the import the compiler does not know the syntax and reports
errors such as "'=>' expected, but 'case' found" at the second outer case. A similar form is described on
Pattern Matching.
Dedented string literals
SIP-72 adds a ''' string whose leading indentation is removed by the language instead of by stripMargin. The opening
delimiter must be followed by a newline, and the closing one must be on its own line; the newline after the opener, the
final newline and the indentation up to the closing delimiter are removed.
//> using scala 3.10.0
import scala.language.experimental.dedentedStringLiterals
def banner(name: String): String =
'''
Trail: stones
Name: Ridge
'''
@main def run(): Unit =
println(banner("x"))
println(banner("x").length)
Trail: stones
Name: Ridge
26
The 26 characters are the two lines and the newline between them, with no indentation and no trailing newline. Without the
import the same file does not compile: the ''' is read as a character literal and the compiler reports "empty character
literal" followed by "unclosed character literal". Triple-quoted strings (""") with stripMargin remain the portable choice;
see Strings & Interpolation.
Numeric literals and multiple spreads
With genericNumberLiterals, a literal that has a fully defined expected type with a scala.util.FromDigits given is
converted through it, with no size limit. Without the import the first line below is rejected with "number too large".
//> using scala 3.10.0
import scala.language.experimental.genericNumberLiterals
@main def run(): Unit =
val big: BigInt = 123_456_789_012_345_678_901
val dec: BigDecimal = 110_222_799_799.99
println(big + 1)
println(dec)
123456789012345678902
110222799799.99
multiSpreads lifts the rule that a spread argument xs* must come last. Without the import the compiler reports "spread
operator * not allowed here; must come last in a parameter list":
//> using scala 3.10.0
import scala.language.experimental.multiSpreads
def total(xs: Int*): Int = xs.sum
@main def run(): Unit =
val a = Seq(1, 2, 3)
val b = Seq(10, 20)
println(total(a*, 100, b*))
136
modularity includes the tracked modifier, which keeps a class parameter’s precise type visible to callers. Without the
import tracked val name: String is a syntax error ("':' expected, but 'val' found"); with it the program below runs:
//> using scala 3.10.0
import scala.language.experimental.modularity
class Trail(tracked val name: String)
@main def run(): Unit =
val t = Trail("Ridge")
println(t.name)
Ridge
The same sub-cases program also runs with the option form, scala run Sub.scala --scalac-option -language:experimental.subCases,
which prints the same four lines. On Scala 3.9.0 the imports multiSpreads and subCases exist and compile as well;
dedentedStringLiterals, inlineTraits and specializedTraits do not exist there.
The three preview features of 3.10
The 3.10 release notes list three SIPs in preview behind -preview: @unroll (SIP-61), relaxed lambda syntax (SIP-75) and
reference-able package objects (SIP-68). The reference page for relaxed lambda syntax also contains SIP-74, partial
function literals with parentheses, so that the preview set has in effect four syntactic or binary-level changes. The SIP
page for SIP-74 says that it "will be activated by default in the next minor release". The previews are enabled by one
flag; the build file or the first line of a script is where it belongs:
//> using scala 3.10.0
//> using option -preview
Relaxed lambda syntax (SIP-75) and parenthesized case (SIP-74)
SIP-75 allows a lambda to follow a : on the same line. The lambda extends to the end of the line. SIP-74 allows a single
case clause, a one-case partial function, inside parentheses. Both are accepted together with curried multi-line lambdas
(fun: (x: Int) => y => followed by an indented body). First what happens without the flag:
//> using scala 3.10.0
@main def run(): Unit =
val elevations = List(120, 340, 95)
val tripled = elevations.map: m => m * 3
println(tripled)
val firsts = List((1, 2), (3, 4)).collect(case (a, b) if b > 2 => a)
println(firsts)
-- Error: Lam.scala:4:16
4 | val tripled = elevations.map: m => m * 3
| ^^^^^^^^^^^^^^^^^
| not a legal formal parameter for a function literal
-- [E018] Syntax Error: Lam.scala:6:44
6 | val firsts = List((1, 2), (3, 4)).collect(case (a, b) if b > 2 => a)
| ^^^^
| expression expected but case found
|
| longer explanation available when compiling with `-explain`
-- [E006] Not Found Error: Lam.scala:4:37
4 | val tripled = elevations.map: m => m * 3
| ^
| Not found: m
...
3 errors found
Compilation failed
With //> using option -preview all of the preview forms work:
//> using scala 3.10.0
//> using option -preview
@main def run(): Unit =
val elevations = List(120, 340, 95)
val tripled = elevations.map: m => m * 3
val summits = List((1, 2), (3, 4)).collect: case (a, b) if b > 2 => a
val joined = Seq((1, 2), (3, 4)).map: (a, b) => a + b
def fun(f: Int => Int => Int): Int = f(1)(2)
val curried = fun: (x: Int) => y =>
x + y
println(tripled)
println(summits)
println(joined)
println(curried)
List(360, 1020, 285)
List(3)
List(3, 7)
3
One limit is easy to hit. The single-line form does not apply when the code is immediately enclosed in parentheses,
because there x: Int => Int has always meant type ascription. Putting the lambda directly inside println(...) therefore
fails even with the flag on, with a misleading message:
-- [E006] Not Found Error: Paren.scala:5:26
5 | println(elevations.map: m => m * 3)
| ^
| Not found: type m
|
| longer explanation available when compiling with `-explain`
...
3 errors found
Compilation failed
The fix is the usual one: bind the call to a val first, as in the example above, or pass an ordinary function literal.
A one-case partial function inside the parentheses of the call, as in collect(case ...), is allowed, which is what SIP-74
adds. On Scala 3.9.0 the same file fails in the same way with or without -preview, because on that line the feature is
still the experimental relaxedLambdaSyntax import: with import scala.language.experimental.relaxedLambdaSyntax the
map: m => m * 3 and the parenthesized collect(case (a, b) if b > 2 => a) forms compiled and ran on 3.9.0 as well, printing
List(360, 1020, 285) and List(3).
Reference-able package objects (SIP-68)
A package object could not be used as a value: network below is a package, and writing it as an expression failed with
"package is not a value". Under -preview, a.b where b is a package that contains a package object expands to
a.b.package.
//> using scala 3.10.0
package trailhead
package object network:
val trailCount = 12
@main def run(): Unit =
val net = network
println(net.trailCount)
-- Error: Pkgobj.scala:8:12
8 | val net = network
| ^^^^^^^
|Preview package object values may only be used when compiling with the `-preview` compiler flag
1 error found
Compilation failed
With //> using option -preview the program prints 12. The expansion applies only when the package is used on its own,
not as part of a longer selection such as network.trailCount, which always worked. On Scala 3.9.0 the program ran with
import scala.language.experimental.packageObjectValues instead. The fact that package objects need a value form at all is
a sign of the deprecation covered in
Packages, Imports & Top-Level Definitions.
@unroll (SIP-61)
@unroll generates forwarders so that adding a defaulted parameter to a method does not break callers compiled against the
older signature. The mechanism and the MiMa results are on
Versions, Compatibility & JDKs, and the Java view
(overloads for each prefix) on Calling Scala from Java. Here
is what the lifecycle looks like. This program was run on 3.10.0:
//> using scala 3.10.0
import scala.annotation.unroll
object Trail:
def describe(name: String, km: Double = 1.0, @unroll difficulty: Int = 1, @unroll lit: Boolean = false): String =
s"$name $km km, difficulty $difficulty, lit=$lit"
@main def run(): Unit =
println(Trail.describe("Ridge"))
val methods = Trail.getClass.getDeclaredMethods.filter(_.getName == "describe")
methods.map(_.getParameterCount).sorted.foreach(n => println(s"describe with $n parameters"))
Ridge 1.0 km, difficulty 1, lit=false
describe with 2 parameters
describe with 3 parameters
describe with 4 parameters
The reference says the feature is "available under -preview since Scala 3.10.0", yet on 3.10.0 as run here the program
compiled and generated the two forwarders, with three methods in total, without the flag, and the output was the same with
//> using option -preview. Pass -preview anyway so that the file documents its dependency and keeps building if the gate
is enforced later. On 3.9.0 the same file stops:
-- Error: Unroll39.scala:5:47
5 | def describe(name: String, km: Double = 1.0, @unroll difficulty: Int = 1, @unroll lit: Boolean = false): String =
| ^^^^^^^
| class unroll is marked @experimental: under review as part of SIP-61
|
| Experimental definition may only be used under experimental mode:
| 1. in a definition marked as @experimental, or
...
2 errors found
Compilation failed
That is the whole lifecycle in one definition: scala.annotation.unroll is annotated @experimental in 3.9 and preview in
3.10.
Preview is not contagious
The reference states that a definition compiled in preview mode is not itself marked preview. This was checked the same way
as the -experimental case: a library function written with the single-line lambda form was compiled with
//> using option -preview and packaged; an application without the flag called it:
//> using scala 3.10.0
//> using jar ../plib.jar
import trailhead.triple
@main def run(): Unit = println(triple(List(1, 2, 3)))
List(3, 6, 9)
The consumer compiled and ran. The standard library’s own preview APIs are marked with an internal annotation,
@preview, whose rules mirror @experimental: they can be referenced only from a unit compiled with -preview. It is
reserved for the standard library, so application code does not use it.
Status table: 3.9 LTS and 3.10
The same features, side by side. The 3.9 column was checked by compiling on 3.9.0 where a note says so.
| Feature | Scala 3.9 LTS | Scala 3.10 |
|---|---|---|
Relaxed lambda syntax (SIP-75) and parenthesized |
experimental import |
preview, |
Reference-able package objects (SIP-68) |
experimental import |
preview, |
|
|
preview; compiled without the flag here |
Strict-equality pattern matching (SIP-67) |
experimental import |
stable; the import is deprecated |
|
stable |
stable; the import is deprecated |
Named tuples, better fors, fewer braces, clause interleaving |
stable |
stable; the imports are deprecated |
Sub cases, numeric literals, flexible varargs ( |
experimental |
experimental |
Dedented string literals, inline traits, specialized traits |
not available |
experimental |
Which state may I use?
The risk of a feature is the risk of rewriting code and of binary incompatibility, and the two audiences carry it differently.
-
Applications and internal tools can use previews freely and pin the compiler version; the stated promise is that the feature will become standard, and small changes are fixed by the compiler or by a rewrite. They can use experimental features if they accept that a minor upgrade may break the build. Avoid
-experimentalas a blanket flag: it is easier to opt in per file with the import or the annotation, and the errors stay visible. -
Libraries should not use
-experimentalat all, since every definition becomes unusable to ordinary consumers, and should avoid preview features in public signatures, because a later compiler may change the binary form. The 3.9 LTS line is the line the Scala team advises for publishing libraries, and 3.10 libraries cannot be read by a project that stays on 3.9; the rules are in Versions, Compatibility & JDKs. -
Every project should treat the 3.10 "graduated" warnings as a to-do: remove imports that now only produce "is now standard" warnings, and replace the two that point to
-previewwith the flag. Doing so keeps the code compiling on the next minor release, which may remove the old imports. -
Compatibility guarantees do not cover either label. Binary compatibility between 3.x releases is promised for stable code only; experimental APIs are explicitly excluded.
Scala 2, Java and Kotlin
Scala 2 had a similar idea in the other direction. Features sat behind import scala.language.<feature>
(implicitConversions, postfixOps, higherKinds) and compiler flags such as -Xfuture or -Ymacro-annotations.
Those imports are about features that are legal but discouraged and that the compiler wants you to opt in to knowingly;
the Scala 3 experimental imports are about features that are not finished. Both still exist side by side: scala.language.implicitConversions
and scala.language.experimental.subCases are imported the same way. Scala 2 macros were an experimental Scala 2 feature,
which is why they could not survive into Scala 3 (see
Migrating from Scala 2).
Java handles unfinished features with preview features defined in JEP 12. A feature that is fully specified but not
yet permanent is compiled and run only with --enable-preview and --release N, and class files compiled that way record the
requirement and run only on that exact JDK version. That is closer to Scala’s preview in intent, and stricter in binary
terms: the Scala compiler does not require that downstream code run on the same compiler version. Java’s incubator modules
correspond to experimental APIs. See JEP 12: Preview features.
Kotlin uses opt-in requirements: a library marks an API with an annotation based on @RequiresOptIn (the standard library
has @ExperimentalStdlibApi), and callers must either propagate the opt-in annotation up their own declarations, which
resembles Scala’s @experimental virality, or opt in locally with @OptIn(...) or the -opt-in compiler argument, which
resembles Scala’s import or flag. The difference is that Kotlin lets a call site accept the risk without spreading the
marker, whereas Scala’s @experimental always spreads unless the whole project is already experimental. See
Kotlin: Opt-in requirements.
Where the books are outdated
-
Material written for Scala 3.0 to 3.7 shows
import scala.language.experimental.namedTuples,fewerBraces,clauseInterleaving,intoorbetterForsas necessary. On 3.10 they are accepted but deprecated, and unneeded. -
Older descriptions of
@unroll, relaxed lambda syntax, package object values and strict-equality pattern matching call them experimental; on 3.10 the first three are preview, enabled with-previewand not with a language import, and the fourth is stable. -
The Scala 3 reference experimental index lists
fewerBracesas an experimental language feature, although that feature became stable in 3.3 LTS (SIP-44). The reference is a hand-edited list: use the compiler’s own deprecation warning as the authoritative answer. -
The official completed SIP list marks the previews of 3.10 (SIP-61, SIP-68, SIP-74, SIP-75) as "Shipped", although they still need
-preview; the list says shipped, the compiler flag tells you whether it is stable.