18. Advanced Types - RobertMakyla/scalaWiki GitHub Wiki
KEY POINTS: Singleton types are useful for method chaining and methods with object parameters. A type projection includes inner class instances for all objects of an outer class. A type alias gives a short name for a type. Structural types are equivalent to 'duck typing.' Existential types provide the formalism for wildcard parameters of generic types. Use a self type declaration to indicate that a trait requires another type. The 'cake pattern' uses self types to implement a dependency injection mechanism. An abstract type must be made concrete in a subclass. A higher-kinded type has a type parameter that is itself a parameterized type.
Singleton Types - used in Fluent Interface
given any reference 'ref' I can call ref.type which has value: ref or null
ref.type is a singleton type
usage: Fluent interfaces:
object Title
object Price
class Book (var title:Title, var price:Price){
private var useNextArgAs: Any = null
def set(obj: Title.type): this.type = { useNextArgAs = obj; this } // param: object type
def set(obj: Price.type): this.type = { useNextArgAs = obj; this } // param: object type
def to(arg: String) { if (useNextArgAs == Title) title = arg; if (useNextArgAs == Price) price = arg }
}
book.set(Title).to("Scala for the Impatient")
book set Title to "Scala for the Impatient" // fluent interface
Note: Unattended test - maybe this could be my multiplier - with fluent interface
Type Projections
class A {
class B
def f(b: B) = println("Got my B!")
}
val a1 = new A
val a2 = new A
a2.f(a1.B) // ERROR type mismatch;
// found : a1.B
// required: a2.B
a1.B and a2.B are different classes !!!
Now, # makes it possible for you to refer to such nested classes without restricting it to a particular instance
A#B // type projection - a B of any instance of A
def g(b: A#B) = println("Got some B.")
a2.g(a1.B) // no error with such function
// --> Got some B.
*Compiler changes a1.B to a1.type#B (B is a member of a1 singleton type)
Caution:
var a1 = new A
val b1 = new a1.B // error - myA not stable as it's var
Type Aliases
class Book {
type BookIndex = Map[String, (Int, Int)] // 'BookIndex' is alias of Map[...]
}
Type Alias - must be nested in class or object
Structural Types !!!
def appendLines(target: { def append(str: String): String }, lines: Iterable[String]) {
for (l <- lines) { target.append(""+l+". "); target.append("\n") }
}
To call appendLines method, I must provide as 1st parameter an instance of any class that has append() method
This is more flexible than defining a Appendable trait
Compound Types
T1 with T2 with T3 ...
In order to belong to the compound type, a value must belong to all of the individual types
e.g.:
val image = new Array[java.awt.Shape with java.io.Serializable]
In order to put objects into this array, it must be BOTH Shape and Serializable
Here, it must be Shape, Serializable and have contains method (Structural Type) :
val image = new Array[Shape with Serializable { def contains(p: Point): Boolean } ]
Technically:
T1 is shortcut from T1 {}
T1 with T2 with T3 is shortcut from T1 with T2 with T3 {}
{ def append(str: String): Any } is shortcut from AnyRef { def append(str: String): Any }
Infix Types
It's a type with two type parameters, written in 'infix' syntax
String Map Int is infix from Map[String, Int]
I can define mathematical notation (AxB)
class MyOperator[T, U]
new (Int MyOperator Int) is infix from new MyOperator[Int,Int]
Existential Types
Array[_ <: JComponent] is the same as Array[T] forSome {type T <: JComponent}
Array[_] is the same as Array[T] for some {type T}
Map[_, _] is the same as Map[T, U] for some {type T, type U}
Self Types
Trait can be mixed only with subtypes of given type
Class can be inherited only from subtypes of given type
this: T =>
this: T with U with ... =>
eg:
trait LoggedException {
this: Exception =>
def log() { getMessage() }
// OK to call getMessage cause this is an Exception
}
But self types don't inherit automatically
trait OtherException extends LoggedException { ... }
// error that OtherException don't support Exception
Need to repeat self type
trait OtherException extends LoggedException {
this: Exception =>
...
}
Dependency Injection (in Java there is Spring for that)
Eg: Switching between Mock database / Real database
trait Logger { def log(msg: String) } // logging
trait Auth { // authentication needs logging
this: Logger =>
def login(id: String, password: String): Boolean
}
trait App { // app needs authentication
this: Logger with Auth =>
...
}
object MyApp extends App with FileLogger("test.log") with MockAuth("users.txt")
As it's a bit awkward, Better use CAKE PATTERN:
You supply a component trait for each service that contains
- Any dependent components, expressed as self types
- A trait describing the service interface
- An abstract val that will be instantiated with an instance of the service
- Optionally, implementations of the service interface
trait LoggerComponent { // logging component
trait Logger { }
val logger: Logger
class FileLogger(file: String) extends Logger { }
class ConsoleLogger(file: String) extends Logger { }
}
trait AuthComponent {
this: LoggerComponent => // access to logger component
trait Auth { }
val auth: Auth
class ActiveDirectoryAuth(file: String) extends Auth { }
class MockAuth(file: String) extends Auth { }
}
object AppComponents extends LoggerComponent with AuthComponent {
val logger = new FileLogger("test.log")
val auth = new MockAuth("users.txt")
}
It's better than Spring (XML config) - here compiler verifies if dependencies are ok
Abstract Types
A class or trait can define an abstract type that becomes concrete in a subclass
trait Reader {
type Contents //abstract type
def read(fileName: String): Contents
}
class StringReader extends Reader {
type Contents = String
def read(fileName: String) = Source.fromFile(fileName, "UTF-8").mkString
}
class ImageReader extends Reader {
type Contents = BufferedImage
def read(fileName: String) = ImageIO.read(new File(fileName))
}
It could be done also with type parameters (generic types) :
trait Reader[C] {
def read(fileName: String): C
}
class StringReader extends Reader[String] {
def read(fileName: String) = Source.fromFile(fileName, "UTF-8").mkString
}
class ImageReader extends Reader[BufferedImage] {
def read(fileName: String) = ImageIO.read(new File(fileName))
}
Use abstract types when the types are expected to be supplied in a subclass (our case)
Use type parameters when the types are supplied as the class is instantiated
Family Polymorphism
Java approach (Observer design pattern)
trait Listener[E] { // listener on E (event)
def occurred(e: E): Unit
}
trait Source[E, L <: Listener[E]] { // collection of listeners
private val listeners = new ArrayBuffer[L]
def add(l: L) { listeners += l }
def fire(e: E) {
for (l <- listeners) l.occurred(e)
}
}
trait ActionListener extends Listener[ActionEvent] // listener on ActionEvent
class Button extends Source[ActionEvent, ActionListener] { // application launcher
def click() {
fire(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "click"))
}
}
Scala approach:
trait ListenerSupport {
type S <: Source // abstract types
type E <: Event
type L <: Listener
trait Event {
var source: S = _
}
trait Listener {
def occurred(e: E): Unit
}
trait Source {
this: S =>
private val listeners = new ArrayBuffer[L]
def add(l: L) { listeners += l }
def remove(l: L) { listeners -= l }
def fire(e: E) {
e.source = this
for (l <- listeners) l.occurred(e)
}
}
}
object ButtonModule extends ListenerSupport {
type S = Button // declaring abstract types
type E = ButtonEvent
type L = ButtonListener
class ButtonEvent extends Event
trait ButtonListener extends Listener
class Button extends Source {
def click() { fire(new ButtonEvent) }
}
}
Higher-Kinded Types
when 'List' depends on a type T, this fact produces another type 'List[T]'
We say that List is a type constructor
How about taking it to another level?
trait Iterable[E, C[_]] { // higher kinded type takes, C depending on any type
def iterator(): Iterator[E]
def build[F](): C[F]
def map[F](f : (E) => F) : C[F]
}
class MyIterator extends Iterable[Int, ArrayList]