पाठ 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)                        // true

Opaque 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.

त्वरित जाँच: 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.