Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Effective Match Types - Scala Days 2025

Effective Match Types - Scala Days 2025

Preventing runtime errors is a hot topic, whether it's the Whitehouse telling you to rewrite code in Rust, or that Jane Street telling you to make illegal states unrepresentable. But Static Types surely get in the way and prevent you writing expressive code surely? Well I propose that match types give you the expressive power of a dynamic language (e..g Ruby or Elixir) but with the safety of static types. This talk contains everything you could need to know about the Scala 3 match types feature.

Avatar for Jamie Thompson

Jamie Thompson

August 21, 2025
Tweet

More Decks by Jamie Thompson

Other Decks in Technology

Transcript

  1. About Me 📖 2 • Engineer @ Mibex Software Recent

    Scala OSS Contributions • ScalaSQL SimpleTable • Mill Scala 3 upgrade • Scala 3 build pipelining • New Scala command bishabosha.github.io @bishabosha (GitHub, Twitter/X)
  2. 5 MINSKY, Y. and WEEKS, S. (2008) ‘Caml trading –

    experiences with functional programming on Wall Street’, Journal of Functional Programming, 18(4), pp. 553–564. doi:10.1017/S095679680800676X. https://bidenwhitehouse.archives.gov/oncd/briefing-room/2024/02/26/press-release-technical-report/ https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ 2024 One of our programming maxims is “make illegal states unrepresentable” 2008 2019 expressive / safe - pick two “Rewrite it in Rust!!!11!!”
  3. 6 Enforce invariants at compile-time Avoid defensive programming => Zero-cost

    safety expressive / safe - pick two Static types = Safety Safe refactoring APIs have stronger contracts
  4. expressive / safe - pick two 9 Static Types Dynamic

    Types “Scala feels dynamic” Match types give the expressive power of a dynamic language, but with the safety of static types
  5. bishabosha/scaladays-2025 10 get '/hello/:name' do "Hello #{params['name']}!" end Vibe-based lookup

    expressive / safe - pick two GET /hello/Jamie GET /hello/World http get "/hello/:name" in: s"Hello ${params.name}!" Safe + Guaranteed 🔐 Match Types 🤓
  6. GET /hello/Jamie GET /hello/World get '/hello/:name' do "Hello #{params['name']}!" end

    11 Vibe-based lookup expressive / safe - pick two http get "/hello/:name" in: s"Hello ${params.name}!" Safe + Guaranteed 🔐 Match Types 🤓 Demo!!! bishabosha/scaladays-2025
  7. http.get("/hello/:name").in: (ctx: (name: String)) ?=> s"Hello ${params(using ctx).name}!" 13 expressive

    / safe - pick two Dependent Typing structural type Via match types bishabosha/scaladays-2025
  8. 14 expressive / safe - pick two value def get(pattern:

    String) type "/hello/:name" type (name: String) value Map("name" -> "Jamie") Singleton type (pattern.type) Type-level programming cast extract from request GET /hello/Jamie Apply a match-type bishabosha/scaladays-2025
  9. expressive / safe - pick two import scala.language.experimental.modularity object http:

    infix def get(pattern: String): GetBuilder(pattern) = ... class GetBuilder(tracked val pattern: String): type Fields = ParamsOf[pattern.type] infix def in[R](body: Params[Fields] ?=> R)... = ... object ParamParsers: type ParamsOf[P <: String] = ... match type ParamsOf["/hello/:name"] =:= (name: String) 15 bishabosha/scaladays-2025
  10. What are match types? 17 T match { case P[x]

    => x } scrutinee Pattern case (1..n)
  11. What are match types? 18 T match { case P[x]

    => x } scrutinee Pattern case (1..n) Demo!!! https://bishabosha.github.io/
  12. 19 Performant built-ins import scala.compiletime.ops.any.* import scala.compiletime.ops.string.* import scala.compiletime.ops.int.* import

    scala.compiletime.ops.boolean.* (23 match {case S[n] => n}) =:= 22 (64 == 128) =:= false ("sca" + "la") =:= "scala" Substring["scala", 3, 5] =:= "la" CharAt["scala", 1] =:= 'c' Length["scala"] =:= 5 • Numerics • Bit Manipulation • Boolean logic • String parsing • Class type ==> Tuple type case class Person(name: String, age: Int) NamedTuple.From[Person] =:= (name: String, age: Int) intrinsics
  13. Tuple.Filter[(10, 3, 7), [X <: Int] =>> X > 5]

    =:= (10, 7) Tuple.Map[(1, 2, 3), [X <: Int] =>> X * 2] =:= (2, 4, 6) Tuple.Zip[(1, 2, 3), ('a', 'b', 'c')] =:= ((1, 'a'), (2, 'b'), (3, 'c')) 20 Tuple Operations • Map • FlatMap • Filter • Fold • Concat • Zip • Contains • Reverse • Union • Last • Head • Drop • Take Library implemented Tuple as a list of types
  14. import scala.compiletime.{constValue, constValueTuple} type Dbl = Tuple.Map[(1, 2, 3), [X

    <: Int] =>> X * 2] assert(constValueTuple[Dbl] == (2, 4, 6)) assert(constValue["sca" + "la"] == "scala") 21 Materialize results type ("name", "age") value ("name", "age") constValueTuple
  15. 23 sealed trait HList case object HNil extends HList case

    class ::[H, T <: HList](head: H, tail: T) extends HList HList type LongHList = Int :: Int :: Int :: Int :: Int :: Int :: String :: HNil.type Problem: Check that the last element type is String Measuring the Cost bishabosha/scaladays-2025
  16. 24 trait Last[L <: HList] { type Out } type

    IsLast[X, L <: HList] = Last[L] { type Out = X } implicit def base[H]: IsLast[H, H :: HNil.type] = new Last[H :: HNil.type] { type Out = H } implicit def inductive[H, T <: HList](implicit rest: Last[T]): IsLast[rest.Out, H :: T] = new Last[H :: T] { type Out = rest.Out } def isLast[X, L <: HList](implicit last: IsLast[X, L]): Unit = () implicits type Last[L <: HList] <: Any = L match case h :: HNil.type => h case _ :: t => Last[t] def isLast[X, L <: HList](implicit last: Last[L] =:= X): Unit = () match types Measuring the Cost bishabosha/scaladays-2025
  17. 28 //> using dep in.nvilla::regsafe:0.0.1 //> using scala 3.2.0-RC1 //>

    using jvm temurin:17 import regsafe.* val rational = Regex("""(\d+)(?:\.(\d+))?""") rational.unapply("3.1415").get ==> ("3", Some("1415")) rational.unapply("23").get ==> ("23", None) Typesafe regex parser null aware `rational.unapply` result type Option[(String, Option[String])] Use Cases
  18. Laminar w. Scala.js 29 import com.raquo.laminar.api.L.* case class Form(zip: String,

    city: String) val formVar = VarLenses(Form("", "")) def zipField = input( placeholder("12345"), maxLength(5), controlled( value <-- formVar.zip.view, onInput.mapToValue.filter(_.forall(Character.isDigit)) --> formVar.zip ) ) zip: (updater: Observer[String], view: Signal[String]) city: (updater: Observer[String], view: Signal[String]) Use Cases Form data Lenses for each field bishabosha/scaladays-2025
  19. 30 Lenses class VarLenses[C](val baseForm: Var[C])(using m: Mirror.ProductOf[C]) extends Selectable:

    type NT = NamedTuple.From[C] type FieldNames = NamedTuple.Names[NT] type IndexOf[A, T <: Tuple, Acc <: Int] <: Int = T match case A *: _ => Acc case _ *: tail => IndexOf[A, tail, S[Acc]] type NameIndex[S <: String] = IndexOf[S, FieldNames, 0] type Lens[X] = (updater: Observer[X], view: Signal[X]) type Fields = NamedTuple.Map[NT, [X] =>> Lens[X]] inline def selectDynamic(name: String): Lens[Any] = compute(constValue[NameIndex[name.type]]) def compute(idx: Int): Lens[Any] = ??? How its Done… bishabosha/scaladays-2025
  20. 31 infix opaque type Refined[A, F[_ <: A] <: Boolean]

    <: A = A Refined types Match-type predicate val one: Int Refined Positive = 1 val small: String Refined MaxChars[8] = "123456789" // error type Positive[A <: Int] <: Boolean = A match case S[?] => true case _ => false bishabosha/scaladays-2025 inline implicit conversion Use Cases Identity
  21. compiletime 32 object Refined: inline implicit def apply[A, F[_ <:

    A] <: Boolean](a: A)( using inline ev: F[a.type] =:= true ): A Refined F = a // identity function Refined types inline implicit conversion Proof match type reduces How its Done… bishabosha/scaladays-2025
  22. 33 case class City( id: Int, name: String, countryCode: String,

    district: String, population: Long ) object City extends SimpleTable[City] val fewLargestCities: Seq[City] = db.run( City.select .sortBy(c => c.population).desc .drop(5).take(3) ) ScalaSql SimpleTable outside query: City inside query: Record[City, Expr] population: Expr[Long] name: Expr[String] com-lihaoyi/scalasql Use Cases
  23. 34 type MapOver[C, T[_]] = T[Internal.Tombstone.type] match { case Internal.Tombstone.type

    => C case _ => Record[C, T] } type Id[C] = C; class Expr[C]; class Column[C] ScalaSql SimpleTable MapOver[City, Id] =:= City // external MapOver[City, Expr] =:= Record[City, Expr] // in query MapOver[City, Column] =:= Record[City, Column] // in query Unlikely `T` returns Tombstone => assume `Id` How its Done… com-lihaoyi/scalasql
  24. 35 ScalaSql SimpleTable final class Record[C, T[_]](data: IArray[AnyRef]) extends Selectable:

    type Fields = NamedTuple.Map[ NamedTuple.From[C], [X] =>> X match { case Nested => Record[X, T] case _ => T[X] } ] Spread fields of nested case class val c: Record[City, Expr] // in query c.name: Expr[String] // in query c.population: Expr[Int] // in query Wrap field in T[_] How its Done… com-lihaoyi/scalasql
  25. DataFrame 36 val text = "The quick brown fox jumps

    over the lazy dog" val toLower = (_: String).toLowerCase val stats = DataFrame .column((words = text.split("\\s+"))) .withComputed( (lowerCase = fun(toLower)(col.words)) ) .groupBy(col.lowerCase) .aggregate( group.key ++ (freq = group.size) ) .sort(col.freq, descending = true) Aggregate argument type ( lowerCase: GExpr[String], freq: GExpr[Int] ) Aggregate result type DataFrame[(lowerCase: String, freq: Int)] shape: (8, 2) ┌───────────┬──────┐ │ lowerCase ┆ freq │ ╞═══════════╪══════╡ │ the ┆ 2 │ │ over ┆ 1 │ │ quick ┆ 1 │ │ lazy ┆ 1 │ │ jumps ┆ 1 │ │ brown ┆ 1 │ │ dog ┆ 1 │ │ fox ┆ 1 │ └───────────┴──────┘ bishabosha/scalar-2025 Use Cases
  26. 37 DataFrame trait GroupBy[A <: AnyNamedTuple, T]: ... def agg[F

    <: AnyNamedTuple]( f: DataFrame.GRef[A, T] ?=> F )[ R <: AnyNamedTuple: {IsGExprRes[F], TagsOf} ]: DataFrame[R] type IsGExprRes[F <: AnyNamedTuple] = [R <: AnyNamedTuple] =>> R <:< GExprRes[F] type GExprRes[F <: AnyNamedTuple] = NamedTuple.Map[F, [X] =>> X match { case DataFrame.GExpr[t] => t } ] Implicit search drives type-inference Extract column types How its Done… bishabosha/scalar-2025
  27. type Foo[I <: Int] <: Int = I match case

    0 => 0 case 1 => 1 def foo[I <: Int](i: I): Foo[I] = i match case _: 0 => 0 def foo[I <: Int](i: I): Foo[I] = i match case _: 0 => 0 case _: 1 => 1 1. Check shape of dependent functions 39 Tips Accidental dependent typing mode Found: 0, Required: Foo[I] ✅
  28. 1. Check shape of dependent functions 41 Return type must

    be a match type! type B[X <: Boolean] = X match case true => Option[String] case false => String def bErr(b: Boolean): Either[Err, B[b.type]] = b match case _: true => Right(Some("hello")) case _: false => Either.cond(nextBoolean, "foo", Err("empty")) Found: Some[String], Required: B[b.type] Found: "foo", Required: B[b.type] Tips/Troubleshooting
  29. 2. Watch out for unchecked patterns 42 Due to type

    erasure, only first case is chosen def last[T <: HList](t: T): Last[T] = t match case t: (a :: HNil.type) => t.head case t: (_ :: ts) => last(t.tail) Unchecked type! Don’t even try! last(("abc", "def", "wxy")) ==> "abc" type Last[X <: HList] = X match case x :: HNil.type => x case _ :: xs => Last[xs] Tips/Troubleshooting
  30. 2. Watch out for unchecked patterns 43 type Last[X <:

    HList] = X match case x :: HNil.type => x case _ :: xs => Last[xs] inline def last[T <: HList](t: T): Last[T] = inline t match case t: (a :: HNil.type) => t.head case t: (_ :: ts) => last(t.tail) last(("abc", "def", "wxy")) ==> "wxy" ✅ inline match resolves at compile time Tips/Troubleshooting
  31. 44 2. Watch out for unchecked patterns type Last2[X <:

    HList] = X match case x :: xs => Last0[x, xs] case _ => Nothing type Last0[H, X <: HList] = X match case HNil.type => H case _ => Last2[X] def last2[T <: HList](t: T): Last2[T] = t match case t: (x :: xs) => last0(t.head, t.tail) case _ => sys.error("Empty") inline def last0[H, X <: HList](inline h: H, xs: X): Last0[H, X] = xs match case _: HNil.type => h case _ => last2(xs) Rewrite to safe patterns Tips/Troubleshooting
  32. //> using javaOpt -Xss512k val tup = (1,2,3,...,200) summon[Last[tup.type] =:=

    200] 4. Watch out for compiler stack space 45 Large types might blow the stack at build time: tune for your environment Perhaps a project to optimise provably finite loops? Tips/Troubleshooting