Several previous initiatives proposed a “export” or “public import” feature that expresses forwarding.
The latest thread on this forum, opened by @japgolly a year ago contains links to several earlier proposals.
I would like to propose a new Pre-SIP for this feature that comes with detailed rules and an implementation. The rest of the proposal here quotes from the doc page, which contains several additional details.
Example
An export clause defines aliases for selected members of an object. Example:
class BitMap
class InkJet
class Printer {
type PrinterType
def print(bits: BitMap): Unit = ???
def status: List[String] = ???
}
class Scanner {
def scan(): BitMap = ???
def status: List[String] = ???
}
class Copier {
private val printUnit = new Printer { type PrinterType = InkJet }
private val scanUnit = new Scanner
export scanUnit.scan
export printUnit.{status => _, _}
def status: List[String] = printUnit.status ++ scanUnit.status
}
The two export
clauses define the following export aliases in class Copier
:
final def scan(): BitMap = scanUnit.scan()
final def print(bits: BitMap): Unit = printUnit.print(bits)
final type PrinterType = printUnit.PrinterType
They can be accessed inside Copier
as well as from outside:
val copier = new Copier
copier.print(copier.scan())
Rules
An export clause has the same format as an import clause. Its general form is:
export path . { sel_1, ..., sel_n }
export implied path . { sel_1, ..., sel_n }
It consists of a qualifier expression path
, which must be a stable identifier, followed by one or more selectors sel_i
that identify what gets an alias. Selectors can be of one of the following forms:
- A simple selector
x
creates aliases for all eligible members ofpath
that are namedx
. - A renaming selector
x => y
creates aliases for all eligible members ofpath
that are namedx
, but the alias is namedy
instead ofx
. - An omitting selector
x => _
preventsx
from being aliased by a subsequent
wildcard selector. - A wildcard selector creates aliases for all eligible members of
path
except for
those members that are named by a previous simple, renaming, or omitting selector.
A member is eligible if all of the following holds:
- its owner is not a base class of the class(*) containing the export clause,
- it is accessible at the export clause,
- it is not a constructor, nor the (synthetic) class part of an object,
- it is an
implied
instance (or an old-styleimplicit
value)
if and only if the export isimplied
.
It is a compile-time error if a simple or renaming selector does not identify any eligible
members.
Type members are aliased by type definitions, and term members are aliased by method definitions. Export aliases copy the type and value parameters of the members they refer to. Export aliases are always final
. Aliases of implied instances are again implied
(and aliases of old-style implicits are implicit
). There are no other modifiers that can be given to an alias. This has the following consequences for overriding:
- Export aliases cannot be overridden, since they are final.
- Export aliases cannot override concrete members in base classes, since they are
not markedoverride
. - However, export aliases can implement deferred members of base classes.
Export aliases for value definitions are marked by the compiler as “stable”. This means that they can be used as parts of stable identifier paths, even though they are technically methods. For instance, the following is OK:
class C { type T }
object O { val c: C = ... }
export O.c
def f: c.T = ...
Export clauses can appear in classes or they can appear at the top-level. An export clause cannot appear as a statement in a block.
(*) Note: Unless otherwise stated, the term “class” in this discussion also includes object and trait definitions.
Motivation
It is a standard recommendation to prefer aggregation over inheritance. This is really an application of the principle of least power: Aggregation treats components as blackboxes whereas inheritance can affect the internal workings of components through overriding. Sometimes the close coupling implied by inheritance is the best solution for a problem, but where this is not necessary the looser coupling of aggregation is better.
So far, object oriented languages including Scala made it much easer to use inheritance than aggregation. Inheritance only requires an extends
clause whereas aggregation required a verbose elaboration of a sequence of forwarders. So in that sense, OO languages are pushing programmers to a solution that is often too powerful. Export clauses redress the balance. They make aggregation relationships as concise and easy to express as inheritance relationships. Export clauses also offer more flexibility than extends clauses since members can be renamed or omitted.
Export clauses also fill a gap opened by the shift from package objects to toplevel definitions. One occasionally useful idiom that gets lost in this shift is a package object inheriting from some class. The idiom is often used in a facade like pattern, to make members of internal compositions available to users of a package. Toplevel definitions are not wrapped in a user-defined object, so they can’t inherit anything. However, toplevel definitions can be export clauses, which supports the facade design pattern in a safer and more flexible way.
Syntax changes:
TemplateStat ::= ...
| Export
TopStat ::= ...
| Export
Export ::= ‘export’ [‘implied’] ImportExpr {‘,’ ImportExpr}