Skip to content
Back to skills

Loom Scala

ASecurity

Scala language expertise for idiomatic, production-quality code.

  • 56 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 1, 2026
ai-agentsrustgojavashellbashsqltestinggitapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 1, 2026

npx -y skills add cosmix/loom --skill loom-scala --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Loom Scala?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Loom Scala
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/cosmix-loom-scala/badge)](https://www.skillsdirectory.com/skills/cosmix-loom-scala)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: loom-scala
description: Scala language expertise for idiomatic, production-quality code.
triggers:
  - scala
  - scala 3
  - sbt
  - build.sbt
  - scala-cli
  - scalatest
  - munit
  - scalacheck
  - zio
  - cats
  - cats-effect
  - fs2
  - http4s
  - circe
  - doobie
  - given
  - using
  - implicit
  - extension
  - case class
  - sealed trait
  - pattern matching
  - scalafmt
  - scalafix
  - pekko
  - akka
---

# Scala Language Expertise

## Overview

Idiomatic, production-grade Scala 3 on the JVM: sbt builds, the Scala 3 syntax, ADTs and pattern matching, givens and extension methods, typed errors, the cats-effect and ZIO effect systems, and MUnit/ScalaTest testing. Assumes the reader writes Scala already. The content is the part that bites: givens that an import silently misses, `IO.pure` running a side effect once and early, blocking calls on the compute pool, non-exhaustive matches that only warn, and an sbt `testOnly` that exits 0 after running nothing.

## Tooling

sbt is the default build. Keep one sbt shell open (or `sbt --client`) for iterative work: every batch invocation boots a JVM and loads the build before running anything.

| Task | Command |
| --- | --- |
| Full gate | `sbt scalafmtCheckAll scalafmtSbtCheck "scalafixAll --check" test` |
| Compile main and tests | `sbt Test/compile` |
| All tests | `sbt test` |
| One suite | `sbt 'testOnly com.acme.FooSuite'` |
| One ScalaTest test by substring | `sbt 'testOnly com.acme.FooSpec -- -z "rejects symlinks"'` |
| Failed and changed tests only | `sbt testQuick` (`~testQuick` reruns on save) |
| Format | `sbt scalafmtAll scalafmtSbt` |
| Dependency tree | `sbt dependencyTree` (after `addDependencyTreePlugin` in `project/plugins.sbt`) |

```scala
// build.sbt
ThisBuild / scalaVersion := "3.3.4"          // the 3.3.x line is the LTS
ThisBuild / organization := "com.acme"
ThisBuild / semanticdbEnabled := true        // scalafix semantic rules need it

lazy val core = project
  .settings(
    libraryDependencies ++= Seq(
      "org.typelevel" %% "cats-effect" % "3.5.4",
      "org.scalameta" %% "munit" % "1.0.0" % Test,
      "org.typelevel" %% "munit-cats-effect" % "2.0.0" % Test,
    ),
    Test / fork := true,                     // tests in a separate JVM: clean system properties, no classloader leaks
  )

lazy val root = (project in file("."))
  .aggregate(core)                           // commands run at the root reach core
```

- `project/build.properties` pins the sbt version; `project/plugins.sbt` lists plugins.
- `%%` appends the Scala binary version to the artifact (`cats-effect_3`); `%` is for Java libraries. Scala 3 can use Scala 2.13 artifacts through `.cross(CrossVersion.for3Use2_13)`, except those that ship Scala 2 macros.
- Compiler flags: sbt-tpolecat sets strict flags per Scala version (`-deprecation`, `-feature`, `-Wunused:all`, and `-Werror` in CI mode). With warnings as errors, a non-exhaustive match fails the build.
- scalafmt reads `.scalafmt.conf`, which must set `version` and `runner.dialect = scala3`. scalafix semantic rules (`OrganizeImports`, `RemoveUnused`) need SemanticDB enabled as above.
- Scala CLI (the `scala` command since Scala 3.5) runs scripts and single-module projects configured with `//> using` directives. Mill is another build tool. Neither uses `build.sbt`.

## Scala 3 Syntax

```scala
enum Shape:
  case Circle(radius: Double)
  case Rect(width: Double, height: Double)

def area(shape: Shape): Double = shape match
  case Shape.Circle(r)            => math.Pi * r * r
  case Shape.Rect(w, h) if w == h => w * w
  case Shape.Rect(w, h)           => w * h

object ids:
  opaque type UserId = Long                  // a Long at runtime, a distinct type at compile time
  object UserId:
    def apply(value: Long): UserId = value
  extension (id: UserId) def value: Long = id

@main def lineCount(path: String): Unit =
  val lines = scala.util.Using.resource(scala.io.Source.fromFile(path))(_.getLines().size)
  println(s"$path: $lines lines")
```

- Significant indentation replaces braces; `then`/`do` follow `if`/`while`/`for` conditions, and `end` markers close long blocks. Pick one style per codebase and have scalafmt enforce it.
- Top-level definitions replace package objects. `export` forwards selected members of a field, which gives composition without hand-written delegation.
- Union (`String | Int`) and intersection (`A & B`) types; `derives` requests type class derivation (`derives CanEqual`, circe's `derives Codec.AsObject`).
- `opaque type` gives a distinct type with no runtime cost. `AnyVal` value classes box inside collections and generic code; opaque types never box beyond their underlying type.
- Migrating from 2.13: compile with `-source:3.0-migration -rewrite` to fix syntax. `implicit` definitions still compile in Scala 3 and interoperate with givens.

## ADTs and Pattern Matching

```scala
sealed trait Command
object Command:
  final case class Upload(source: String, target: String) extends Command
  final case class Delete(target: String) extends Command
  case object Flush extends Command

def run(cmd: Command): Either[String, Unit] = cmd match
  case Command.Upload(src, dst) if dst.isBlank => Left(s"empty target for $src")
  case Command.Upload(src, dst)                => upload(src, dst)
  case Command.Delete(target)                  => delete(target)
  case Command.Flush                           => Right(())
```

- `sealed` traits and `enum`s let the compiler check exhaustiveness. The check only warns, so build with `-Werror`. The compiler cannot see through guards: keep an unguarded case for every variant.
- `final case class` keeps data types closed. Case classes give structural `equals`/`hashCode`, `copy`, and an extractor for patterns.
- Type patterns are erased: `case xs: List[String]` matches any `List` and only emits an unchecked warning. Match on the elements, or keep the type in the ADT.
- A value no case matches throws `MatchError` at runtime. `collect { case ... }` and other `PartialFunction` uses make partiality explicit.
- Custom extractors: `object Email { def unapply(s: String): Option[(String, String)] = ... }` makes `case Email(user, domain) =>` work on plain strings.
- Scala 3 deprecates non-local `return` from inside a lambda. Use `scala.util.boundary` (3.3):

```scala
import scala.util.boundary, boundary.break

def firstNegative(xs: List[Int]): Option[Int] =
  boundary:
    for x <- xs do
      if x < 0 then break(Some(x))
    None
```

## Givens, Using and Extension Methods

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

object Show:                                   // the companion is in the implicit scope of Show[X]
  given Show[Int] with
    extension (a: Int) def show: String = a.toString

  given [A](using s: Show[A]): Show[List[A]] with
    extension (as: List[A]) def show: String = as.map(a => s.show(a)).mkString("[", ", ", "]")

def describe[A: Show](a: A): String = s"value: ${a.show}"   // the context bound puts Show[A] in scope

describe(42)              // Show[Int] found in the companion, no import
describe(List(1, 2, 3))   // Show[List[Int]] built from Show[Int]
```

- Put instances in the companion of the type class or of the data type: that is the implicit scope, searched without imports. Instances in unrelated objects (orphans) need explicit imports and invite ambiguity.
- `import a.*` does not import `given` definitions. Use `import a.given` or `import a.{*, given}`. Scala 2 `implicit` definitions, including most of cats' syntax, still arrive with `*`.
- Extension syntax on a bare value (`42.show` outside any `Show` context) searches the implicit scope of the receiver's type, which does not include `Show`'s companion; bring the instances into lexical scope with `import Show.given`.
- Two eligible givens of the same type fail with an ambiguity error. Resolve by specificity or by moving the fallback into a lower-priority parent trait of the companion.
- `using` parameters carry context (an `ExecutionContext`, a transaction, a config). Passing ordinary dependencies implicitly hides them from readers; use constructor parameters.
- Implicit conversions (`given Conversion[A, B]`) need a language import and surprise readers. Extension methods cover the legitimate cases.
- Automatic derivation of deep type class trees (circe's fully automatic mode) is slow to compile. Semi-automatic `derives` on each type keeps compile times flat.

## Errors

```scala
enum ConfigError:
  case Missing(key: String)
  case Invalid(key: String, value: String)

def port(env: Map[String, String]): Either[ConfigError, Int] =
  for
    raw  <- env.get("PORT").toRight(ConfigError.Missing("PORT"))
    port <- raw.toIntOption.filter(p => p > 0 && p < 65536).toRight(ConfigError.Invalid("PORT", raw))
  yield port

def readConfig(path: Path): Either[Throwable, String] =
  Try(Files.readString(path)).toEither          // wrap the throwing Java call once, at the edge
```

- `Either[E, A]` with an error ADT for expected failures, `Option` for absence, `Try` only to capture exceptions from Java or legacy APIs, converted to `Either` at the edge. For-comprehensions chain all three.
- Throw for bugs. Inside an effect type, failures travel in the effect (`IO.raiseError`, ZIO's error channel).
- Catch with `case NonFatal(e) =>`. `case e: Throwable` also catches `InterruptedException` and VM errors.
- `scala.util.Using.resource(open())(use)` and `Using.Manager` close `AutoCloseable` resources on every path.

## Effects: Cats Effect and ZIO

Pick one effect system per codebase. `Future` is eager, memoized and not cancellable; it suits simple async glue, and blocking code inside it belongs in `blocking {}`.

```scala
import cats.effect.{IO, IOApp, Resource}

object Main extends IOApp.Simple:
  def fetch(id: Long): IO[String] =
    IO.blocking(legacyClient.get(id))                        // blocking call on the blocking pool

  val run: IO[Unit] =
    Resource.fromAutoCloseable(IO(openDatabase())).use { db =>
      IO.parTraverseN(4)((1L to 20L).toList)(fetch)          // at most 4 in flight
        .flatMap(pages => IO.blocking(db.saveAll(pages)))
    }
```

- An `IO` value describes a computation; nothing runs until the runtime executes it. `IO.pure(expr)` evaluates `expr` once, at construction. Side effects go in `IO(...)`/`IO.delay`.
- Blocking calls go in `IO.blocking`, or `IO.interruptible` to allow cancellation by thread interrupt. A blocking call in plain `IO` stalls one of the compute threads, of which there is one per core.
- `Resource` for anything with a release step; `use` releases on success, error and cancellation.
- `Ref` for shared state, `Deferred` for a one-shot signal, `Queue` for handoff between fibers. A fiber from `start` that nobody joins leaks when its parent is cancelled; use `background` (a `Resource`) or a `Supervisor`.
- `unsafeRunSync()` belongs at the program edge only. Inside `IO` code it blocks a compute thread and can deadlock.

```scala
import zio.*

trait UserRepo:
  def find(id: Long): IO[RepoError, Option[User]]

final case class UserService(repo: UserRepo):
  def name(id: Long): IO[RepoError, String] =
    repo.find(id).someOrFail(RepoError.NotFound(id)).map(_.name)

object UserService:
  val layer: ZLayer[UserRepo, Nothing, UserService] =
    ZLayer.fromZIO(ZIO.service[UserRepo].map(UserService(_)))
```

- `ZIO[R, E, A]` carries an environment, a typed error and a result. Expected failures go in `E`; defects (`ZIO.die`, exceptions thrown inside `ZIO.succeed`) are for bugs. `orDie` turns a failure into a defect, `refineOrDie` keeps the errors callers can handle.
- `ZIO.succeed` takes its argument by name, so the effect is deferred; code that can throw belongs in `ZIO.attempt`, blocking code in `ZIO.attemptBlocking`.
- `ZLayer` wires dependencies; `ZIO.acquireRelease` with `ZIO.scoped` manages resources; `ZIO.foreachPar(xs)(f).withParallelism(n)` bounds fan-out.

## Collections

```scala
val totals: Map[String, BigDecimal] =
  orders.groupMapReduce(_.customer)(_.amount)(_ + _)                  // one pass, duplicates summed

val firstLarge = orders.view.filter(_.amount > 1000).map(_.id).headOption   // stops at the first match
```

- Immutable by default. `List` prepends in O(1), but indexing and `:+` are O(n); `Vector` gives near-constant indexing and append. Build locally with `ListBuffer`/`ArrayBuffer` and return an immutable result.
- `.view` makes a chain lazy; `Iterator` is single-pass; `LazyList` memoizes.
- `Map.mapValues` and `filterKeys` return lazy views in 2.13 and 3 that recompute on every access (and are deprecated). Write `.view.mapValues(f).toMap`.
- `toMap` on pairs keeps the last value for a duplicate key without warning. `groupBy`, `groupMap` and `groupMapReduce` keep or combine them.
- `Array` equality is reference equality; compare with `sameElements` or use `ArraySeq`.
- `foldLeft` is stack-safe on `List`; deep `foldRight` over a large `List` is where stack overflows come from in older code.

## Testing

```scala
package com.acme.spool

import java.nio.file.{Files, Path}

class SymlinkedSpoolSuite extends munit.FunSuite:
  val dirs = FunFixture[Path](
    setup = _ => Files.createTempDirectory("spool"),
    teardown = dir => deleteTree(dir),
  )

  dirs.test("rejects a spool directory that is a symlink") { root =>
    val outside = Files.createTempDirectory("outside")
    val spool = Files.createSymbolicLink(root.resolve("spool"), outside)
    intercept[SecurityException](SpoolWriter(spool).write("a.json", "{}"))
  }
```

- MUnit (`munit.FunSuite`): `test("name") { ... }`, `assertEquals(obtained, expected)` with a diff on failure, `intercept[E]`, `FunFixture` for per-test setup. `munit-cats-effect`'s `CatsEffectSuite` runs `IO` tests (`assertIO(io, expected)`).
- ScalaTest: styles (`AnyFunSuite`, `AnyFlatSpec`, `AnyWordSpec`) with `Matchers` (`result shouldBe 3`, `an[IllegalArgumentException] should be thrownBy f()`); `AsyncFunSuite` for `Future` code.
- ZIO Test: `object UserServiceSpec extends ZIOSpecDefault` with `suite(...)(test(...) { ... assertTrue(...) })`; `TestClock` controls time.
- Property tests: ScalaCheck `forAll`, through `munit-scalacheck` or ScalaTest's `ScalaCheckPropertyChecks`.
- Without forking, sbt runs suites in parallel inside its own JVM. With `Test / fork := true` they run in one separate JVM, one suite after another unless `testGrouping` splits them. Suites that share files, ports or global state need `Test / parallelExecution := false` or isolation.

## Loom Test Runner Adapter

**Adapter.** Scala uses `sbt`. A directory with `build.sbt` is kind `scala`, and the runner for kind `scala` is always `sbt`; `loom project detect` prints it per package. Mill and Scala CLI projects have no `build.sbt`, so detection does not see them as Scala.

```text
ingest  kinds=scala  runner=sbt  skills=loom-scala
```

**Single-test command.** loom runs it with the package directory, the one holding `build.sbt`, as the working directory:

```bash
sbt 'testOnly {test}'
```

**The `test` field.** The fully qualified suite name, for example `com.acme.spool.SymlinkedSpoolSuite`; for a ZIO Test `object`, the object's name as written in source. The command selects a whole suite and passes no per-test filter, so every test in that suite runs.

**No-match behaviour.** D5 marks `sbt` "documented": no fixture was captured, and loom's `sbt` parser was written from sbt's documented output. When the name matches no suite, sbt prints `No tests to run for Test / testOnly` and exits 0, so the exit code alone would read a misspelled name as a pass. loom reads the runner's summary instead: a name that selects nothing classifies as `NotSelected`, which fails the freeze ("the runner did not select the test") and fails the completion check ("contract test not selected").

**Writing contract tests.** Test sources live under `src/test/scala/` of the subproject that owns the code, for example `core/src/test/scala/com/acme/spool/SymlinkedSpoolSuite.scala` for `lazy val core = project`. Because the adapter selects suites:

- write one suite per contract, holding only that contract's test, and name it after the contract;
- keep the `package` clause equal to the directory path; Scala does not enforce it, and a mismatch makes the suite name differ from what the path suggests;
- make sure the root project aggregates the subproject (`.aggregate(core)`); `testOnly` at the root never reaches an unaggregated subproject. A build with no explicit root project gets a default root that aggregates every subproject.

```yaml
contracts:
  - id: rejects-symlinked-spool
    file: core/src/test/scala/com/acme/spool/SymlinkedSpoolSuite.scala
    test: com.acme.spool.SymlinkedSpoolSuite
    scenario: the spool directory is a symlink to a directory outside the data root
    rejects: a writer that follows the symlink and writes files outside the data root
```

**Build failures.** A contract suite that does not compile yet counts as red at freeze time: loom classifies the compiler failure as `BuildFailed`. sbt compiles a subproject's whole `Test` configuration together, so one uncompilable contract suite makes every suite in that subproject `BuildFailed` until the implementation exists. Freeze and completion run each command under a 300-second limit, and every run starts a fresh sbt: run the single-test command once before freezing so dependency resolution and the first compile happen outside the timed run. A contract that needs a new test dependency edits `build.sbt`, which must match one of the stage's `harness` globs.

## Anti-Patterns

```scala
// Partial accessors throw on the empty case
val user = users.find(_.id == id).get                     // BAD: NoSuchElementException
val user = users.find(_.id == id).toRight(UserNotFound(id))   // GOOD: the caller handles absence

// Catching everything
try risky() catch { case e: Throwable => log(e) }         // BAD: also catches InterruptedException, VM errors
try risky() catch { case NonFatal(e) => log(e) }          // GOOD

// Side effect inside IO.pure runs once, when the value is built
val tick = IO.pure(println("tick"))                       // BAD
val tick = IO.println("tick")                             // GOOD: runs every time tick runs

// Blocking inside IO on the compute pool
IO(statement.executeQuery(sql))                           // BAD: stalls a compute thread
IO.blocking(statement.executeQuery(sql))                  // GOOD
```

Quick swaps: `null` from Java → `Option(javaValue)`; `.head`/`.last` on a possibly empty collection → `headOption`/`lastOption`; `return` inside a lambda → `boundary`/`break` or a fold; `Await.result` in `Future` code → `flatMap`/`map`; a `var` holding a mutable collection shared between threads or fibers → `Ref` or `AtomicReference`; `String` or `Long` ids → opaque types; `asInstanceOf` → a pattern match; implicit conversions → extension methods; `List[Any]` → an ADT; `==` between unrelated types (compiles by default) → `-language:strictEquality` with `derives CanEqual`.

## Expert Practices

### Equality and Variance

- Universal equality lets `1 == "1"` compile (and return `false`). `-language:strictEquality` plus `derives CanEqual` on your types turns such comparisons into compile errors.
- Variance: `+A` for producers and immutable containers, `-A` for consumers (function inputs, encoders). Mutable containers stay invariant; that is why `Array[A]` is invariant.
- Case classes holding functions or arrays have `equals` by reference for those fields.

### Security

- The JVM rules apply: no Java deserialization of untrusted bytes, XML parsers with DTDs disabled, `SecureRandom` for tokens, normalized paths checked against their base. Pekko keeps Java serialization off by default, as Akka does since 2.6; leave it off.
- SQL interpolators: doobie's `sql"... where name = $name"` and Slick's `sql"... $name"` bind parameters. doobie's `Fragment.const` and Slick's `#$name` splice raw text and are injectable.
- JSON codecs (circe, jsoniter-scala) decode into known types. Keep class names out of the payload; model polymorphism as a sealed ADT with a discriminator field.
- `sys.process` with a single string splits on whitespace (`"tar -xf " + file`). Pass a `Seq` of arguments.

### Performance

- `@tailrec` makes the compiler verify that a self-recursive call is in tail position; it compiles to a loop.
- Generic code over `Int`, `Long` or `Double` boxes every element; Scala 3 does not specialize. Hot numeric loops use `Array[Int]` or `IArray`.
- Appending to a `List` in a loop is quadratic. Prepend and reverse once, or build with `ListBuffer`/`VectorBuilder`.
- Compile time is a cost: automatic derivation, large macros and very large files slow every build. Split modules along dependency lines so incremental compilation recompiles less.
- Profile with JFR or async-profiler, and benchmark with sbt-jmh; timing loops in tests measure JIT warm-up.

## Verification Checklists

**Before marking Scala work done:**

- [ ] `sbt scalafmtCheckAll "scalafixAll --check" test` passes, with warnings as errors (sbt-tpolecat or `-Werror`)
- [ ] New behaviour has tests; the new suites appear in the run summary with a non-zero count
- [ ] Every `match` over an ADT is exhaustive without guards hiding a variant; no type patterns on erased type arguments
- [ ] No `.get`, `.head` or `asInstanceOf` on values that can be absent or of another type
- [ ] Expected failures are typed (`Either`, the effect's error channel); catches use `NonFatal`
- [ ] Givens live in companions or are imported with `given`; no new implicit conversions
- [ ] Raw SQL splices (`Fragment.const`, `#$`) never see untrusted input

**Effects and concurrency review:**

- [ ] Side effects are suspended (`IO(...)`, `ZIO.attempt`); no `IO.pure` around effects
- [ ] Blocking calls in `IO.blocking`/`ZIO.attemptBlocking`; fan-out bounded (`parTraverseN`, `withParallelism`)
- [ ] Resources acquired through `Resource`/`ZIO.acquireRelease`; no fiber started without an owner
- [ ] No `unsafeRunSync`, `Await.result` or mutable shared state outside `Ref` inside effect code
- [ ] Suites that share files, ports or global state run isolated (`Test / parallelExecution` or forking)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…