Scala 3 skill for Codex

Givens, enums and opaque types, migrated safely. Codex writes Scala 3 with the constructs the language now has: enums for ADTs, given and using for type classes, extension methods instead of implicit classes, and opaque types for domain IDs. For Scala 2.13 codebases it cross-builds, migrates leaf modules first, and lets the compiler's -source migration and -rewrite flags do the mechanical edits in reviewable commits.

  • Languages
  • $scala-3
  • 171 lines
  • 2 files
  • ~1,800 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- scala-3

Installs to ~/.agents/skills/scala-3/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $scala-3, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes idiomatic Scala 3 and migrates Scala 2.13 code - given/using instead of implicits, extension methods, enums and ADTs, opaque types, union types, derives clauses, significant indentation - with sbt, Mill or Scala CLI builds, scalafmt and scalafix, plus compiler rewrite flags for mechanical migration. Use when starting a Scala 3 module, porting a 2.13 codebase, cross-building a library, or decoding Scala 3 compiler errors about givens and implicits.

Use it when

  • Starting a new Scala 3 module or service
  • Porting Scala 2.13 code or cross-building a library for both
  • Decoding errors about givens, implicits or ambiguous instances
  • Modelling a domain with enums, opaque types and union types

Use something else when

  • The problem is JVM garbage collection or heap sizing: use jvm-tuning
  • The project is Kotlin rather than Scala: use kotlin-idioms

How to check the result

The build compiles with warnings as errors and tests pass on every cross Scala version, with scalafmt and MiMa checks clean for libraries.

The mistake it prevents

Using wildcard imports and expecting givens to come with them; Scala 3 needs import x.given, which is the root of many missing-instance errors after migration.

~/.agents/skills/scala-3/SKILL.md

--- name: scala-3 description: Writes idiomatic Scala 3 and migrates Scala 2.13 code - given/using instead of implicits, extension methods, enums and ADTs, opaque types, union… license: MIT ---

Good Scala 3 code says what it means with the new constructs (enums for ADTs, given for type class instances, extension for added methods, opaque types for zero-cost wrappers) and keeps implicit resolution small and local. Migrations are done with the compiler’s rewrite tools in small, compiling steps, not by hand-editing thousands of files.

Before changing code

  1. Read AGENTS.md, build.sbt / build.mill / project.scala, project/plugins.sbt, .scalafmt.conf and .scalafix.conf.
  2. Record scalaVersion and whether the build cross-compiles (crossScalaVersions). Scala 3.3.x is the LTS line; libraries usually publish against LTS so downstream users on any newer 3.x can consume them. Applications can track the latest 3.x.
  3. Dependencies resolve from Maven Central; the first sbt compile needs network access and can take minutes. Ask before running it in a restricted sandbox.

Scala 2 to 3 translation table

Scala 2Scala 3
implicit val ord: Ordering[User] = ...given Ordering[User] = ...
def f(x: Int)(implicit ec: ExecutionContext)def f(x: Int)(using ec: ExecutionContext)
implicitly[Show[A]]summon[Show[A]]
implicit class RichString(s: String) { def x = ... }extension (s: String) def x = ...
sealed trait Color; case object Red extends Colorenum Color { case Red, Green }
implicit def a2b(a: A): Bgiven Conversion[A, B] = ... (plus import scala.language.implicitConversions at use sites)
import cats.implicits._import cats.syntax.all.* (wildcard is *; givens need import x.given)
List[_]List[?]
def main(args: Array[String]) in an object@main def run(args: String*) = ...
value class class Id(val v: Long) extends AnyValopaque type Id = Long
macros (scala.reflect)inline + quotes/splices; Scala 2 macros do not run in Scala 3

Idioms

ADTs with enums

enum PaymentEvent:
  case Authorized(id: PaymentId, amount: BigDecimal)
  case Captured(id: PaymentId)
  case Failed(id: PaymentId, reason: String)

def describe(e: PaymentEvent): String = e match
  case PaymentEvent.Authorized(id, amt) => s"authorized $amt on $id"
  case PaymentEvent.Captured(id)        => s"captured $id"
  case PaymentEvent.Failed(id, why)     => s"failed $id: $why"

The compiler warns on non-exhaustive matches; treat that warning as an error (-Werror or -Xfatal-warnings depending on version) in CI.

Opaque types for domain IDs

