Skip to Content
Course content

3: Scala 2 vs Scala 3 Differences

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've been skimming through old Scala forums or reading legacy documentation, you've probably run into the idea that Scala 3 is basically "Scala 2 with optional curly braces." It's a common misconception—the idea that the update was primarily aesthetic, designed to make the code look more like Python or Haskell without changing the underlying machinery.

I can tell you from experience: that's a dangerous way to approach the language. If you treat Scala 3 as just a syntax sugar update, you'll spend your time fighting the compiler because you're trying to write Scala 2 code without braces, rather than leveraging the actual architectural shifts. The real changes aren't about where the } goes; they're about how the type system handles data and how the compiler manages dependencies.

It's more than just missing curly braces

Yes, the indentation-based syntax is the first thing you'll notice. It's great, and I personally love not having to hunt for a missing closing brace in a 50-line method. But the shift is deeper. In Scala 2, we spent a lot of time creating "boilerplate" to simulate features that Scala 3 now treats as first-class citizens. For example, creating a proper Algebraic Data Type (ADT) in Scala 2 required a sealed trait and a handful of case objects or case classes. It worked, but it was verbose.

In Scala 3, we have enum. It's not just a Java-style enum; it's a powerful tool for defining data shapes that the compiler can actually reason about more efficiently.

// The Scala 2 way
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Square(side: Double) extends Shape

// The Scala 3 way
enum Shape:
  case Circle(radius: Double)
  case Square(side: Double)


Replacing the chaos of implicit with given and using

This is where most developers hit a wall. In Scala 2, the implicit keyword was the "Swiss Army Knife" of the language. It did everything: it defined implicit parameters, implicit returns, and implicit conversions. It was powerful, but it was a nightmare for readability. When you saw implicit, you had to stop and ask, "Wait, is this providing a value, or is it changing the type of this object?"

Scala 3 splits this into given and using. I find this much more honest. A given defines a value that can be used implicitly, and using explicitly marks a parameter that the compiler should look for a given to fill.

// Scala 2
implicit def jsonEncoder(d: User): String = s"{\"name\": \"${d.name}\"}"
def printUser(u: User)(implicit enc: String) = println(enc)

// Scala 3
given jsonEncoder: User => String = u => s"{\"name\": \"${u.name}\"}"
def printUser(u: User)(using enc: String) = println(enc)


The logic is the same, but the intent is clear. You aren't guessing anymore.

Actually using Union Types to handle multiple shapes

For years, if we wanted a method to accept either a String or an Int, we had to wrap them in a custom trait or use Any (which is basically giving up on type safety). Scala 3 introduces Union Types via the | operator. This is a game-changer for API design.

Imagine you're building a system that accepts a user ID as either a numeric database ID or a UUID string. In Scala 2, you'd be writing a wrapper class. In Scala 3, you just do this:

def findUser(id: Int | String): User = 
  if id.isInstanceOf[Int]) {
    // logic for Int
  } else {
    // logic for String
  }


It's clean, it's direct, and it removes a huge amount of the "wrapper" noise that used to clutter my projects.




📋 Practical Task

Refactoring the Legacy Payment Gateway Validator

You've inherited a piece of legacy Scala 2 code used to validate payment methods. It uses the old sealed trait pattern and a confusing implicit setup. Your task is to refactor this into idiomatic Scala 3.

The Legacy Code:

sealed trait PaymentMethod
case class CreditCard(number: String) extends PaymentMethod
case class PayPal(email: String) extends PaymentMethod

implicit def paymentFormatter(p: PaymentMethod): String = p match {
  case CreditCard(n) => s"Card ending in ${n.takeRight(4)}"
  case PayPal(e) => s"PayPal account ${e}"
}

def logPayment(p: PaymentMethod)(implicit fmt: String) = println(fmt)


Requirements for your refactor:

  • Convert the PaymentMethod hierarchy into a Scala 3 enum.
  • Replace the implicit def with a given instance.
  • Update the logPayment method to use the using keyword for its implicit parameter.
  • Remove all unnecessary curly braces to follow Scala 3's indentation-based style.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.