Path-dependent types

Until now, we have avoided talking about paths, mostly because they are not types themselves. However, they can be a part of named types, and thus have an important role in Scala's type system.

A path can have one of the following forms:

  • An empty path, denoted with ε. It cannot be written directly, but implicitly precedes any other path.
  • C.this, where C is a reference class. This is the path that is constructed if this is used inside of a class.
  • C.super.x. or C.super[P]. refers to the member x of the superclass or designated parent class, P of C. It plays the same role as this for the class, but refers to the classes that are upper in the hierarchy.
  • p.x, where p is a path and x is a stable member of p. The stable member is an object definition or a value definition for which it is possible for the compiler to tell that it will always be accessible (as opposed to the volatile type where it is not possible, for example, there is an abstract type definition that can be overridden by a subclass).

Types within the path can be referred to by two operators, # (hash) and . (dot). The former is known as type projection, and T#m refers to the type member m of the type T. We can demonstrate the difference between these operators by building a type-safe lock:

case class Lock() {
final case class Key()
def open(key: Key): Lock = this
def close(key: Key): Lock = this
def openWithMaster(key: Lock#Key): Lock = this
def makeKey: Key = new Key
def makeMasterKey: Lock#Key = new Key
}

Here, we defined a type, Lock, with a nested type, Key. The key can be referenced using its path, Lock.Key, or by using a projection, Lock#Key. The former denotes a type tied to a specific instance, and the latter denotes a type that is not. The specific types of key are returned by two different constructor methods. The makeKey return type is a Key that is a shortcut for this.Key, which in turn is an alias for Lock.this.type#Key and represents a path-dependent type. The latter is just a type projection, Lock#Key. Because the path-dependent type refers to the concrete instance, the compiler will only allow the use of the appropriate type to call the open and close methods:

val blue: Lock = Lock()
val red: Lock = Lock()
val blueKey: blue.Key = blue.makeKey
val anotherBlueKey: blue.Key = blue.makeKey
val redKey: red.Key = red.makeKey

blue.open(blueKey)
blue.open(anotherBlueKey)
blue.open(redKey) // compile error
red.open(blueKey) // compile error

The masterKey is not path-dependent, and so can be used to call methods on any instance in a typical way:

val masterKey: Lock#Key = red.makeMasterKey

blue.openWithMaster(masterKey)
red.openWithMaster(masterKey)

These path-dependent types conclude our journey regarding concrete types and can be used to describe values. All of the types we've seen so far (except method types) are named value types to reflect this fact. A named value type is called a type designator. All type designators are shorthand for type projections. 

We will now switch gears and inspect how types can be used to narrate definitions of other types.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.245.233