Composition, Export & Self Types

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Scala 3.10 (3.10.0, 2026-10-12) and every example was run with Scala CLI and Scala 3.10.0 on JDK 26 on 2026-10-15 (the examples were not re-run on 3.9 or 3.3). Export clauses, self types and abstract type members have all been available since Scala 3.0, so everything here works on the 3.9 LTS and 3.3 LTS lines too; only the wording of diagnostics may differ between compiler versions.

Inheritance is not the only way to put behaviour together. A class can also hold collaborators and forward calls to them (composition), and Scala 3 added the export clause to remove the boilerplate of writing those forwarders by hand. This page covers export, the self type that lets a trait demand a collaborator without inheriting from it, abstract type members, and how these pieces combine into modules in an object-oriented and in a functional style. For traits and mixin linearization see Traits & Mixins, for the inheritance modifiers see Inheritance: Abstract, Sealed, Open & Final, and for extension methods see Extension Methods.

The examples use the trailhead scenario of this section: a toolkit that plans hikes on a network of mountain trails.

Composition over inheritance

Inheritance ties a class to the full public surface and the construction order of its parent, and a class can extend only one class. Composition keeps a private field of the collaborator’s type and exposes just what is needed. The classic cost was boilerplate: one forwarding method per member. In Scala 3 an export clause creates those forwarders for you.

Export clauses

An export clause looks like an import, but it defines members. For export compass.bearingTo inside a class, the compiler adds a method bearingTo to the class that calls compass.bearingTo.

//> using scala 3.10.0

class Compass:
  def bearingTo(x: Double, y: Double): Double = math.toDegrees(math.atan2(x, y))
  def declination: Double = 1.5

class Altimeter:
  def altitude: Int = 1840
  private def calibrate(): Unit = ()

class Hiker(name: String):
  private val compass = new Compass
  private val altimeter = new Altimeter
  export compass.bearingTo
  export altimeter.altitude
  def label: String = s"$name at $altitude m"

@main def e1(): Unit =
  val h = Hiker("Ana")
  println(h.altitude)
  println(h.bearingTo(1, 1))
  println(h.label)
1840
45.0
Ana at 1840 m

The rules worth knowing:

  • The qualifier must be an immutable path (a val, an object, a this-path), because the forwarder re-evaluates it on every call and must see the same object. A def or var is rejected, as the pitfalls section shows.

  • Only accessible members are eligible. The private calibrate of Altimeter is never exported, and naming an inaccessible member is an error.

  • Forwarders are final members of the exporting class, with the type of the original. The exporting class does not become a subtype of the collaborator: Hiker is not a Compass.

  • All overloads of a name are exported together. A selector names a member, not a signature.

  • Exports take part in override and abstract-member checks like hand-written members, so an export can implement an abstract member of a trait the class extends.

  • Exports are also allowed at the top level of a file and inside objects, which supports the facade pattern: a single object that re-exposes selected members of several others.

Selectors: wildcard, renaming and exclusion

An export clause takes the same selector forms as an import: x.m, x.{a, b}, x.*, x.m as n to rename, and x.m as _ to exclude a member (usually followed by *).

//> using scala 3.10.0

class Printer:
  def print(bits: Int): String = s"bits=$bits"
  def print(text: String): String = s"text=$text"
  def status: String = "idle"
  def reset(): Unit = println("reset")
  val model = "P-1"

class Scanner:
  def scan(): String = "scanned"
  def status: String = "ready"

class Copier:
  private val printUnit = Printer()
  private val scanUnit = Scanner()
  export printUnit.{status as printerStatus, print as _, *}
  export scanUnit.scan
  export printUnit.print          // both overloads
  export scanUnit.{status as scannerStatus}

@main def e2(): Unit =
  val c = Copier()
  println(c.printerStatus)
  println(c.scannerStatus)
  println(c.print(3))
  println(c.print("x"))
  println(c.model)
  println(c.scan())
idle
ready
bits=3
text=x
P-1
scanned

