How can you achieve polymorphism in Scala?

Instruction: Discuss different ways to achieve polymorphism in Scala, providing examples for each.

Context: This question assesses the candidate's understanding of polymorphism in Scala, including subtyping and ad-hoc polymorphism, and their ability to apply these concepts in code.

Official Answer

Certainly, I'm delighted to share my understanding and approach to achieving polymorphism in Scala, a concept that's central to crafting scalable and maintainable code in many software engineering projects, including those I've led at top tech companies.

Polymorphism in Scala can primarily be achieved through two avenues: subtyping and ad-hoc polymorphism. Both play crucial roles in allowing developers to write flexible and reusable code.

Starting with subtyping, it's the form of polymorphism most familiar to those coming from an object-oriented programming background. It allows a function to accept arguments of many different types, as long as they all are subclasses of a common superclass.

For example, imagine we have a superclass called Shape, and subclasses Circle and Rectangle. We can define a function def draw(shape: Shape) which will accept any Shape object, including Circle and Rectangle. This enables us to write generic functions that can operate on a family of types.

abstract class Shape {
  def draw(): Unit
}

class Circle extends Shape {
  def draw(): Unit = println("Drawing a circle")
}

class Rectangle extends Shape {
  def draw(): Unit = println("Drawing a rectangle")
}

def drawShape(shape: Shape): Unit = {
  shape.draw()
}

This example demonstrates how subtyping facilitates polymorphism by letting us treat different types of shapes uniformly.

Moving on to ad-hoc polymorphism, it's achieved in Scala through features like implicit parameters and type classes, enabling functions to operate on arguments of multiple types without them necessarily sharing a common superclass.

A quintessential example involves creating a type class to abstract the concept of adding two elements, allowing us to define addition for various types.

trait Addable[T] {
  def add(x: T, y: T): T
}

implicit object IntAddable extends Addable[Int] {
  def add(x: Int, y: Int): Int = x + y
}

implicit object StringAddable extends Addable[String] {
  def add(x: String, y: String): String = x.concat(y)
}

def add[T](x: T, y: T)(implicit addable: Addable[T]): T = {
  addable.add(x, y)
}

With ad-hoc polymorphism, we can now use the add function to sum integers, concatenate strings, or perform any operation defined for the type, as long as an implicit Addable instance is available for that type.

It's important to note that while subtyping is powerful for cases where a strict is-a relationship exists, ad-hoc polymorphism offers more flexibility and power, enabling operations on a wider variety of types without forcing them into a rigid hierarchy.

In conclusion, achieving polymorphism in Scala through both subtyping and ad-hoc polymorphism not only exemplifies the language's hybrid nature, blending object-oriented and functional paradigms, but also equips developers with tools to write more adaptable and reusable code. By understanding and leveraging these concepts, developers can significantly enhance the robustness and flexibility of their Scala applications.

Related Questions