Showing posts with label Covariance. Show all posts
Showing posts with label Covariance. Show all posts

Wednesday, December 11, 2019

Variance Annotation in Scala 2.x

Target audience: Intermediate
Estimated reading time: 3'

The purpose of this post, is to introduce the basics of parameterized classes, upper and lower bounds and variance sub-typing in Scala.
Table of contents
Overview
Follow me on LinkedIn

Note: The code associated with this article is written using Scala 2.12.4

Overview

Scala is a statically typed language similar to Java and C++. Most of those concepts have been already introduced and commonly in Java with generics and type parameters wildcard. However Scala added some useful innovation regarding sub-typing covariance and contra-variance. API designers and library developers are facing the dilemma of choosing between parameterized types and abstract types and which parameters should be invariant, covariant or contra-variant.


Sub-classing

Like in Java, generic or parameterized type have been introduced to overcome the limitation of polymorphism. Moreover, understanding of concept behind the Scala type system help developers decipher and fix the occasional compilation failure related to typing.
Let's consider a simple collection class ,Container that adds and retrieves an array of items of type Item as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Item
case class SubItem() extends Item
 
class Container {
  def add(items: Array[Item]) : Unit =  items.foreach( add(_) )

  def retrieve(items: Array[Item]) : Unit =
    Range(0, items.size).foreach(items.update(_, retrieve))

  def add(item: Item) : Unit = { }
  def retrieve : Item = new SubItem
}

The client code can invoke the add (lines 5 & 10) and retrieve (lines 7 & 11) methods passing an array of elements of type Item. However passing an array of type SubItem inherited from Item (line 2) will generate a compiling error!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
val subItemsContainer = new Container
val array = Array[SubItem](new SubItem())
 
  // Compile error:
  // "type mismatch;  found   : Array[SubItem]  
  // required: Array[Item] 
  // Note: SubItem <: Item, but class Array is 
  // invariant in type T..."
 subItemsContainer.add(array)
 subItemsContainer.retrieve(Array[SubItem](new SubItem))


The Scala compiler throws a compiling error because the class Container does not have the adequate information about the sub type Item into SubItem in the add method (line 9). The invocation of the retrieve method, passing an argument of type SubItem generates also a compiler error. 
Scala provides developers with upper bounds and lower bounds sub-typing which is quite similar to the type variance available in Java.

Variance to the Rescue

The type T upper bounded by the type Item as defined by the following notation (line 1). T <: Item can be processed as a 'producer' of type Item. The notation provides the Scala compiler with any type inheriting Item. It should be allowed to be passed as argument of the add method (lines 2 & 7).
Similarly, the type U with a lower bound type SubItem
   U >: SubItem
can be processed as a 'consumer' operation of type SubItem, in the retrieve method (lines 4 & 8).

1
2
3
4
5
6
7
8
9
class Container[T <: Item] {
   def add(items: Array[T]) : Unit = items.foreach( add(_) )
      
   def retrieve[U >: SubItem](items: Array[U]) : Unit = 
       Range(0, items.size)foreach(items.update(_, retrieve))
  
   def add(item: T): Unit = {}
   def retrieve: U = new SubItem
}

Container[T] is a covariant class for the type Item as argument of the method add. The method retrieve[U] is contra-variant for the type SubItem.
This time around the compiler has the all the necessary information to subtype Item and super type SubItem, therefore the following code snippet won't generate a compiling error

val itemsContainer = new ContainerT[SubItem]
itemsContainer.add(Array[SubItem](new SubItem)))
itemsContainer.retrieve[Item](Array[Item](new SubItem))


Scala has a dedicated annotation for unvariance 'T', covariance '+T' and contra-variance '-T'. Using this annotation the container class can be expressed as follow.

1
2
3
4
5
6
7
8
9
class Container[+T] {
   def add(items: Array[T]) : Unit = items.foreach( add(_) )
 
   def retrieve[-U](items: Array[U]) : Unit = 
          Range(0, items.size).foreach(items.update(_, retrieve))
  
   def add(item: T) : Unit = { }
  
   def retrieve : U = new SubItem
}