In this example export printUnit.{status as printerStatus, print as _, *} forwards every member of the printer except print, with status renamed so that it does not clash with the scanner’s status. The two print overloads are then exported by a separate clause, which shows that an overloaded name is exported as a whole.

A wildcard export differs from an import wildcard in two ways. It does not export members that the class already defines, and it does not export given instances (see the next section).

Givens and extensions

A plain * skips given instances, exactly as a plain import does; export them with given (or given T to select by type).

//> using scala 3.10.0

trait Cmp[T]:
  def cmp(a: T, b: T): Int
object Instances:
  given Cmp[Int] = (a, b) => a - b
  def helper = 1

object Plain:
  export Instances.*        // the given is not exported
object WithGiven:
  export Instances.given    // only givens

@main def e18(): Unit =
  println(Plain.helper)
  import WithGiven.given
  println(summon[Cmp[Int]].cmp(3, 1))
1
2

An export clause may also appear inside an extension block. The qualifier must then be an identifier that refers to a unique parameterless extension method in the same block, and every accessible member of its result becomes an extension method of the block’s receiver.

//> using scala 3.10.0

class TextOps(x: String):
  def shout: String = x.toUpperCase + "!"
  def twice: String = x + x

extension (x: String)
  def firstWord: String = x.takeWhile(_ != ' ')
  private def moreOps = TextOps(x)
  export moreOps.*

@main def e16(): Unit =
  println("rock climbing".firstWord)
  println("hut".shout)
  println("ab".twice)
rock
HUT!
abab

Here shout and twice are extension methods of String, forwarded from the TextOps helper that the private extension method moreOps builds from the receiver.

Export, inheritance and import

extends export import

Creates members in the class

Yes (inherited)

Yes (forwarders)

No

Subtype of the source

Yes

No

No

Source can be a val

No, it is a type

Yes

Yes

Self types

A self type declares what a trait or class needs from its eventual concrete instance, without extending it. In trait Planner { this: Clock & Logger => ... } the body can call now and log as if inherited, but Planner is not a subtype of Clock.

//> using scala 3.10.0

trait Clock:
  def now: Long

trait Logger:
  def log(msg: String): Unit

// self type: Planner needs a Clock and a Logger, without inheriting from them
trait Planner:
  this: Clock & Logger =>
  def plan(trail: String): String =
    log(s"planning $trail")
    s"$trail at $now"

trait Fixed extends Clock, Logger:
  def now = 1000L
  def log(msg: String) = println(s"[log] $msg")

object App extends Planner, Clock, Logger:
  def now = 42L
  def log(msg: String) = println(s"[app] $msg")

@main def e7(): Unit =
  println(App.plan("Aguja"))
  val p = new Planner with Fixed
  println(p.plan("Ordesa"))
[app] planning Aguja
Aguja at 42
[log] planning Ordesa
Ordesa at 1000

The compiler checks the requirement wherever the trait is mixed in. Leaving out a required collaborator is an error:

//> using scala 3.10.0
trait Clock:
  def now: Long
trait Planner:
  this: Clock =>
  def plan: String = s"at $now"
class Bad extends Planner
@main def e8(): Unit = ()
-- [E058] Type Mismatch Error: e8.scala:7:6
7 |class Bad extends Planner
  |      ^
  |illegal inheritance: self type Bad of class Bad does not conform to self type Clock
  |of parent trait Planner

And the self type is a constraint on the implementation, not an addition to the public interface. With val p: Planner = new Planner with Clock { def now = 1L }, the static type Planner does not expose now:

-- [E008] Not Found Error: e9.scala:10:12
10 |  println(p.now)
   |          ^^^^^
   |          value now is not a member of Planner

Self types are the basis of the cake pattern of dependency injection: components are traits, each declares its dependencies as a self type, and a final object mixes them all together (the Cake object in the module example below).

