Remember the sorting function:
def insertionSort(xs: List[Int]): List[Int] = {
def insert(y: Int, ys: List[Int]): List[Int] =
ys match {
case List() => y :: List()
case z :: zs =>
if (y < z) y :: z :: zs
else z :: insert(y, zs)
}
xs match {
case List() => List()
case y :: ys => insert(y, insertionSort(ys))
}
}
How to parameterize insertionSort
so that it can also be used for
lists with elements other than Int
(like, for instance, Rational
)?
def insertionSort[T](xs: List[T]): List[T] = ...
The above attempt does not work, because the comparison <
in insert
is not defined for arbitrary types T
.
Idea: parameterize insert
with the necessary comparison function.
The most flexible design is to make the function insertionSort
polymorphic and to pass the comparison operation as an additional
parameter:
def insertionSort[T](xs: List[T])(lessThan: (T, T) => Boolean): List[T] = {
def insert(y: T, ys: List[T]): List[T] =
ys match {
…
case z :: zs =>
if (lessThan(y, z)) y :: z :: zs
else …
}
xs match {
…
case y :: ys => insert(y, insertionSort(ys)(lessThan))
}
}
We can now call insertionSort
as follows:
val nums = List(-5, 6, 3, 2, 7)
val fruit = List("apple", "pear", "orange", "pineapple")
insertionSort(nums)((x: Int, y: Int) => x < y)
insertionSort(fruit)((x: String, y: String) => x.compareTo(y) < 0)
Or, since parameter types can be inferred from the call insertionSort(xs)
:
insertionSort(nums)((x, y) => x < y)
There is already a class in the standard library that represents orderings.
scala.math.Ordering[T]
provides ways to compare elements of type T
. So instead of
parameterizing with the lessThan
operation directly, we could parameterize
with Ordering
instead:
def insertionSort[T](xs: List[T])(ord: Ordering[T]): List[T] = {
def insert(y: T, ys: List[T]): List[T] =
… if (ord.lt(y, z)) …
… insert(y, insertionSort(ys)(ord)) …
}
Calling the new insertionSort
can be done like this:
insertionSort(nums)(Ordering.Int)
insertionSort(fruits)(Ordering.String)
This makes use of the values Int
and String
defined in the
scala.math.Ordering
object, which produce the right orderings on
integers and strings.
Problem: Passing around lessThan
or ord
values is cumbersome.
We can avoid this by making ord
an implicit parameter:
def insertionSort[T](xs: List[T])(implicit ord: Ordering[T]): List[T] = {
def insert(y: T, ys: List[T]): List[T] =
… if (ord.lt(y, z)) …
… insert(y, insertionSort(ys)) …
}
Then calls to insertionSort
can avoid the ordering parameters:
insertionSort(nums)
insertionSort(fruits)
The compiler will figure out the right implicit to pass based on the demanded type.
Say, a function takes an implicit parameter of type T
.
The compiler will search an implicit definition that
implicit
T
T
.If there is a single (most specific) definition, it will be taken as actual argument for the implicit parameter. Otherwise it's an error.
The combination of types parameterized and implicit parameters is also called type classes.
Define an ordering for the Rational
type:
class Rational(x: Int, y: Int) {
private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
private val g = gcd(x, y)
lazy val numer: Int = x / g
lazy val denom: Int = y / g
}
val compareRationals: (Rational, Rational) => Int = res0
implicit val rationalOrder: Ordering[Rational] =
new Ordering[Rational] {
def compare(x: Rational, y: Rational): Int = compareRationals(x, y)
}
val half = new Rational(1, 2)
val third = new Rational(1, 3)
val fourth = new Rational(1, 4)
val rationals = List(third, half, fourth)
insertionSort(rationals) shouldBe List(fourth, third, half)