object ids:
  opaque type PaymentId = String
  object PaymentId:
    def apply(raw: String): Either[String, PaymentId] =
      Either.cond(raw.startsWith("pay_"), raw, s"invalid payment id: $raw")
    extension (id: PaymentId) def value: String = id

Outside ids, a PaymentId is not a String, at zero runtime cost.

Type classes with given/using and derives

trait Show[A]:
  extension (a: A) def show: String

object Show:
  given Show[Int] with
    extension (a: Int) def show = a.toString

final case class User(name: String, age: Int) derives CanEqual

def render[A: Show](xs: List[A]): String = xs.map(_.show).mkString(", ")
  • Put givens in the companion object of the type or the type class so they are found without imports.
  • Name givens that are imported explicitly elsewhere (given userOrdering: Ordering[User]); anonymous givens get compiler-generated names that change with the type.
  • derives works with library type classes that provide derived (e.g. circe Codec.AsObject, CanEqual).
  • Use import x.given (or import x.{given Ordering[?]}) to bring givens in; plain * does not import them.

Union types and errors

def parse(s: String): Int | ParseError is fine for local, closed alternatives. For public APIs, prefer Either[Error, A] or an effect type the codebase already uses (cats-effect IO, ZIO); do not mix three error styles in one module.

Syntax style

Scala 3 accepts both braces and significant indentation. Follow the repo’s choice; if none exists, pick one and enforce it in .scalafmt.conf:

version = 3.8.3
runner.dialect = scala3
rewrite.scala3.convertToNewSyntax = true
rewrite.scala3.removeOptionalBraces = true   # or false to keep braces

Use an exact scalafmt version that the repo pins; the line above is an example.

Migrating a Scala 2.13 codebase

  • Get the 2.13 build green with -Xsource:3 (2.13.x accepts much Scala 3 syntax and warns about future breakage). Fix warnings.
  • Check every dependency has a _3 artifact. For 2.13-only libraries that do not use macros, ("org" %% "lib" % "x").cross(CrossVersion.for3Use2_13) works; macro libraries need a Scala 3 version.
  • Cross-build: crossScalaVersions := Seq("2.13.x", "3.3.x"), then migrate module by module, leaves first.
  • Use compiler rewrites: scalacOptions ++= Seq("-source:3.4-migration", "-rewrite") for a compile, commit, then remove the flags. -new-syntax -rewrite or -indent -rewrite convert syntax style.
  • Replace implicit conversions and implicit classes with Conversion givens and extensions; replace macros.
  • Run tests on both versions until the 2.13 leg is dropped.

Scala-version-specific source goes in src/main/scala-2.13/ and src/main/scala-3/; sbt picks them up automatically.

Build tool notes

ToolCommand
sbtsbt compile, sbt test, sbt "testOnly *PaymentSpec", sbt +test (all cross versions)
Scala CLI (the scala runner since 3.5)scala run ., scala test ., directives //> using scala 3.3, //> using dep org::lib:1.2.3
Mill./mill app.compile, ./mill app.test

Run sbt as a long-lived server (sbt then commands, or sbt --client) to avoid paying JVM startup on each step.

Gotchas

  • Ambiguous given errors usually mean two instances are in scope (one imported, one in a companion); remove the import or prioritise with a lower-priority trait.
  • “No given instance of type X was found” lists what it tried; look for a missing import x.given before writing a new instance.
  • == between unrelated types compiles unless strict equality is on (import scala.language.strictEquality with derives CanEqual); enable it for domain-heavy code.
  • Extension methods are found only if the extension is in scope or in the companion of a type involved; a top-level extension in another package needs an import.
  • Opaque types are transparent inside their defining scope; validation in the companion apply can be bypassed there.
  • -Werror after an upgrade surfaces new warnings in old code; budget for it.
  • Changing a public given name or an enum’s cases breaks binary compatibility; run MiMa (sbt mimaReportBinaryIssues) for libraries.

Verification

  • sbt +compile (or the equivalent) with warnings treated as errors in CI.
  • sbt +test passes on every cross version.
  • sbt scalafmtCheckAll and, if configured, sbt "scalafixAll --check".
  • For libraries: MiMa against the last release.
  • Report the Scala version(s), rewrite flags used and any dependencies still on for3Use2_13.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Scala 3"
  short_description: "Givens, enums and opaque types, migrated safely"
  default_prompt: "$scala-3 Migrate this sbt project from Scala 2.13 to Scala 3"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc