Scala - Dropped Features



Scala 3 introduced various changes and removed some features from Scala 2. These are various changes done in Scala 3 from Scala 2 as given below.

1. DelayedInit

DelayedInit is no longer supported. This trait was used to delay the initialization of classes and objects. So, you can run some code before the class and object initialization code. You should avoid using DelayedInit.

Scala 2:

 
trait Helper extends DelayedInit {
  def delayedInit(body: => Unit): Unit = {
    println("dummy text, printed before initialization of C")
    body // evaluates the initialization code of C
  }
}

class C extends Helper {
  println("this is the initialization code of C")
}

object Test extends App {
  val c = new C
}

Here, Helper prints a message before running the initialization code of C using the delayedInit method. But you can initialize directly in the class in Scala 3 as given below.

Scala 3:

  
class C {
  println("this is the initialization code of C")
}

object Test extends App {
  val c = new C
}

2. Scala 2 Macros

Scala 2 macro system has been replaced with a cleaner system based on inline and quote-splice mechanisms ('{...} and ${...}). So you can have better integration and safety.

Scala 2:

  
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context

object Macros {
  def hello: Unit = macro helloImpl
  def helloImpl(c: Context): c.Expr[Unit] = {
    import c.universe._
    reify {
      println("Hello, world!")
    }
  }
}

object Test extends App {
  Macros.hello
}

Macros are used to generate code at compile time in Scala 2. The hello macro prints "Hello, world!" using a macro definition. The macro system is replaced with inline and quote-splice mechanisms in Scala 3 as given below.

Scala 3:

  
inline def hello: Unit = println("Hello, world!")

object Test extends App {
  hello
}

3. Existential Types

Existential types using forSome have been removed. Types that can be used with wildcards are still supported but are treated as refined types.

Scala 2:

  
def process(m: Map[_, _]): Unit = {
  println(m)
}

val m = Map("key" -> 42)
process(m)

In Scala 2, existential types are used without knowing exactly what it is. Here, Map[_, _] is a map with any key and value types. Now, you can use wildcards in Scala, like Map[? <: Any, ? <: Any] for above same result -

Scala 3:

  
def process(m: Map[? <: Any, ? <: Any]): Unit = {
  println(m)
}

val m = Map("key" -> 42)
process(m)

4. General Type Projection

General type projections like T#A where T is an abstract type is no longer supported. So you can encourage the use of path-dependent types and implicit parameters instead.

Scala 2:

  
trait T {
  type A
}

def foo(a: T#A): Unit = {}

You can define a type A inside a trait T and refer to it using T#A in Scala 2. This is known as  type projection and used for type safety.

Scala 3:

  
trait T {
  type A
  def getA: A
}

def foo(t: T)(a: t.A): Unit = {}

You should use path-dependent types in Scala 3. Here, foo takes an instance of T and uses its type A. So it is safer and avoids the issues with type projections.

5. Do-While

The do-while loop syntax has been removed. You can use equivalent while loop instead for cleaner and more consistent syntax.

Scala 2:

  
var i = 0
def f(i: Int): Int = if (i < 5) 0 else 1

do {
  i += 1
} while (f(i) == 0)

println(i) // Output: 5

In Scala 2, do-while loops were used to run a block of code at least once before checking the condition.

Scala 3:

  
var i = 0
def f(i: Int): Int = if (i < 5) 0 else 1

while {
  i += 1
  f(i) == 0
} do ()

println(i) // Output: 5

In Scala 3, you should use a while loop with a block to achieve the same effect. 

6. Procedure Syntax

Procedure syntax (def f() { ... }) has been dropped. Now you should use def f() = { ... } or def f(): Unit = { ... }.

Scala 2:

  
def f() {
  println("Hello")
}

f() // Output: Hello

Procedure syntax is used to define methods without specifying a return type in Scala 2. But, you need to specify the return type explicitly in Scala 3.

Scala 3:

  
def f() = {
  println("Hello")
}

f() // Output: Hello

7. Package Objects

Package objects are deprecated and will be removed. You can now write all kinds of definitions at the top level.

Scala 2:

  
package object p {
  val a = 42
  def b = "Hello"
}

object Test extends App {
  println(p.a) // Output: 42
  println(p.b) // Output: Hello
}

Scala 3:

  
package p

val a = 42
def b = "Hello"

object Test extends App {
  println(a) // Output: 42
  println(b) // Output: Hello
}

8. Early Initializers

Early initializers (class C extends { ... } with SuperClass ...) have been removed. These were not used and are not necessary due to the support of trait parameters in Scala 3.

Scala 2:

  
class C extends {
  val x = 2
} with SuperClass

You were initializing parts of a class before calling the superclass constructor in Scala. But, you can use trait parameters to achieve the same effect in Scala 3. It avoids the need for early initializers in Scala 3.

Scala 3:

  
trait Base(val x: Int)

class C extends Base(2)

object Test extends App {
  val c = new C
  println(c.x) // Output: 2
}

9. Class Shadowing

Class shadowing is no longer supported to avoid confusion. Inner classes in subclasses can have the same name as in superclasses. This is known as class shadowing.

Scala 2:

  
class Base {
  class Ops
}

class Sub extends Base {
  class Ops
}

You can have inner classes in subclasses with the same name as in superclasses in Scala 2. But it is not supported in Scala 3 to avoid confusion.

Scala 3:

  
class Base {
  class Ops
}

class Sub extends Base {
  class NewOps
}

10. Limit 22

The limit of 22 parameters for functions and tuples is removed. Functions and tuples can now have any number of parameters and fields.

Scala 2:

  
val tuple = (1, 2, 3, ..., 22)

Tuples and functions were limited only to 22 parameters.

Scala 3:

  
val tuple = (1, 2, 3, ..., 50)

But, there is no limit on the number of parameters for tuples and functions in Scala 3.

11. XML Literals

XML literals are deprecated and will be replaced with XML string interpolation. This change improves the consistency and safety of XML handling in Scala.

Scala 2:

  
 val xml = <message><from>John</from>
    <to>Doe</to></message>
println(xml)

XML literals were used directly in the code in Scala 2.

Scala 3:

  
import dotty.xml.interpolator.*

val xml = xml"<message><from>John</from>
<to>Doe</to></message>"
println(xml)

But, XML string interpolation is used instead of XML literals in Scala 3.

12. Symbol Literals

Symbol literals ('xyz) are no longer supported. You can use plain string literals ("xyz") instead. The Symbol class will be deprecated in the future.

Scala 2:

  
val sym = 'symbol
println(sym) // Output: 'symbol

Symbol literals were used to create symbols in Scala 2.

Scala 3:

  
val sym = "symbol"
println(sym) // Output: symbol

You need to use plain string literals instead of symbol literals in Scala 3.

13. Auto-Application

Implicit insertion of () for nullary methods is no longer supported. Now, it must match the parameter syntax exactly in Scala 3.

Scala 2:

  
def next(): Int = 42
val n = next
println(n) // Output: 42

You can call a method without () even if it was defined with () in Scala 2.

Scala 3:

  
def next(): Int = 42
val n = next()
println(n) // Output: 42

You need to match the method definition exactly in Scala 3. If the method is defined with (), you must call it with ().

14. Weak Conformance

The concept of weak conformance is used to widen numeric types in certain expressions. Now it has been removed.

Scala 2:

  
val list = List(1.0, math.sqrt(3.0), 0, -3.3) // List[Double]
println(list)

Weak conformance was mixing different numeric types in a list. So it resulted in a list of the widest type. But, Scala 3 drops the general notion of weak conformance, and instead keeps one rule: Int literals are adapted to other numeric types if necessary.

15. Nonlocal Returns

Nonlocal returns from nested anonymous functions are deprecated due to hidden performance costs and unintended behavior. You can use scala.util.boundary and boundary.break instead in Scala 3.

Scala 2:

  
def foo(xs: List[Int]): Int = {
  xs.foreach { x =>
    if (x > 10) return x
  }
  0
}

val result = foo(List(1, 2, 3, 11, 5))
println(result) // Output: 11

You can return from a nested anonymous function in Scala 2. But this had hidden costs and issues.

Scala 3:

  
import scala.util.boundary, boundary.break

def foo(xs: List[Int]): Int = boundary {
  xs.foreach { x =>
    if (x > 10) break(x)
  }
  0
}

val result = foo(List(1, 2, 3, 11, 5))
println(result) // Output: 11

You can use boundary and break for nonlocal returns in Scala 3. This approach is clearer and avoids hidden costs.

16. private[this] and protected[this]

The private[this] and protected[this] access modifiers are deprecated. The compiler now infers access only via this for private members. So these modifiers are redundant in Scala 3.

Scala 2:

  
class C {
  private[this] val x = 42
  def getX: Int = x
}

val c = new C
println(c.getX) // Output: 42

private[this] restricted access to within the instance itself in Scala 2.

Scala 3:

  
class C {
  private val x = 42 // treated as private[this]
  def getX: Int = x
}

val c = new C
println(c.getX) // Output: 42

The compiler infers private[this] automatically for private members accessed only via this in Scala 3. This simplifies the code.

17. Wildcard Initializer

The syntax var x: A = _ for uninitialized fields has been dropped. Instead, you can use import scala.compiletime.uninitialized and var x: A = uninitialized in Scala 3.

Scala 2:

  
var x: Int = _
println(x) // Output: 0 (default initialization)

The _ was used to indicate uninitialized fields, which defaulted to a value in Scala 2.

Scala 3:

  
import scala.compiletime.uninitialized

var x: Int = uninitialized
println(x) // Output: <uninitialized>

You can use uninitialized for uninitialized fields in Scala 3.

These are various changes that have been made in Scala 3. These changes improve type safety and provide code clarity and consistency in Scala 3.

Advertisements