Design Patterns Using Singleton Types in Scala
The fusion of functional and object-oriented programming in Scala leads to many opportunities for design patterns.
Join the DZone community and get the full member experience.
Join For FreeScala's fusion of functional programming and object-oriented programming can lead to many design patterns for this language. The richness of Scala’s type system has also inspired many interesting design patterns to solve programming problems. The Singleton type is one of the types in Scala type family; it bridges the abstraction and its inhabitants — the values — making many design scenarios possible. In this article, we explore design problems and various design patterns in Scala in the presence of singleton types.
At the time of the writing, the latest Scala release is 2.12.7. Anything after this release is beyond our discussion.
Introduction
The Scala spec has a definition for singleton types:
A singleton type is of the form p.type, where p is a path pointing to a value expected to conform to scala.AnyRef
. The type denotes the set of values consisting of null and the value denoted by p.
In the following code snippet, stype
is a legitimate Singleton type, where s is the path to the value of String “hello."
scala> val s = "hello"
s: String = hello
scala> type stype = s.type
defined type alias stype
scala> val ss:stype = s
ss: stype = hello
A singleton type may have more than one instance, but these instances are references of the same value. Compiler reports error on the following code, as ss refers to a different value:
scala> val ss:stype = "world"
<console>:12: error: type mismatch;
found : String("world")
required: stype
(which expands to) s.type
val ss:stype = "world"
^
However, the instance of any singleton type can reference null:
scala> val ss:stype = null
ss: stype = null
An AnyVal
cannot have a singleton type. In the following code, v
is of type Int
, an AnyVal
.
scala> val v=3
v: Int = 3
scala> type vv = v.type
<console>:12: error: type mismatch;
found : v.type (with underlying type Int)
required: AnyRef
type vv = v.type
^
For the same reason, value objects, which inherit from AnyVal
, do not have singleton types:
scala> class Wrapper(val v:Int) extends AnyVal
defined class Wrapper
scala> val w = new Wrapper(3)
w: Wrapper = Wrapper@3
scala> type ww = w.type
<console>:12: error: type mismatch;
found : w.type (with underlying type Wrapper)
required: AnyRef
Note that Wrapper extends Any, not AnyRef.
Such types can participate in value classes, but instances
cannot appear in singleton types or in reference comparisons.
type ww = w.type
^
In the following sections, we will discuss how singleton types can help to build useful design patterns in Scala.
Singleton Pattern and Singleton types
Singleton pattern in OO paradigm is an embedded language feature in Scala — a top-level object is a singleton. It is closely related to singleton types, but they are not the same.
An object is a value. The definition of an object uses the keyword object:
object box
The type of an object is of singleton type, which can be obtained by its member type. For instance, to pass an object to a function:
def f(b: box.type)
Scala defines a Singleton trait, but it is not the same as a singleton type.Scala. Singleton is a type, while singleton type is of type Singleton and also is bounded to value denoted by the path. However, we can enforce an abstract type to be of singleton type (an object) using Singleton trait as an upper bound. For instance:
trait Log {
def log(m:String) = println(m)
}
object consoleLog extends Log
val fileLog = new Log{}
trait X {
type T <: Log with scala.Singleton //upper bound
val v:T
def process(msg:String) = {
v.log(msg)
}
}
An abstract type T is bounded to Log and Singleton. When we instantiate X instances, the compiler is at work:
val a = new X {
type T = consoleLog.type //this works
val v = consoleLog
}
val b = new X {
type T = Log //illegal
val v = fileLog
}
type T has incompatible type: type T = Log
Method Chaining Pattern
One form of singleton types is this.type
, which represents the object itself. It works nicely in a form ofobejct.method1.method2
to chain the methods together; when the class can be extended, a return type of this.type
tells the compiler that an object of the subclass is returned. This allows a uniform set of APIs that represent a sequence of functions being applied to the object. It can be also used to chain methods in mixin composition. For instance:
class A {
def f1:this.type = this
}
class B extends A {
def f2:this.type = this
}
trait C {
def f3:this.type = this
}
val abc = new B with C
abc.f1.f2.f3
Phantom Types Pattern
Phantom types in Scala are a type-checking mechanism without instantiating the types. As the type information is erased in runtime, this mechanism allows type errors being found at compiling time.
Singleton types not only conform to the type denoted by the path but also to the value of its inhabitant. Singleton types can type-check both conformances of a value parameter at compile time. This extends type-checking to values at compile time, which usually happens at runtime. This bridges a gap between the type and its instances during compile time. The following encoding is efficient in enforcing the relationship between the containers and its inhabitants:
trait Room {
def newInhabitant() = new Inhabitant[this.type]
def notify(inhabitant: Inhabitant[this.type]) = inhabitant.hello
}
class Inhabitant[M <: Room] {
def hello = println("hello")
}
object RoomA extends Room
object RoomB extends Room
RoomA.notify(RoomA.newInhabitant()) //works!
RoomB.notify(RoomA.newInhabitant()) //error! type mismatch!
The containers are able to notify instances of own inhabitants — those created by newInhabitant
, annotated using its own singleton type. The compiler type-checks these instances when the container tries to send a message to inhabitants, any type mismatch is captured during compiling time. The participation of singleton types in phantom types allows type system to type-check different instances of the same type and makes it more flexible to the encoding.
Pattern Matching and Singleton Types
In Scala, pattern matching takes various forms. A typed pattern x: T consists of a pattern variable x and a type pattern T. This pattern matches any value matched by the type pattern; it binds the variable name to that value.
The type can be of a singleton type p.type
. This type pattern matches only the value denoted by the path p (that is, a pattern match involved a comparison of the matched value with p using method eq in class AnyRef). AnyRef.eq
is a synthetic function to test whether two objects are references to each other, in addition to the type of equivalence relation. For instance:
class A (val value:Int)
val a = new A(1)
val b = new A(2)
val c = new A(2)
def m(x:A) = x match {
case _:a.type => println("match a")
case _:b.type => println("match b")
case _ => println("no match")
}
m(a) // match a
m(b) // match b
m(c) // no match
Although C satisfies type equivalence relation with b, pattern matching on singleton types is a no match. Again, singleton type bridges the abstraction (the types), instances (the values), and makes pattern matching more powerful.
Opinions expressed by DZone contributors are their own.
Trending
-
System Testing and Best Practices
-
How to Handle Secrets in Kubernetes
-
Integration Architecture Guiding Principles, A Reference
-
RBAC With API Gateway and Open Policy Agent (OPA)
Comments