Summary
In this blog, I attempt to answer a common question among Scala programmers: when would I opt to use an abstract type member instead of a generic type parameter in Scala API design?
Advertisement
A common question asked by people learning Scala is how to decide between
abstract type members and generic type parameters when you need to model an abstract type. For those
unfamiliar with the difference, type parameters in Scala are similar to type parameters in Java, except instead of
Java's angle brackets:
interface Collection<T> {
// ...
}
Scala uses square brackets:
// Type parameter version
trait Collection[T] {
// ...
}
Abstract type members in Scala have no equivalent in Java. In both languages, classes, interfaces (in Java), and traits (in Scala) can have
methods and fields as members. In Scala, a class or trait can also have type members, and just as methods can be abstract in Java, methods
fields, and types can all be abstract in Scala. Here's how an abstract type member looks in Scala:
// Type member version
trait Collection {
type T
// ...
}
In both cases, the abstract type can be made concrete in a subtype. Here's a subtype of the type parameter version of Collection that
specifies the concrete type String for T:
// Type parameter version
trait StringCollection extends Collection[String] {
// ...
}
Here's a subtype of the type member version of Collection that also
specifies the concrete type String for T:
// Type member version
trait StringCollection extends Collection {
type T = String
// ...
}
This seems to be, and in fact is, two different ways to achieve the same goal. So when would you choose one over the other?
I asked Martin Odersky this question in an interview. He first
explained that one reason for having both abstract type members and generic type parameters is orthogonality:
There have always been two notions of abstraction: parameterization and abstract members. In Java you also have both, but it depends on what you are abstracting over. In Java you have abstract methods, but you can't pass a method as a parameter. You don't have abstract fields, but you can pass a value as a parameter. And similarly you don't have abstract type members, but you can specify a type as a parameter. So in Java you also have all three of these, but there's a distinction about what abstraction principle you can use for what kinds of things. And you could argue that this distinction is fairly arbitrary.
What we did in Scala was try to be more complete and orthogonal. We decided to have the same construction principles for all three sorts of members. So you can have abstract fields as well as value parameters. You can pass methods (or "functions") as parameters, or you can abstract over them. You can specify types as parameters, or you can abstract over them. And what we get conceptually is that we can model one in terms of the other. At least in principle, we can express every sort of parameterization as a form of object-oriented abstraction. So in a sense you could say Scala is a more orthogonal and complete language.
He also described a difference between abstract type members and generic type parameters that can show up in practice:
But in practice, when you [use type parameterization] with many different things, it leads to an explosion of parameters, and usually, what's more, in bounds of parameters. At the 1998 ECOOP, Kim Bruce, Phil Wadler, and I had a paper where we showed that as you increase the number of things you don't know, the typical program will grow quadratically. So there are very good reasons not to do parameters, but to have these abstract members, because they don't give you this quadratic blow up.
When he gave this answer, I wasn't sure I understood exactly what he was talking about, but I think I can now offer some more insight into this difference.
I have tended to use generic type parameters by default, perhaps because I come from a C++ and Java background and are more familiar with type
parameterization than type members. However, I have encountered one design problem
that I ended up solving with abstract type members, not generic type parameters.
The problem was that I wanted to provide traits in ScalaTest that allow users to write tests into which they can pass fixture objects.
This would give people the option of writing tests in a functional style instead of the traditional imperative style of JUnit's setUp
and tearDown methods. To provide this option, I needed to allow users to indicate the type of the fixture object by
either specifying a concrete type parameter or member. In other words, I'd either provide this trait in the ScalaTest API:
// Type parameter version
trait FixtureSuite[F] {
// ...
}
Or this one:
// Type member version
trait FixtureSuite {
type F
// ...
}
In either case, F would be the type of the fixture parameter to pass into the tests, which suite subclasses would make concrete.
Here's an example of a concrete suite of tests that needs a StringBuilder passed into each test, using the type parameter approach:
// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] {
// ...
}
And here's an example of a concrete suite of tests that needs a StringBuilder passed into each test using the abstract type member approach:
// Type member version
class MySuite extends FixtureSuite {
type F = StringBuilder
// ...
}
So far there's not much difference. However, one other use case I had is that I wanted to allow people to create traits that provide a concrete definition for the fixture type and could be mixed into suite classes. This would allow users to encode commonly used fixtures into helper traits that could be mixed
into any of their suite classes that need them. This is where a difference showed up. Here's how you'd write that trait using the generic type
parameter approach:
// Type parameter version
trait StringBuilderFixture { this: FixtureSuite[StringBuilder] =>
// ...
}
The "this: FixtureSuite[StringBuilder]" syntax is a self type, which indicates that trait StringBuilderFixture
can only be mixed into a FixtureSuite[StringBuilder]. By contrast, here's what this trait would look like when
taking the type member approach:
// Type member version
trait StringBuilderFixture { this: FixtureSuite =>
type F = StringBuilder
// ...
}
So far there's still only cosmetic differences, but the pain point of the type member approach is about to arise. As soon as a user tries
to mix the StringBuilderFixture into a suite class, you'll see a usability difference. In the type parameter approach, the user
must repeat the type parameter even though it is defined in the trait:
// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
// ...
}
But in the abstract type member approach, no such repetition is necessary:
// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
// ...
}
Because of this difference, I chose to model the fixture parameter type as an abstract type member, not a generic type parameter, in ScalaTest.
I named the type member FixtureParam, to make it more obvious to readers of the code what the type is used for.
(This feature is in ScalaTest 1.0. For the details, check out the scaladoc documentation of
trait FixtureSuite.) In a future
release of ScalaTest, I plan to add new traits that allow multiple fixture
objects to be passed into tests. For example, if you want to pass three different fixture objects into tests, you'll be able to do so, but
you'll need to specify three types, one for each parameter. Thus had I taken the
type parameter approach, your suite classes could have ended up looking like this:
// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
// ...
}
Whereas with the type member approach it will look like this:
// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
// ...
}
One other minor difference between abstract type members and generic type parameters is that when a generic type parameter is
specified, readers of the code do not see the name of the type parameter. Thus were someone to see this line of code:
// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
// ...
}
They wouldn't know what the name of the type parameter specified as StringBuilder was without looking it up.
Whereas the name of the type parameter is right there in the code in the abstract type member approach:
// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
type FixtureParam = StringBuilder
// ...
}
In the latter case, readers of the code could see that StringBuilder is the "fixture parameter" type. They still
would need to figure out what "fixture parameter" meant, but they could at least get the name of the type without looking
in the documentation. I think there will be design situations where the explicit type member name will be better, and situations where it is worse. I think having the FixtureParam name explicit in ScalaTest FixtureSuites helps the readability of the code. But I wouldn't want to have to do that for collections. I like saying new ListBuffer[String], and am glad I don't have to say new ListBuffer { type E = String }.
My observation so far about abstract type members is that they are primarily a better choice than generic type parameters when you want to let people mix in definitions of those types via traits. You may also want to consider using them when you think the explicit mention of the type member name when it is being defined will help code readability.
What do you think of this take on abstract type members in Scala? Do you have any other insights about the abstract-type-member-versus-generic-type-parameter question to share?
Not only complexity was reduced, but, more important, also expressiveness was added
With type members I could naturally constrain children of cats to be cats, while with type parameters I could only constrain children of cats to be animals (compiler complained when constraining to cats).
ps: the code on the blog is very old, and I was just starting to learn about Scala, maybe it can be improved.
> Bill, > > You have no choice but to use a type member if you want > the type to be bounded by an inner class/trait. > That's true and a very good point. I'll add that to my list of differences.
> They're also to be preferred if you have multiple types, > where you would otherwise need to know the correct order > of the type parameters. > Another good point. I was kind of going in that direction when talking about explicit name showing up in the code, but didn't quite get there. It is similar to the difference between regular and named value parameters in 2.8. You don't need to remember the order when you can specify "name=".
> These points I mention in section 3.2 of my paper > describing a Scala API for remote monitoring and > control[1]. > Thanks for your input.
It seems to me that the difference between parametrisation and type member may be more obvious with more parameters and more hierarchy steps.
Compare
trait Upper[S, T, U]
trait Middle[S, T, U] extends Upper[S, T, U]
trait Lower[S, U] extends Middle[S, String, U]
class MyClass[S] extends Lower[S, Int]
new MyClass[Boolean]
with
trait Upper { type S; type T; type U }
trait Middle extends Upper
trait Lower extends Middle { type T = String }
class MyClass[M] extends Lower { type S = M; type U = Int }
new MyClass[Boolean]
In the type parameter version you have to always carry the parameters around (like with method delegation).
In the abstract type member version the abstract types remain open until they are specified at one specific level (like with abstract methods in abstract classes).
And the type parameter approach comes into play on the API level: when the user of this type system instanciates MyClass. The parameter explicitly declares the contract.
So the decision between this two ways may perhaps be a result of the consideration of what of the types is "public" from a library point of view.
I'm writing on a master-thesis looking at Scala and DSL development. One DSL example I've been working on is a small numeric DSL for image processing. The two main datastructures here are Matrix and Image (an image is pretty much just a matrix with some extra operations). Both structures need some kind of abstract types. Matrix typically being a matrix of int or double/float, where a gray-scale-image typically will have a datatype of one int-matrix (or double-matrix) and an RGB-image typically three int-matrices..
The way I see it is that type parameters are the best option when you want the user to specify the type as some kind of external type, typically the Matrix in my example (with Int and Double):
class IntMatrix extends Matrix[Int]
class DoubleMatrix extends Matrix[Double]
Abstract types are very well suited for hidden internal types (like in your examples). I use this for my Image class:
trait Image {
type DataType
val data: DataType
...
}
class GrayScaleImage extends Image {
type DataType = Matrix[Int]
...
}
class RGBImage extends Image {
type DataType = (Matrix[Int],Matrix[Int],Matrix[Int])
...
}
Does this sound like a sensible separation? Or would I be better off just using type parameters or abstract types for both?
I really like the way abstract types totally hide the implementation specific details from the user of the classes.
> Abstract types are very well suited for hidden internal > types (like in your examples). I use this for my Image > class: >
trait Image {
> type DataType
> val data: DataType
> ...
> }
>
> class GrayScaleImage extends Image {
> type DataType = Matrix[Int]
> ...
> }
>
> class RGBImage extends Image {
> type DataType = (Matrix[Int],Matrix[Int],Matrix[Int])
> ...
> }
> Does this sound like a sensible separation? Or would I be > better off just using type parameters or abstract types > for both? > > I really like the way abstract types totally hide the > implementation specific details from the user of the > classes.
I also use something similar as you and it's centered around working with pixels (value+position) and mapping images into new ones (with a little bit of cheating by reusing objects). I'm not so concerned about hiding details. The program generates sourcecode instead of using DSL from little snippets of code. For example, inverting the colors in an image:
> Proof, yet again, that I am not smart enough to program in > Scala.
ja, as much as i liked SML more than Scheme, and i think Haskell is pretty nifty-keen, i do think you have a good point and that the typing in things like Scala is pretty off-putting. it is like there needs to be a meta-level of compiler/user interface that somehow helps generate the present-day-Scala-style code that is sort of an assembly code. or something. i mean, i still want to be able to get guarantees, just w/out so much overhead.
I think we really need an 'Effective Scala' book giving advice on which features to use for which problems (or patterns). You can do so much with a language like Scala that it's hard to see the wood for the trees.
The Programming in Scala book is a good start - the chapter on defining equals() is excellent IMHO. More like this would be great.
We need style advice too: there's a serious danger of each Scala programmer's style turning into his own personal DSL!
Generic-type parameters are a subset of abstract-type parameters, perhaps it would be clearer if generic-type parameters were presented as a short hand for common uses. Even better if the compiler issued the same code for both, currently it doesn't.