Self type or inheritance?

  • Use extends when the new type really is a kind of its parent and clients should see the parent’s members.

  • Use a self type when the trait only uses another one as an implementation detail, so that the dependency does not leak into the trait’s public API.

  • Prefer constructor parameters for dependencies that are not themselves part of a mixin hierarchy. They are simpler to test and do not constrain how the final object is assembled.

  • Self types may be cyclic: two traits may each require the other, and a concrete class must then mix in both.

The self-type spelling

Any identifier may replace this: trait A { self: B => ... }. The alias is useful in a nested class, where this would mean the inner instance. The compound requirement is an intersection type, written with &. The Scala 2 spelling this: A with B => is no longer accepted by default on 3.10.0:

-- [E003] Syntax Error: e20.scala:5:10
5 |  this: A with B =>
  |          ^^^^
  |with as a type operator has been deprecated; use & instead
  |This construct can be rewritten automatically under -rewrite -source 3.4-migration.

(with is still used to mix in traits in a new expression; only its use as a type operator was dropped.)

Abstract type members

A type can be left abstract in a trait or class, to be fixed by a subclass. It is the alternative to a type parameter when the type is an implementation detail of the family.

//> using scala 3.10.0

trait Climb:
  type Grade                       // abstract type member
  def grade: Grade
  def describe(g: Grade): String

class UiaaClimb(val grade: Int) extends Climb:
  type Grade = Int
  def describe(g: Int) = s"UIAA $g"

class FrenchClimb(val grade: String) extends Climb:
  type Grade = String
  def describe(g: String) = s"French $g"

// type parameter version: the grade shows up in every signature
trait Route[G]:
  def grade: G

@main def e10(): Unit =
  val c: Climb = UiaaClimb(5)
  println(c.describe(c.grade))
  val f = FrenchClimb("6a")
  println(f.describe(f.grade))
  val other: Climb = FrenchClimb("7b")
  // c.describe(other.grade)   // does not compile: other.Grade is not c.Grade
UIAA 5
French 6a

Type members are path-dependent: each Climb instance has its own Grade. Mixing up two instances is a compile error (same hierarchy, with val c: Climb = UiaaClimb(5) and val other: Climb = FrenchClimb("7b")):

-- [E007] Type Mismatch Error: e11.scala:15:27
15 |  println(c.describe(other.grade))
   |                     ^^^^^^^^^^^
   |                     Found:    other.Grade
   |                     Required: c.Grade

Choose a type parameter (Route[G]) when clients must name or vary the type (Route[Int]), and an abstract type member when the implementation chooses the type once and the interface should not carry it in every signature.

Modules

A module is a cohesive unit with a stable interface and a hidden implementation. Scala has two main idioms.

Object-oriented style

The interface is a trait, the implementation a class, and dependencies come in through the constructor (DefaultService(repo: TrailRepository) in the example below). This is a service-oriented design: callers depend on TrailService, never on DefaultService, and tests pass in a fake repository.

Functional style

The module is an object (or the top level of a file) holding plain functions over immutable data types. There is no state and no interface to implement, so tests need no stubs.

//> using scala 3.10.0

// OOP style module: a trait with abstract members, a class with the implementation
trait TrailRepository:
  def names: List[String]

trait TrailService:
  def longest: String

class InMemoryRepository extends TrailRepository:
  def names = List("Aguja", "Ordesa", "Cares")

// constructor injection
class DefaultService(repo: TrailRepository) extends TrailService:
  def longest = repo.names.maxBy(_.length)

// FP style module: an object with plain functions over immutable data
object Trails:
  final case class Trail(name: String, km: Double)
  def total(ts: List[Trail]): Double = ts.map(_.km).sum
  def longest(ts: List[Trail]): Option[Trail] = ts.maxByOption(_.km)

@main def e12(): Unit =
  val svc: TrailService = DefaultService(InMemoryRepository())
  println(svc.longest)
  val ts = List(Trails.Trail("Aguja", 7.5), Trails.Trail("Cares", 12.0))
  println(Trails.total(ts))
  println(Trails.longest(ts))
Ordesa
19.5
Some(Trail(Cares,12.0))

