ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 02.01.2026

Просмотров: 3443

Скачиваний: 0

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

Section 20.6

Chapter 20 · Abstract Members

459

20.6 Abstract types

In the beginning of this chapter, you saw, “type T”, an abstract type declaration. The rest of this chapter discusses what such an abstract type declaration means and what it’s good for. Like all other abstract declarations, an abstract type declaration is a placeholder for something that will be defined concretely in subclasses. In this case, it is a type that will be defined further down the class hierarchy. So T above refers to a type that is at yet unknown at the point where it is declared. Different subclasses can provide different realizations of T.

Here is a well-known example where abstract types show up naturally. Suppose you are given the task of modeling the eating habits of animals. You might start with a class Food and a class Animal with an eat method:

class Food

abstract class Animal { def eat(food: Food)

}

You might then attempt to specialize these two classes to a class of Cows that eat Grass:

class Grass extends Food class Cow extends Animal {

override def eat(food: Grass) {} // This won’t compile

}

However, if you tried to compile the new classes, you’d get the following compilation errors:

BuggyAnimals.scala:7: error: class Cow needs to be abstract, since method eat in class Animal of type

(Food)Unit is not defined class Cow extends Animal {

ˆ

BuggyAnimals.scala:8: error: method eat overrides nothing override def eat(food: Grass) {}

ˆ

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index

Section 20.6

Chapter 20 · Abstract Members

460

What happened is that the eat method in class Cow does not override the eat method in class Animal, because its parameter type is different—it’s Grass in class Cow vs. Food in class Animal.

Some people have argued that the type system is unnecessarily strict in refusing these classes. They have said that it should be OK to specialize a parameter of a method in a subclass. However, if the classes were allowed as written, you could get yourself in unsafe situations very quickly. For instance, the following script would pass the type checker:

class

Food

 

abstract class Animal {

 

def

eat(food: Food)

 

}

 

 

class

Grass extends Food

 

class

Cow extends Animal {

override def eat(food: Grass) {} // This won’t compile,

}

 

// but if it did,...

class

Fish extends Food

 

val bessy: Animal = new Cow

bessy

eat (new Fish)

// ...you could feed fish to cows.

The program would compile if the restriction were eased, because Cows are Animals and Animals do have an eat method that accepts any kind of Food, including Fish. But surely it would do a cow no good to eat a fish!

What you need to do instead is apply some more precise modeling. Animals do eat Food, but what kind of Food each Animal eats depends on the Animal. This can be neatly expressed with an abstract type, as shown in Listing 20.9:

class Food

abstract class Animal { type SuitableFood <: Food

def eat(food: SuitableFood)

}

Listing 20.9 · Modeling suitable food with an abstract type.

With the new class definition, an Animal can eat only food that’s suitable. What food is suitable cannot be determined at the level of the Animal class.

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 20.7

Chapter 20 · Abstract Members

461

That’s why SuitableFood is modeled as an abstract type. The type has an upper bound, Food, which is expressed by the “<: Food” clause. This means that any concrete instantiation of SuitableFood (in a subclass of Animal) must be a subclass of Food. For example, you would not be able to instantiate

SuitableFood with class IOException.

class Grass extends Food class Cow extends Animal {

type SuitableFood = Grass override def eat(food: Grass) {}

}

Listing 20.10 · Implementing an abstract type in a subclass.

With Animal defined, you can now progress to cows, as shown in Listing 20.10. Class Cow fixes its SuitableFood to be Grass and also defines a concrete eat method for this kind of food. These new class definitions compile without errors. If you tried to run the “cows-that-eat-fish” counterexample with the new class definitions, you would get the following compiler error:

scala> class Fish extends Food defined class Fish

scala> val bessy: Animal = new Cow bessy: Animal = Cow@2e3919

scala> bessy eat (new Fish) <console>:12: error: type mismatch;

found : Fish

required: bessy.SuitableFood bessy eat (new Fish)

ˆ

20.7 Path-dependent types

Have a look at the last error message: What’s interesting about it is the type required by the eat method: bessy.SuitableFood. This type consists of an object reference, bessy, which is followed by a type field, SuitableFood,

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index

Section 20.7

Chapter 20 · Abstract Members

462

of the object. So this shows that objects in Scala can have types as members. The meaning of bessy.SuitableFood is “the type SuitableFood that is a member of the object referenced from bessy,” or alternatively, the type of food that’s suitable for bessy. A type like bessy.SuitableFood is called a path-dependent type. The word “path” here means a reference to an object. It could be a single name, such as bessy, or a longer access path, such as farm.barn.bessy.SuitableFood, where each of farm, barn, and bessy are variables (or singleton object names) that refer to objects.

As the term “path-dependent type” says, the type depends on the path: in general, different paths give rise to different types. For instance, say you defined classes DogFood and Dog, like this:

class DogFood extends Food class Dog extends Animal {

type SuitableFood = DogFood override def eat(food: DogFood) {}

}

If you attempted to feed a dog with food fit for a cow, your code would not compile:

scala> val bessy = new Cow bessy: Cow = Cow@e7bbeb

scala> val lassie = new Dog lassie: Dog = Dog@ce38f1

scala> lassie eat (new bessy.SuitableFood) <console>:14: error: type mismatch;

found : Grass required: DogFood

lassie eat (new bessy.SuitableFood)

ˆ

The problem here is that the type of the SuitableFood object passed to the eat method, bessy.SuitableFood, is incompatible with the parameter type of eat, lassie.SuitableFood. The case would be different for two Dogs however. Because Dog’s SuitableFood type is defined to be an alias for class DogFood, the SuitableFood types of two Dogs are in fact the same. As a result, the Dog instance named lassie could actually eat the suitable food of a different Dog instance (which we’ll name bootsie):

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 20.7

Chapter 20 · Abstract Members

463

scala> val bootsie = new Dog bootsie: Dog = Dog@66db21

scala> lassie eat (new bootsie.SuitableFood)

A path-dependent type resembles the syntax for an inner class type in Java, but there is a crucial difference: a path-dependent type names an outer object, whereas an inner class type names an outer class. Java-style inner class types can also be expressed in Scala, but they are written differently. Consider these two classes, Outer and Inner:

class Outer { class Inner

}

In Scala, the inner class is addressed using the expression Outer#Inner instead of Java’s Outer.Inner. The ‘.’ syntax is reserved for objects. For example, imagine you instantiate two objects of type Outer, like this:

val o1 = new Outer val o2 = new Outer

Here o1.Inner and o2.Inner are two path-dependent types (and they are different types). Both of these types conform to (are subtypes of) the more general type Outer#Inner, which represents the Inner class with an arbitrary outer object of type Outer. By contrast, type o1.Inner refers to the Inner class with a specific outer object (the one referenced from o1). Likewise, type o2.Inner refers to the Inner class with a different, specific outer object (the one referenced from o2).

In Scala, as in Java, inner class instances hold a reference to an enclosing outer class instance. This allows an inner class, for example, to access members of its outer class. Thus you can’t instantiate an inner class without in some way specifying an outer class instance. One way to do this is to instantiate the inner class inside the body of the outer class. In this case, the current outer class instance (referenced from this) will be used. Another way is to use a path-dependent type. For example, because the type, o1.Inner, names a specific outer object, you can instantiate it:

scala> new o1.Inner

res11: o1.Inner = Outer$Inner@1df6ed6

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 20.8

Chapter 20 · Abstract Members

464

The resulting inner object will contain a reference to its outer object, the object referenced from o1. By contrast, because the type Outer#Inner does not name any specific instance of Outer, you can’t create an instance of it:

scala> new Outer#Inner

<console>:7: error: Outer is not a legal prefix for a constructor

new Outer#Inner

ˆ

20.8 Structural subtyping

When a class inherits from another, the first class is said to be a nominal subtype of the other one. It’s a nominal subtype because each type has a name, and the names are explicitly declared to have a subtyping relationship. Scala additionally supports structural subtyping, where you get a subtyping relationship simply because two types have the same members. To get structural subtyping in Scala, use Scala’s refinement types.

Nominal subtyping is usually more convenient, so you should try nominal types first with any new design. A name is a single short identifier and thus is more concise than an explicit listing of member types. Further, structural subtyping is often more flexible than you want. A widget can draw(), and a Western cowboy can draw(), but they aren’t really substitutable. You’d typically prefer to get a compilation error if you tried to substitute a cowboy for a widget.

Nonetheless, structural subtyping has its own advantages. One is that sometimes there really is no more to a type than its members. For example, suppose you want to define a Pasture class that can contain animals that eat grass. One option would be to define a trait AnimalThatEatsGrass and mix it into every class where it applies. It would be verbose, however. Class Cow has already declared that it’s an animal and that it eats grass, and now it would have to declare that it is also an animal-that-eats-grass.

Instead of defining AnimalThatEatsGrass, you can use a refinement type. Simply write the base type, Animal, followed by a sequence of members listed in curly braces. The members in the curly braces further specify— or refine, if you will—the types of members from the base class. Here is how you write the type, “animal that eats grass”:

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index

Section 20.8

Chapter 20 · Abstract Members

465

Animal { type SuitableFood = Grass }

Given this type, you can now write the pasture class like this:

class Pasture {

var animals: List[Animal { type SuitableFood = Grass }] = Nil

// ...

}

Another place structural subtyping is helpful is if you want to group together a number of classes that were written by someone else. For example, suppose you want to generalize the loan pattern example from Section 9.4. The original example worked only for type PrintWriter, and you might want to have it work for any type with a close method. That is, one caller might use the routine to clean up an open file:

using(new PrintWriter("date.txt")) { writer => writer.println(new Date)

}

Another caller, meanwhile, might want to clean up an open socket:

using(serverSocket.accept()) { socket => socket.getOutputStream().write("hello, world\n".getBytes)

}

Implementing using is mostly straightforward. The method performs an operation and then closes an object, so it must take two arguments: the operation and the object. The operation is a function from any type to any other type, so using must have two type parameters as well. Here is a first try at implementing this method:

def using[T, S](obj: T)(operation: T => S) = { val result = operation(obj)

obj.close() // type error! result

}

This attempt almost works, but it will get a type error where close() is called. The problem is that, as written, T can be any type at all. To indicate that it only really supports types with close() methods, the <: notation can

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index