Table of Contents
This is an excerpt from the 1st Edition of the Scala Cookbook (partially modified for the internet). This is Recipe 19.6, “How to define a Scala collection type whose elements are all of some base type.”
Problem
You want to specify that a Scala class (or method) takes a generic type parameter, and that parameter is limited so it can only be that base type or a sub-type of that base type.
Solution
Define the Scala class or method by specifying the type parameter with an upper bound. To demonstrate this, create a simple type hierarchy:
trait CrewMember class Officer extends CrewMember class RedShirt extends CrewMember trait Captain trait FirstOfficer trait ShipsDoctor trait StarfleetTrained
Then create a few instances:
val kirk = new Officer with Captain val spock = new Officer with FirstOfficer val bones = new Officer with ShipsDoctor
Given this setup, imagine that you want to create a collection of officers on a ship, like this:
val officers = new Crew[Officer]() officers += kirk officers += spock officers += bones
The first line lets you create officers
as a collection that can only contain types that are an Officer
, or subtype of an Officer
.
In this example, those who are of type RedShirt
won’t be allowed in the collection, because they don’t extend Officer
:
val redShirt = new RedShirt officers += redShirt // ERROR: this won't compile
To enable this functionality and let Crew
control which types are added to it, define it with an upper bound while extending ArrayBuffer
:
class Crew[A <: CrewMember] extends ArrayBuffer[A]
This states that any instance of Crew
can only ever have elements that are of type CrewMember
. In this example, this lets you define officers
as a collection of Officer
, like this:
val officers = new Crew[Officer]()
It also prevents you from writing code like this, because String
does not extend CrewMember
:
// error: won't compile val officers = new Crew[String]()
In addition to creating a collection of officers
, you can create a collection of RedShirts
, if desired:
val redshirts = new Crew[RedShirt]()
(I don’t know the names of any redshirts, otherwise I’d add a few to this collection.)
Typically you’ll define a class like Crew
so you can create specific instances as shown.
You’ll also typically add methods to a class like Crew
that are specific to the type (CrewMember
, in this case). By controlling what types are added to Crew
, you can be assured that your methods will work as desired. For instance, Crew
could have methods like beamUp
, beamDown
, goWhereNoOneElseHasGone
, etc. — any method that makes sense for a CrewMember
.
Discussion
This type is referred to as a bound, specifically an upper bound.
If you’re working with an implicit conversion, you’ll want to use a view bound instead of an upper bound. To do this, use the
<%
symbol instead of the<:
symbol.
You can use the same technique when you need to limit your class to take a type that extends multiple traits. For example, to create a Crew
that only allows types that extend CrewMember
and StarfleetTrained
, declare the Crew
like this:
class Crew[A <: CrewMember with StarfleetTrained] extends ArrayBuffer[A]
If you adapt the officers to work with this new trait:
val kirk = new Officer with Captain with StarfleetTrained val spock = new Officer with FirstOfficer with StarfleetTrained val bones = new Officer with ShipsDoctor with StarfleetTrained
you can still construct a list of officers, with a slight change to the Crew
definition:
val officers = new Crew[Officer with StarfleetTrained]() officers += kirk officers += spock officers += bones
This approach works as long as the instances have those types somewhere in their lineage (class hierarchy). For instance, you can define a new StarfleetOfficer
like this:
class StarfleetOfficer extends Officer with StarfleetTrained
You could then define the kirk
instance like this:
val kirk = new StarfleetOfficer with Captain
With this definition, kirk
can still be added to the officers
collection; the instance still extends Officer
and StarfleetTrained
.
Methods
Methods can also take advantage of this syntax. For instance, you can add a little behavior to CrewMember
and RedShirt
:
trait CrewMember { def beamDown { println("beaming down") } } class RedShirt extends CrewMember { def putOnRedShirt { println("putting on my red shirt") } }
With this behavior, you can write methods to work specifically on their types. This method works for any CrewMember
:
def beamDown[A <: CrewMember](crewMember: Crew[A]) { crewMember.foreach(_.beamDown) }
But this method will only work for RedShirt
types:
def getReadyForDay[A <: RedShirt](redShirt: Crew[A]) { redShirt.foreach(_.putOnRedShirt) }
In both cases, you control which type can be passed into the method using an appropriate upper bound definition on the method’s type parameter.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
See Also
- Recipe 19.3, “Using Duck Typing (Structural Types)”
- Scala also includes a lower type bound, though it is used less frequently. A lower bound is briefly demonstrated in Recipe 19.8, “Building Functionality with Types”. The page titled “A Tour of Scala: Lower Type Bounds” also describes a situation where a lower type bound might be used.