====== Lab 08: Polymorphism =======
===== 8.1. Companion objects and Option =====
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.
**8.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.
**8.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]] = ???
**8.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]] = ???
===== 8.2. Dictionaries =====
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)]) {
}
**8.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.
**8.2.2.** Create and implement member functions ''+'' (adds a new key-value pair to the Dictionary), ''contains'' and ''containsKey''. Use higher-order functions.
**8.2.3.** Implement the method ''get'' which retrieves a value iff it exists:
def get(key: K): Option[V] = ???
**8.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 = ???
**8.2.5.** Implement ''map'' as a member function for dictionaries. Think about how should ''map'' work over a dictionary.
**8.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!
**8.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.
===== 8.3. Polynomials =====
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])
**8.3.1.** Add a method ''*'' which multiplies the polynomial by a given coeficient:
def *(n: Int): Polynomial = ???
**8.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 = ???
**8.3.3.** Implement a method ''hasRoot'' which checks if a given integer is a root of the polynomial.
def hasRoot(r: Int): Boolean = ???
**8.3.4.** Implement a method ''degree'' which returns the degree of the polynomial.
def degree: Int = ???
**8.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 = ???
**8.2.6.** Implement a method ''*'' which multiplies polynomials. Hint: this operation is very similar to cartesian product.
def *(p2: Polynomial): Polynomial = ???