Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Design Patterns Using Singleton Types in Scala

DZone's Guide to

Design Patterns Using Singleton Types in Scala

The fusion of functional and object-oriented programming in Scala leads to many opportunities for design patterns.

· Java Zone ·
Free Resource

Get the Edge with a Professional Java IDE. 30-day free trial.

Scala'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.

Get the Java IDE that understands code & makes developing enjoyable. Level up your code with IntelliJ IDEA. Download the free trial.

Topics:
scala ,design pattern ,singleton ,singelton pattern ,types ,singleton types ,java ,tutorial ,scala tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}