This document discusses inheritance and polymorphism in Scala programming. It defines inheritance as deriving classes from existing classes and describes different types of inheritance like single, multiple, multilevel, hierarchical, and hybrid inheritance. It also discusses the difference between inheritance and composition. The document then defines polymorphism and describes different types like parametric, subtype, and ad-hoc polymorphism. It provides examples to illustrate single inheritance, multiple inheritance, multilevel inheritance, and subtype polymorphism in Scala.
1. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 1
Inheritance and Polymorphism
Inheritance: Defining derived classes & Visibility modes – Types of Inheritance: Single-
level Inheritance - Multilevel Inheritance - Multiple Inheritance - Hierarchical Inheritance
- Hybrid Inheritance – Inheritance vs Composition – Polymorphism: Types of
Polymorphism - Parametric Polymorphism - Subtype Polymorphism - Ad-Hoc
Polymorphism - Function Overloading - Operator Overloading.
Inheritance:
Inheritance in Scala is the concept of inheriting values and properties by one object from
another object.
Scala programming language is a programming language that integrates features of both object-
oriented programming and function programming supports inheritance too, as it is a really
important feature of OOP's concept.
Before going ahead, let's see some key terms related to inheritance in Scala,
Super Class is also known as base class or parent class. It is the class whose features are
inherited by other classes.
Sub Class is also known as child class, derived class, or extended class. It is the class that
inherits features from other classes.
To implement the concepts of inheritance we need to understand the working of some keywords
in Scala:
extends: The extends keyword in Scala is used to inherit features of one class by another
class.
baseClass extends parentClass
This extends keyword is used to inherit class while creating a new one.
With: the with keyword in Scala is used when we need to inherit more than one class by
another class.
Syntax
class SubClassName extends SuperClassName(){
/* Write your code
* methods and fields etc.
*/
}
2. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 2
Types of Inheritances in Scala
While inheriting classes in Scala, there can be multiple ways to inherit classes.
Here are types of inheritance in Scala:
1. Single Inheritance
2. Multiple Inheritance
3. Multilevel Inheritance
4. Hierarchical Inheritance
5. Hybrid Inheritance
1. Single Inheritance
3. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 3
Single inheritance is a type of inheritance in which one class is inherited by another class.
The concept is depicted in the following diagram,
Here, in single inheritance, one class is simply inherited by another class in Scala.
Example:
class class1{
var value1 = 935
}
class class2 extends class1{
val value2 = 54
def printValues(){
println("Value 1: " + value1 )
println("Value 2: " + value2 )
println("Sum: " + (value1 + value2))
}
}
object MyClass {
def main(args: Array[String]) {
val object1 = new class2();
object1.printValues()
}
}
Multiple Inheritance
Multiple inheritance is a type of inheritance in which multiple classes are inherited by one class
at a time.
4. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 4
Example:
trait baseClass1{
var value1 = 935
}
trait baseClass2{
var value2 = 43
}
class derivedClass extends baseClass1 with baseClass2{
def printValues(){
println("Derived Class")
println("Value 1 : " + value1 )
println("Value 2 : " + value2 )
println("Concatination : " + (value1 + value2) )
}
}
object MyClass {
def main(args: Array[String]) {
val object1 = new derivedClass();
5. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 5
object1.printValues()
}
}
Multilevel Inheritance
Multi-Level inheritance is a type of inheritance in which one class is inherited by another
class which is in turn inherited by the third class.
The concept is depicted in the following diagram,
Example:
class class1{
var value1 = 935
}
class class2 extends class1{
val value2 = 54
}
class class3 extends class2{
6. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 6
def printValues(){
println("Value 1: " + value1 )
println("Value 2: " + value2 )
println("Sum: " + (value1 + value2))
}
}
object MyClass {
def main(args: Array[String]) {
val object1 = new class3();
object1.printValues()
}
}
Hierarchical Inheritance
Hierarchical inheritance is a type of inheritance in which one class is inherited by more
than one class.
The concept is depicted in the following diagram,
Example:
class baseClass{
7. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 7
var value1 = 935
}
class derivedClass1 extends baseClass{
def printValues(){
println("nDerived Class 1")
val value2 = 54
println("Value 1: " + value1)
println("Value 2: " + value2)
println("Sum: " + (value1 + value2))
}
}
class derivedClass2 extends baseClass{
def printValues(){
val value2 = 341
println("nDerived Class 2")
println("Value 1 : " + value1 )
println("Value 2 : " + value2 )
println("Concatination : " + value1 + value2 )
}
}
object MyClass {
def main(args: Array[String]) {
8. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 8
val object1 = new derivedClass1();
object1.printValues()
val object2 = new derivedClass2();
object2.printValues()
}
}
Hybrid Inheritance
Hybrid Inheritance is a special type of inheritance which is a combination of hierarchical
and multiple inheritance in Scala.
class A {
var numA: Int = 0;
def setA(n: Int) {
numA = n;
}
def printA() {
printf("numA: %dn", numA);
}
}
class B extends A {
var numB: Int = 0;
def setB(n: Int) {
numB = n;
}
def printB() {
printf("numB: %dn", numB);
}
}
class C extends B {
var numC: Int = 0;
def setC(n: Int) {
numC = n;
9. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 9
}
def printC() {
printf("numC: %dn", numC);
}
}
class D extends A {
var numD: Int = 0;
def setD(n: Int) {
numD = n;
}
def printD() {
printf("numD: %dn", numD);
}
}
object Sample {
def main(args: Array[String]) {
var obj1 = new C();
var obj2 = new D();
obj1.setA(10);
obj1.setB(20);
obj1.setC(30);
obj1.printA();
obj1.printB();
obj1.printC();
obj2.setA(40);
obj2.setD(50);
obj2.printA();
obj2.printD();
}
}
Inheritance vs Composition:
Composition and inheritance are two ways to define a new class in terms of another existing
class. If what you're after is primarily code reuse, you should in general prefer composition to
inheritance. Only inheritance suffers from the fragile base class problem, in which you can
inadvertently break subclasses by changing a superclass.
10. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 10
One question you can ask yourself about an inheritance relationship is whether it models an is-a
relationship.[8] For example, it would be reasonable to say that ArrayElement is-an Element.
Another question you can ask is whether clients will want to use the subclass type as a
superclass type.[9] In the case of ArrayElement, we do indeed expect clients will want to use an
ArrayElement as an Element.
If you ask these questions about the inheritance relationships shown in Figure 10.3, do any of
the relationships seem suspicious? In particular, does it seem obvious to you that a LineElement
is-an ArrayElement? Do you think clients would ever need to use a LineElement as an
ArrayElement? In fact, we defined LineElement as a subclass of ArrayElement primarily to
reuse ArrayElement's definition of contents. Perhaps it would be better, therefore, to define
LineElement as a direct subclass of Element, like this:
class LineElement(s: String) extends Element {
val contents = Array(s)
override def width = s.length
override def height = 1
}
What is Inheritance?
Inheritance is one of the most powerful tools in implementing code reusability in object-
oriented programming. It refers to the functionality by which one object acquires the
characteristics of one or more other objects. Inheritance in C++ means you can create classes
that derive their attributes from existing classes. This means you specialize a class to create an
is-a relationship between the classes which results in a strong coupling between the base and
derived classes. Implementing inheritance promotes code reusability because new classes are
created from existing classes. Class inheritance also makes it easier to modify the
implementation being reused. But class inheritance has some downsides to it too. First, because
inheritance is defined at compile-time, you cannot change the implementations inherited from
parent classes at run-time.
What is Composition?
OOP provides yet another relationship between classes called composition, which is also known
as a has-a relationship. If the features of one object need to be a part of another object, the
relationship calls for composition. To compose a class from existing classes, an object of each
class should be declared as the member of the new class. In simple words, using an object
within another object is known as composition. Many a times, you might want to use an object
as a field within another class. You use an object inside a class in composition. Unlike class
11. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 11
inheritance, object composition is defined dynamically at run-time through objects acquiring
references to other objects. Additionally, composition provides a better way to use an object
without compromising the internal details of the object, that is where composition is useful.
Difference between Composition and Inheritance
Approach
While both inheritance and composition promote code reusability in object oriented system by
establishing relationships between classes and they provide equivalent functionality in many
ways, they use different approaches. With inheritance, you can create classes that derive their
attributes from existing classes so while using inheritance to create a class, you can expand on
an existing class. On the contrary, using an object within another object is known as
composition. Object composition is an alternative to class inheritance. If the features of one
object need to be a part of another object, the relationship calls for composition.
Relationship
In inheritance, you specialize a class to create an “is-a” relationship between the classes which
results in a strong coupling between the base and derived classes. It enables a hierarchy of
classes to be designed and the hierarchy begins with the most general class and moves to more
specific classes. By implementing inheritance, member functions from one class become
properties of another class without coding them explicitly within the class. In composition, you
use an object inside a class and any requests to the object are forwarded to the object. The
internal details are not exposed to each other in composition, so it is a “has-a” relationship.
Implementation
Class inheritance is defined at compile-time, so you cannot change the implementations
inherited from parent classes at run-time. Because inheritance exposes a subclass to details of its
parent’s implementation, it often breaks encapsulation. Any changes in the parent class will
reflect in the subclass which can create problems when you try to reuse a subclass. Object
composition, on the contrary, is defined dynamically at run-time through objects acquiring
references to other objects. And because objects are accessed solely through their interfaces, it
won’t break encapsulation. Any object can be replaced at run-time by another object as long as
it has the same type.
12. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 12
Polymorphism
In the simplest of terms, polymorphism means one object having many forms or an
object’s ability to exhibit multiple behaviors. In computer terms, this would show up as
handling different data types using the same interface or method.
The more formal definition is: polymorphism is the provision of a single interface to
entities of different types or the use of a single symbol to represent multiple different
types.
Polymorphism means that a function type comes "in many forms".
In programming it means that the function can be applied to arguments of many types, or
the type can have instances of many types.
Consider the following class hierarchy:
13. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 13
Example:
trait Animal {
def fitness: Int
}
trait Reptile extends Animal
trait Mammal extends Animal
trait Zebra extends Mammal {
def zebraCount: Int
}
trait Giraffe extends Mammal
Parametric Polymorphism
If we’re to take only one statement from this section, it’s that parametric polymorphism is
just generics as used in languages such as Java, C#, and Scala.
We can easily recognize parametrically polymorphic functions in Scala by the presence of
one or more type parameters delimited by square brackets in the method signature — they
enable us to apply the same logic to different data types.
Let’s look at an example to illustrate this. Imagine we’re writing a method to pair-wise
reverse a List. We want to be able to input List(1,2,3,4,5,6) and get output List(2,1,4,3,6,5).
If the length is odd, then the last element should stay in its place such that List(1,2,3,4,5)
becomes List(2,1,4,3,5).
The Naive Solution
14. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 14
def pairWiseReverseInt(xs: List[Int]): List[Int] = xs.grouped(2).flatMap(_.reverse).toList
Let’s create a test for the above scenario:
it should "reverse an even length int list" in {
val input = List(1,2,3,4,5,6)
val expected = List(2,1,4,3,5,6)
val actual = pairWiseReverseInt(input)
assert(actual == expected)
}
The method we’ve written works well for integer arrays but is not reusable for other types.
As an example, if we’d like to pair-wise reverse a list of strings or doubles, we’d have to
create additional methods and pointlessly duplicate the logic:
def pairWiseReverseString(xs: List[String]): List[String] =
arr.grouped(2).flatMap(_.reverse).toList
it should "pair-wise reverse a string list" in {
val original = List("a","b","c","d","e")
val expected = List("b","a","d","c","e")
val actual = pairWiseReverseString(original)
assertResult(expected)(actual)
}
The DRY Solution
Parametric polymorphism helps us eliminate this unnecessary duplication of code by
introducing a second formal parameter list for type parameters. With parametric
polymorphism, the logic remains the same for all the different types. To illustrate, we’ll
refactor the above pair-wise reverse methods into one that works for all types:
def pairWiseReverse[A](xs:List[A]): List[A] = xs.grouped(2).flatMap(_.reverse).toList
We’ve represented the type parameter by letter A, but we could just as well have used K, T,
or any other letter — it doesn’t matter. As with all formal parameters, we expect to substitute
15. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 15
A during a method call with a concrete type such as Int, String, or any other concrete type in
our case:
it should "pair-wise reverse lists of any type" in {
val originalInts = List(1,2,3,4,5)
val expectedInts = List(2,1,4,3,5)
val originalStrings = List("a","b","c","d","e")
val expectedStrings = List("b","a","d","c","e")
val originalDoubles = List(1.2,2.7,3.4,4.3,5.0)
val expectedDoubles = List(2.7,1.2,4.3,3.4,5.0)
assertResult(expectedInts)(pairWiseReverse[Int](originalInts))
assertResult(expectedStrings)(pairWiseReverse[String](originalStrings))
assertResult(expectedDoubles)(pairWiseReverse[Double](originalDoubles))
}
Note that the Scala compiler can infer the type from the function call argument we pass in.
It’s not mandatory that we explicitly pass in the type argument.
Subtype Polymorphism
The key concept in subtype polymorphism is substitutability as defined in the Liskov
substitution principle. We can recognize a Scala function that exhibits subtype polymorphism
when at least one of its parameter types has subtypes — that is, when it’s a supertype of at
least one type.
This type relation is sometimes written as S <: T, which means S is a subtype of T or T :> S,
meaning T is a supertype of S. We sometimes call this inclusion polymorphism. Unlike
parametric polymorphism, we restrict the range of types a function can be applied to with this
subtype relationship.
In the following example, we make Circle and Square subtypes of Shape. The method
printArea() accepts a Shape but will also work correctly if any of the subtypes is passed to it:
trait Shape {
16. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 16
def getArea: Double
}
case class Square(side: Double) extends Shape {
override def getArea: Double = side * side
}
case class Circle(radius: Double) extends Shape {
override def getArea: Double = Math.PI * radius * radius
}
def printArea[T <: Shape](shape: T): Double = (math.floor(shape.getArea) * 100)/100
Let’s add a test to capture the expected behavior:
"Shapes" should "compute correct area" in {
val square = Square(10.0)
val circle = Circle(12.0)
assertResult(expected = 100.00)(printArea(square))
assertResult(expected = 452.39)(printArea(circle))
}
In the above example, the subtyping relation is written as T <: Shape, to mean that any
argument of type T can be safely used in a context where a term of type Shape is expected.
We constrain T to only variables that are subtypes of Shape. So, we say that function
printArea() is subtype polymorphic over subtypes of Shape.
Ad-Hoc Polymorphism
One of the easiest ways to think about ad-hoc polymorphism is in terms of a switch statement
that’s happening in the background but without a default case. In this case, the compiler
switches between different code implementations depending on the type of input a method
receives.
There’s a similarity between parametric and ad-hoc polymorphism: they both use generics to
allow code to work on different types. The main difference is that in the former, the same
17. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 17
code runs for every type. In the latter, possibly different logic is executed based on the type,
hence the switch-statement analogy in the previous paragraph.
There are three outstanding concepts required to understand ways Scala supports ad-hoc
polymorphism: function overloading, operator overloading, and implicits.
Function Overloading
Let’s look at a built-in example of where ad-hoc polymorphism is applied in Scala. Assume
we have a list of integers that we want to sort:
it should "sort integers correctly" in {
val intList = List(3,5,2,1,4)
val sortedIntList = intList.sorted
assertResult(expected = List(1,2,3,4,5))(actual = sortedIntList)
}
We just called the method sorted on the List, and it knew exactly how to sort integers since
they are a primitive type. What if we also want List to know how to sort another type that we
have, say student ID’s wrapped in a custom class:
case class StudentId(id: Int)
Much as the underlying type is an Int, the Scala compiler does not know how to sort the type
StudentId. Going by our switch-statement analogy, it has encountered a case without
implementation, and remember, we don’t have a default case.
As a matter of fact, let’s attempt to do that:
it should "sort custom types correctly" in {
val studentIds = List(StudentId(5), StudentId(1),StudentId(4), StudentId(3), StudentId(2))
val sortedStudentIds = studentIds.sorted
assertResult(
expected = List(
StudentId(1),
StudentId(2),
18. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 18
StudentId(3),
StudentId(4),
StudentId(5)
)
)(actual = sortedStudentIds)
}
This test will fail to compile with the following error:
No implicit arguments of type: Ordering[StudentId]
It so happens that the method sorted expects some help with sorting types it doesn’t know
about. The type Ordering is a trait whose job is very similar to Java’s Comparator interface,
in that it also declares a compareTo method that provides a sorting strategy for a type.
The idiomatic way to fix this error is to create an implicit class or value of type Ordering that
is within the scope of our code.
Since this is not an article about implicits, let’s fix the error by providing an implementation
of Ordering type for StudentId just within the test. We’ll do this by defining the ordering
strategy and passing it to the sorted method as an argument:
...
val ord: Ordering[StudentId] = (x, y) => x.id.compareTo(y.id)
val sortedStudentIds = studentIds.sorted(ord)
...
Operator Overloading
Operator overloading is not so different from function overloading. This is where different
operators have different implementations depending on the types of their arguments.
We know the semantics of common operators, especially the arithmetic ones like the addition
(+) and subtraction (-) operators. Operator overloading is the reason all the following
snippets of code compile and work:
19. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 19
val intSum = 1990 + 10
val dblSum = 13.37 + 15.81
val strConcat = "FirstName " + "LastName"
The addition operator is able to work for integers, doubles, and strings alike because it’s
overloaded for each of those types. The compiler can infer the right implementation of the
operator from the types of the operands.
We can take this same idea and overload existing Scala operators to take care of our unique
needs. Assuming we are building software to solve math problems, would we rather write our
operations like this:
a + b * c
Or:
Add(a, Multiply(b, c))
Both are equivalent, but the first one just looks more expressive and commonplace in the
mathematics domain. So, we can say operator overloading is just syntactic sugar to allow us
to program using notations that are closer to our target domain.
How Scala Supports Operator Overloading
In actual technical terms, operator overloading is really made possible by Scala’s
permissiveness with method names and flexibility in how we invoke methods on objects.
More specifically; Scala allows us to use some operator names in method naming. + and –
are valid method names for any class we create in Scala, unlike Java. Let’s create an
abstraction for handling complex numbers as a demo:
case class Complex(re: Double, im: Double) {
def + (op: Complex): Complex = Complex(re + op.re, imaginary + op.im)
def - (op: Complex): Complex = Complex(re - op.re, imaginary - op.im)
override def toString: String = s"$re + ${im}i"
}
20. 191AIC302T-Object Oriented Programming with SCALA
Unit-III
Department of AI & DSPage 20
Notice that we can use operators as method names, which actually communicates our
intention more clearly, given the context. Secondly, there’s also flexibility in method
invocation syntax:
it should "add and subtract complex numbers successfully" in {
val cmpl1 = Complex(8.0, 3.0)
val cmpl2 = Complex(6.0, 2.0)
val cmplSum = cmpl1.+(cmpl2)
val cmplDiff = cmpl1 - cmpl2
assertResult(expected = "14.0 + 5.0i")(actual = cmplSum.toString)
assertResult(expected = "2.0 + 1.0i")(actual = cmplDiff.toString)
}
Notice that we can use postfix notation as in the addition invocation, but Scala also allows us
to separate the object, method name, and the argument as in the subtraction invocation where
we call (-) as an infix operator. Both are valid syntax in Scala.