In a nutshell, variances can be represented as functors:
Covariance: if (B inherit A) then (Container[B] inherit Container[A])
Contra-variance: if( B inherit A) then (Container[A] super class of Container[B])
Univariance: No rule applies.
 

The sub-typing rules above can be represented graphically 


Thank you for reading this article. For more information ...

References



---------------------------
Patrick Nicolas has over 25 years of experience in software and data engineering, architecture design and end-to-end deployment and support with extensive knowledge in machine learning. 
He has been director of data engineering at Aideo Technologies since 2017 and he is the author of "Scala for Machine Learning" Packt Publishing ISBN 978-1-78712-238-3

Sunday, June 11, 2017

Immutability & Covariance in Scala 2.x

Target audience: Beginner
Estimated reading time: 4'

This posts illustrates the concept of immutable and covariant containers/collections in Scala in the case of the stack data structure. 

Note: This article uses Scala 2.11.6

Overview

There is a relation between immutability and covariance which may not be apparent to a novice Scala programmer. Let's consider the case of a mutable and immutable implementation of a stack. The mutable stack is a container of elements with method to push element into (pop the last element from) the stack.

class MutableStack[T]  {
  private[this] val _stack = new ListBuffer[T]
  
  final def pop: Option[T]= 
    if(_stack.isEmpty) 
      None 
    else 
      Some(_stack.remove(_stack.size-1))
  
   def push(t: T): Unit = _stack.append(t)
}

The internal container is defined as a ListBuffer instance. The elements are appended to the list buffer (push) and the method pop pops the last elements pushed onto the stack.
This implementation has a major inconvenient: It cannot accept elements of type other than T because ListBuffer is a invariant collection. Let's consider then a immutable stack

Immutability and covariance

An covariant immutable stack cannot access its elements unless its elements are contained by itself. This feat is accomplish by breaking down the stack recursively as the last element pushed into the stack and the previous state of the stack.

class ImmutableStack[+T](
   val t: T, 
   val stack: Option[ImmutableStack[T]]) {

  def this(t: T) = this(t, Some(new EmptyImmutableStack(t)))
  ...
}

In this recursive approach the immutable stack is initialized with a single element of type T and the option of the existing immutable stack. The stack can be defined as reusable with covariance because elements are managed by the stack itself stack.
The next step is to define the initial state of the stack. We could have chosen a singleton empty stack with no elements. Instead, we define the first state of the immutable stack as:

 
class EmptyImmutableStack[+T](t: T) extends ImmutableStack[T](t, None) 
 

Next let's define the pop and push operators for ImmutableStack. The pop method return the previous state of the immutable stack that is next to last element pushed into the stack. The push method is contra-variant as its push an element of super type of T. The existing state this stack is added as the previous (2nd argument) state.

final def pop: Option[ImmutableStack[T]] = 
      stack.map(sk => new ImmutableStack[T](sk.t, sk.stack))

def push[U >: T](u: U): ImmutableStack[U] = new ImmutableStack[U](u, Some(this))


Tail recursion

The next step is to traverse the entire stack and return a list of all its element. This is accomplished through a tail recursion on the state of the stack

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def popAll[U >: T]: List[U] = pop(this,List[U]())
 
@scala.annotation.tailrec
private def pop[U >: T](
    _stck: ImmutableStack[U], 
    xs: List[U]
): List[U] = _stck match { 
  
  case st: EmptyImmutableStack[T] => xs.reverse
  case st: ImmutableStack[T] => {
     val newStack = _stck.stack.getOrElse(
        new EmptyImmutableStack[T](_stck.t)
     )
     pop(newStack, _stck.t :: xs)
  }
}

The recursion call pop (line 4) updates the list xs (line 6) and exists when the ImmutableStack is empty of type EmptyImmutableStack (line 9). The list has to be reversed to index the list elements from the last to the first (line 9). As long as the stack is not empty (or type ImmutableStack) the method recurses (line 14).
It is time to test drive this immutable stack.

 
val intStack = new ImmutableStack[Int](4)
val newStack = intStack.push(56).push(14).push(77)
 
println(newStack.popAll.mkString(", "))

The values in the stack are: 77, 14, 56, 4.
This examples illustrates the concept of immutable, covariant stack by using the instance of the stack has its state (current list of elements it contains).

References