====== 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 = ???