Parametric Polymorphism is a fundamental concept in programming languages that allows functions and data types to be written generically, enabling code reuse and flexibility. In this blog, we’ll explore parametric polymorphism in Scala 3, the latest version of Scala programming language, with clear examples and explanations.
What is Parametric Polymorphism?
Parametric polymorphism, or generics, lets you write code that works smoothly with many different types. In Scala 3, you achieve this by using Type Parameters. These parameters can be limited to specific types or groups of types, adding extra safety and enabling advanced concepts. Essentially, parametric polymorphism is about abstracting type details, allowing you to create implementations that can adapt to different situations, sometimes with help from the compiler.
Example 1: Generic functions
def printElement[A](element: A): Unit = {
println(element)
}
printElement(42)
printElement(“Hello”)
Example 2: Generic classes
class Box[A](val value: A) {
def getValue: A = value
}
val intBox = new Box(42)
val stringBox = new Box(“Scala”)
println(intBox.getValue) // Output: 42
println(stringBox.getValue) // Output: Scala
Parametric Polymorphism: Scala 2 vs. Scala 3
Parametric polymorphism, or generics, is a fundamental feature in both Scala 2 and 3, but there are some key differences and improvements in Scala 3 compared to Scala 2. Let’s compare Parametric Polymorphism in Scala 2 and 3:
-
Syntax
- In Scala 2, parametric polymorphism is achieved using type parameters declared within square brackets (
[T]). - Scala 3 retains similar syntax for declaring type parameters, but it introduces some improvements and simplifications in syntax, such as using
givenandusingfor context parameters.
- In Scala 2, parametric polymorphism is achieved using type parameters declared within square brackets (
-
Type Class Support
- In Scala 2, type classes are implemented using implicit, which can sometimes lead to complex and verbose code.
- Scala 3 introduces improvements to support type classes more directly using
givenandusingclauses, making the code more readable and expressive. Additionally, Scala 3 allows for better inference of implicit parameters, reducing the need for explicit type annotations.
-
Intersection Types
- Scala 2 does not have built-in support for intersection types, which limits the expressiveness of parametric polymorphism.
- Scala 3 introduces intersection types, allowing types to express multiple constraints. This feature enhances the flexibility and composability of generic code.
-
Simplified Syntax
- Scala 3 introduces various syntax simplifications and improvements, making code more concise and readable. For example, Scala 3 allows for parameter clauses to be written more naturally using
usinginstead ofimplicitandimplicitparameters.
- Scala 3 introduces various syntax simplifications and improvements, making code more concise and readable. For example, Scala 3 allows for parameter clauses to be written more naturally using
-
Trait Parameters
- In Scala 2, traits cannot take type parameters directly, which limits their usability in certain contexts.
- Scala 3 introduces trait parameters, allowing traits to take type parameters directly, which enhances their flexibility and composability.
-
Type Inference
- Scala 3 improves type inference compared to Scala 2, reducing the need for explicit type annotations in many cases. This leads to cleaner and more concise code.
Overall, while parametric polymorphism is a core feature in both Scala 2 and Scala 3, Scala 3 introduces several improvements and simplifications, particularly in the area of type classes and syntax, making generic programming more powerful and expressive.
Types of Parametric Polymorphism in Scala 3
Certainly! In Scala, there are three types of parametric polymorphism: subtype polymorphism, ad hoc polymorphism, and parametric polymorphism. Let’s explore each type with examples:
1. Subtype Polymorphism
In Scala, developers primarily achieve subtype polymorphism through inheritance and method overriding, leveraging the relationships between classes and interfaces to facilitate this form of polymorphism, also known as inheritance polymorphism.
trait Animal {
def makeSound(): Unit
}class Dog extends Animal {
override def makeSound(): Unit = println(“Woof!”)
}class Cat extends Animal {
override def makeSound(): Unit = println(“Meow!”)
}def makeAnimalSound(animal: Animal): Unit = {
animal.makeSound()
}val dog = new Dog()
val cat = new Cat()makeAnimalSound(dog) // Output: Woof!
makeAnimalSound(cat) // Output: Meow!
Explanation:-
- In this example, we define a trait
Animalwith an abstract methodmakeSound()which represents the sound an animal makes. - We create two concrete classes
DogandCatthat extend theAnimaltrait and provide implementations for themakeSound()method. - The
makeAnimalSound()function takes an argument of typeAnimaland calls itsmakeSound()method, which is dynamically dispatched based on the actual type of the object passed. - When we pass instances of
DogandCattomakeAnimalSound(), the appropriatemakeSound()method for each animal is invoked, demonstrating subtype polymorphism.
2. Ad Hoc Polymorphism
Developers often implement ad hoc polymorphism in Scala 3 using function overloading, implicit conversions, or type classes, mirroring the familiar approach seen in Scala.
Example:- using function overloading:
def add(x: Int, y: Int): Int = x + y
def add(x: Double, y: Double): Double = x + y
println(add(1, 2)) // Output: 3
println(add(1.5, 2.5)) // Output: 4.0
using implicit conversions:
case class StudentId(id: Int)
val studentIds = List(StudentId(5), StudentId(1), StudentId(4), StudentId(3), StudentId(2))
//defining the ordering strategy and passing it to the sorted method as an argument to sort the studentId’s
val ord: Ordering[StudentId] = (x, y) => x.id.compareTo(y.id)
val sortedStudentIds = studentIds.sorted(using ord)
Example using operator overloading:
trait Addable[T] {
def +(x: T, y: T): T
}
// Define instances of the type class for specific types
given Addable[Int] {
def +(x: Int, y: Int): Int = x + y
}
given Addable[Double] {
def +(x: Double, y: Double): Double = x + y
}
// Define a generic function that uses the type class
def add[T](x: T, y: T)(using ev: Addable[T]): T = ev.+(x, y)
// Usage
val result1 = add(1, 2)
// result1: Int = 3
val result2 = add(1.5, 2.5)
// result2: Double = 4.0
val result3 = add(“Hello, “, “world!”)
// Compilation error: No Addable instance found for String
3. Parametric Polymorphism:
Using type parameters in Scala 3 enables developers to write functions and data types generically, devoid of explicit type declarations, thereby facilitating the creation of versatile code adaptable to many types.
def identity[A](x: A): A = x
println(identity(42)) // Output: 42
println(identity(“Hello”)) // Output: Hello
Explanation:
- This example illustrates parametric polymorphism using a generic function
identity. The function takes a single argumentxof typeAand returns the same value without any modification. - The type parameter
Aserves as a placeholder for any type. When we callidentitywith anIntargument, the function returns anInt. Similarly, when called withString argument,it returns aString. - Parametric polymorphism allows the
identityfunction to work with any type, providing flexibility and code reuse without sacrificing type safety.
The Real power of Parametric Polymorphism
The real power of parametric polymorphism in Scala 3 lies in several key aspects:
- Code Reusability: Parametric polymorphism enables you to write functions, classes, and data structures that can work with any type. By embracing parametric polymorphism, you empower yourself to craft generic algorithms capable of seamless integration across diverse types, thereby mitigating code redundancy and fostering robust code reuse.
- Type Safety: Scala’s type system ensures that parametrically polymorphic code is type-safe. The compiler enforces type constraints at compile-time, preventing type-related errors and promoting robustness in your code.
- Abstraction: Parametric polymorphism allows you to abstract over types, enabling you to write general-purpose code that operates independently of the specific types it manipulates. This level of abstraction can lead to more modular, maintainable, and scalable codebases.
- Expressiveness: Generics in Scala 3 support a rich set of features, including variance annotations (covariance, contravariance, and invariance), context bounds, and type constraints. These features enhance the expressiveness of parametric polymorphism, allowing you to specify complex relationships between types and write more flexible code.
- Compile-Time Safety and Optimization: Parametric polymorphism enables the Scala compiler to perform type inference and static type checking at compile-time. This results in the early detection of type errors and optimization opportunities, leading to more efficient and reliable code execution.
- Library Support: Scala’s standard library and popular third-party libraries leverage parametric polymorphism extensively to provide generic data structures, algorithms, and abstractions. This rich ecosystem of generic libraries empowers Scala developers to build sophisticated applications with ease.
Conclusion
Parametric polymorphism in Scala 3 offers powerful abstractions for writing reusable and type-safe code. By leveraging type parameters, developers can write functions and data structures that apply to a wide range of types, promoting code reuse and maintainability. In this blog, we’ve explored the syntax and usage of parametric polymorphism in Scala 3 through examples. Understanding these concepts is crucial for mastering functional programming in Scala and building robust and scalable applications.