Lab 07: Polymorphism

Consider the following implementation of the type Nat, which you are well-familiar with:

trait Nat {
   def +(other: Nat): Nat
}
case object Zero extends Nat{
   override def +(other: Nat): Nat = other
}
case class Succ(n: Nat) extends Nat{
   override def +(other: Nat): Nat = Succ(n + other)
}

It might be the case that we would like to use nat values from different sources, e.g. fromInteger(1) + Zero + fromString(“2”). Keeping track of all different functions that convert other values to Nat might be tedious. A work-around is to: (1) define a companion object for Nat by creating a new object with the same name as Nat, in the same scala file (or worksheet).

object Nat { // a companion object for trait Nat 
 
}

We can now populate the companion objects with any methods we would like. In particular, we can add a method:

def apply(i: Int): Nat = ???

which can be called simply as: Nat(5). Also, we can (2) overload apply in order to support creating naturals with the call Nat(.) from different other types.

7.1.1. Create a companion object for Nat and add appropriate apply functions in order to make the expression Nat(1) + Zero + Nat(“2”) successfully compile and run.

7.1.2. Define a function that takes a list of integers and converts them to a list of type List[Option[Nat]]. If an integer x is positive, it should become a value Some(x). Otherwise it should become None.

def fromList(l: List[Integer]): List[Option[Nat]] = ???

7.1.3. Define a function which takes a list of Option[Nat] and converts it to Option[List[Nat]]. The logic here is that if the list contains at least one None value (hence some conversion from integer failed), then the result should be None . Otherwise, it should be a list containing all valid naturals:

def fromOptions(l: List[Option[Nat]]): Option[List[Nat]] = ???

Create a class Dictionary (which mimics Scala's Map), starting from the following template. A map takes as parameter a list of key-value pairs. (Other, more efficient implementations are possible).

class Dictionary[K,V](inner: List[(K,V)]) {
 
}

7.2.1. Create a companion object for Dictionary. Create an apply method which creates a dictionary from a list. For now, it's sole purpose is to hide from the programmer the usage of the keyword new when creating a new dictionary, but we will use the companion also later on.

7.2.2. Create and implement member functions + (adds a new key-value pair to the Dictionary), contains and containsKey. Use higher-order functions.

7.2.3. Implement the method get which retrieves a value iff it exists:

def get(key: K): Option[V] = ???

7.2.4. Implement the method getOrElse which returns a default value iff a key is non-existent in the map:

def getOrElse(default: V)(key: K): V = ??? 

7.2.5. Implement map as a member function for dictionaries. Think about how should map work over a dictionary.

7.2.6. In some situations, we would like to use maps that always return the same default value for a non-existent key. For this reason, implement:

def withDefaultValue(default: V): Dictionary[K,V] = ???

In order to implement it, you need to create a new type of dictionary that stores a default value. The easiest way to implement this, is to modify the class Dictionary as follows:

class Dictionary[K,V](inner: List[(K,V)], default: Option[V]) {
 
}

if default is None, then the dictionary behaves exactly as before. On the other hand, if default is Value(v), then the method get will return the default value. Careful: The function map should also modify the default value!

7.2.7. Modify the companion object of Dictionary such that the class constructor is concealed: define two apply methods, one for no default value, and one for dictionaries with a given default value.

In this section, we can resort to the datatype Map instead of your previously-defined Dictionary.

Consider a polynomial encoded as a map, where each present key denotes a power of X, and a value denotes its coefficient.

Map(2 -> 2, 0 -> 3) // encodes 2*X^2 + 3 

Add to your worksheet, the definition below:

case class Polynomial (nonZeroTerms: Map[Int,Int]) 

7.3.1. Add a method * which multiplies the polynomial by a given coeficient:

def *(n: Int): Polynomial = ???

7.3.2. Override the toString method to nicely display a polynomial. The output should be ordered from the highest-ranking polynomial, and terms with the zero-coeficient should not be displayed.

override def toString: String = ???

7.3.3. Implement a method hasRoot which checks if a given integer is a root of the polynomial.

def hasRoot(r: Int): Boolean = ???

7.3.4. Implement a method degree which returns the degree of the polynomial. def degree: Int = ??? </code>

7.2.5. Implement a method + which adds a given polynomial to this one. Hint - you can use zip, but other efficient options are possible.

def +(p2: Polynomial): Polynomial = ???

7.2.6. Implement a method * which multiplies polynomials. Hint: this operation is very similar to cartesian product.

def *(p2: Polynomial): Polynomial = ???