Assembling modules with export

A larger module can be assembled from services and expose their combined surface through export, or through a self-typed cake. Both produce working modules; the export version keeps each piece private and does not require the final object to extend every component.

//> using scala 3.10.0

trait Storage:
  def load(): List[String]
trait Notifier:
  def send(msg: String): Unit

// each service is a trait; the concrete module assembles them with export
class StorageImpl extends Storage:
  def load() = List("Aguja", "Cares")
class NotifierImpl extends Notifier:
  def send(msg: String) = println(s"notify: $msg")

class HikeModule:
  private val storage = StorageImpl()
  private val notifier = NotifierImpl()
  export storage.load
  export notifier.send

// a self-type "cake" assembly, for contrast
trait StorageComponent:
  def storage: Storage
trait PlannerComponent:
  this: StorageComponent =>
  def firstTrail: String = storage.load().head

object Cake extends PlannerComponent, StorageComponent:
  val storage = StorageImpl()

@main def e13(): Unit =
  val m = HikeModule()
  m.send(m.load().mkString(","))
  println(Cake.firstTrail)
notify: Aguja,Cares
Aguja
flowchart TD HM["HikeModule"] --> ST["private storage: StorageImpl"] HM --> NO["private notifier: NotifierImpl"] ST -->|"export storage.load"| L["HikeModule.load"] NO -->|"export notifier.send"| SE["HikeModule.send"] CK["Cake object"] --> PC["PlannerComponent with self type StorageComponent"] CK --> SC["StorageComponent: storage"] PC -.->|"requires"| SC

Pitfalls

Name clash between an export and a member. The forwarder is a real member, so defining a method with the same name and signature is a conflict (verified on 3.10.0, with export p.status plus def status: String = "mine"):

-- [E120] Naming Error: e4.scala:6:11
6 |  export p.status
  |           ^
  |Conflicting definitions:
  |def status: String in class Copier at line 7 and
  |final def status: String in class Copier at line 6
  |have the same type (): String after erasure.
  |
  |Consider adding a @targetName annotation to one of the conflicting definitions
  |for disambiguation.

Rename one with as, or exclude the name with as _.

Exporting a non-accessible member. Private and protected members of the collaborator are not eligible:

-- Error: e5.scala:9:11
9 |  export e.hidden
  |           ^^^^^^
  |         no eligible member hidden at Hut.this.e
  |         Hut.this.e.hidden cannot be exported because it is not accessible

Exporting from something that is not immutable. With def c = new Compass and export c.bearing:

-- [E083] Type Error: e17.scala:6:9
6 |  export c.bearing
  |         ^
  |(Hiker.this.c : => Compass) is not a valid export prefix, since it is not an immutable path

Treating a forwarder as a subtype relation. Hiker does not become a Compass, so it cannot be passed where a Compass is expected. If you need that, use inheritance or a shared trait.

Expecting a wildcard export to carry givens. export Instances.* forwards helper but not the given; the example in the givens section needs export Instances.given.

Differences from other languages

  • Java has no equivalent of export; delegation is written by hand as forwarding methods. Java interfaces can carry default methods, which is the nearest thing to a Scala trait.

  • Kotlin has delegation with by (class Hiker(c: Compass) : CompassApi by c), which also makes the class a subtype of the interface. Scala’s export works with classes as well as interfaces but does not create a subtype.

  • Scala 2 had no export; the usual workarounds were hand-written forwarders, implicit conversions, or the cake pattern to assemble modules.

Where the books are outdated

  • Scala 2 books present the cake pattern as the standard way to inject dependencies; in Scala 3 constructor and context parameters are usually preferred, although self types are unchanged.

  • this: A with B => appears in older code; Scala 3 requires this: A & B =>.

  • Before Scala 3 there was no export, so older books show manual forwarders or implicit conversions for the same purpose.

The run results above are from 2026-10-15 on Scala 3.10.0 and were not re-run on the 3.9 LTS or 3.3 LTS releases.