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, anobject, athis-path), because the forwarder re-evaluates it on every call and must see the same object. Adeforvaris rejected, as the pitfalls section shows. -
Only accessible members are eligible. The private
calibrateofAltimeteris never exported, and naming an inaccessible member is an error. -
Forwarders are
finalmembers of the exporting class, with the type of the original. The exporting class does not become a subtype of the collaborator:Hikeris not aCompass. -
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.
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
Source: Tour of Scala: Self-type
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
extendswhen 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
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’sexportworks 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 requiresthis: 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.