Lesson 15 / 25
Extension Methods, Opaque Types and Union Types
Use Scala 3 features for safer, clearer APIs.
Modern Scala 3 type features
Extension methods add methods to existing types without wrappers: extension (s: String) def isPinCode: Boolean = ..., after which "411001".isPinCode works wherever the extension is in scope. Opaque type aliases create zero-cost distinct types: opaque type Paise = Long is a plain Long at run time, but outside its defining scope the compiler treats it as a separate type, so you cannot accidentally pass a Paise where a CustomerId is expected. Provide constructors and operations in the companion object. Union types A | B express that a value is one of several types without a common hierarchy (def parse(s: String): Int | ParseError), and intersection types A & B require both. Literal types (val mode: "test" | "live") restrict values precisely. Match types compute types from other types, and inline and the new macro system enable compile-time metaprogramming in libraries. Also useful: export clauses to forward members, and top-level definitions, so you no longer need package objects.
Opaque types and extension methods
Distinct money and ID types at zero runtime cost.
object Domain:
opaque type Paise = Long
object Paise:
def apply(value: Long): Paise =
require(value >= 0, "money cannot be negative")
value
def fromRupees(rupees: BigDecimal): Paise = (rupees * 100).toLongExact
extension (p: Paise)
def +(other: Paise): Paise = p + other // inside Domain, Paise is a Long
def rupees: BigDecimal = BigDecimal(p) / 100
opaque type CustomerId = String
object CustomerId:
def apply(raw: String): CustomerId = raw
import Domain.*
def credit(customer: CustomerId, amount: Paise): String = s"credited ${amount.rupees} to $customer"
val total = Paise(4950) + Paise.fromRupees(BigDecimal("199.00"))
println(credit(CustomerId("c-42"), total)) // credited 248.5 to c-42
// credit(total, CustomerId("c-42")) // compile error: arguments swapped
// val wrong: Paise = 100L // compile error outside Domain
extension (s: String)
def isPinCode: Boolean = s.length == 6 && s.forall(_.isDigit)
def parseQty(s: String): Int | String = // union type: a number or an error message
s.toIntOption.getOrElse(s"not a number: $s")
println("411001".isPinCode) // trueOpaque types prevent mix-ups
Bugs like passing an order ID where a customer ID is expected, or rupees where paise are expected, become compile errors with opaque types, and they cost nothing at run time because they erase to the underlying type.
Quick check: What is the runtime cost of an opaque type alias such as opaque type Paise = Long?
- A wrapper object per value
- A reflection lookup
- None: at run time it is the underlying Long
- A boxed Java object always
Answer
None: at run time it is the underlying Long — Opaque types are distinct only at compile time and erase to the underlying type.