In this post, I discuss the already introduced intimidating phrase "Type Constructor Polymorphism" through a series of code snippets ranging from toys to some real-world stuff. The idea is, once again, not to evangelize type theory intricacies, but share some of the experiences of how this feature in Scala's type system can help write idiomatic code, while staying away from the complexities of its underlying implementation.
Jump on ..
We have a list of
Option[String]
that we need to abstract over and compute some value. Say, for the sake of keeping the example simple, we will calculate the sum of lengths of all the strings present ..val employees: List[Option[String]] =
List(Some("dave"), None, Some("john"), Some("sam"))
val n: List[Int] =
employees.map { x =>
x match {
case Some(name) => name.length
case None => 0
}
}.elements.reduceLeft[Int](_ + _)
println(n)
Let us take another problem that needs to abstract over a different
List
structure, a List
of List
of String
s, and compute the same result as before, i.e. the sum of lengths of all the strings encountered in the collection ..val brs: List[List[String]] =
List(List("dave", "john", "sam"), List("peter", "robin", "david"), List("pras", "srim"))
val m: List[Int] =
brs.flatMap {x => x.map {_.length}}
.elements.reduceLeft[Int](_ + _)
println(m)
Do you see any commonality in the solution structure in the above snippets ? After all, the problem space has a common generic structure ..
- we have a
List
with someString
values abstracted in different forms - need to iterate over the list
- do some stuff with elements in the list and compute an
Int
value
Unfortunately the actual solution structures look quite different and have to deal a lot digging into the abstractions of the underlying representations within the collection itself. And this is because, we cannot abstract over the type constructor (the List in this case) that takes another type constructor as an argument (
Option[String]
in the first case and List[String]
in the second case).Enter type constructor polymorphism.
Sounds intimidating ? Maybe, but ask the Haskellers .. they have been using typeclasses ever since so successfully in comprehensions, parser combinators and embedded DSLs and programming at a different level of abstraction.
Scala supports type constructor polymorphism since 2.5, and the details have been discussed in a recent paper by Adriaan Moors et al in OOPSLA 2008.
Here is a snippet of the Scala code that works seamlessly for both of the above cases ..
val l: List[Int] = employees.flatMapTo[Int, List]{_.map{_.length}}
val sum: Int = l.elements.reduceLeft[Int](_ + _)
println(sum)
The above code abstracts over
List
through higher order parametric polymorphism, i.e. independent of whether the List parameter is an Option[]
or another List[]
. Incidentally both of them (List
and Option
) are monads and flatMapTo abstracts a monadic computation, hiding all details of type constructor polymorphism from the developer.Now here is some real life example (elided for simplicity) ..
Here are the simple domain models for
Customer
, Instrument
and Trade
, used for modeling a use case where a Customer
can order for the Trade
of an Instrument
in an exchange.case class Customer(id: Int, name: String)
case object nomura extends Customer(1, "NOMURA")
case object meryll extends Customer(2, "MERYLL")
case object lehman extends Customer(3, "LEHMAN")
case class Instrument(id: Int)
case object ibm extends Instrument(1)
case object sun extends Instrument(2)
case object google extends Instrument(3)
case class Trade(ref: Int, ins: Instrument, qty: Int, price: Int)
And we fetch the following list through a query from the database. It is a
List
of tuples where each tuple consists of a Customer
and a trade that has been executed based on the Order
he placed at the exchange. And here is the snippet of the code that computes the sum total of the values of all trades executed in the day for all customers.val trades: List[(Customer, Option[Trade])] =
List((nomura, Some(Trade(100, ibm, 20, 12))),
(meryll, None), (lehman, Some(Trade(200, google, 10, 10))))
val ts: List[Option[Trade]] = trades.map(_._2)
val t: List[Int] = ts.flatMapTo[Int, List]{_.map{x => x.qty * x.price}}
val value = t.elements.reduceLeft[Int](_ + _)
println(value)
Really not much different from the above simple cases where we were dealing with toy examples - isn't it ? The structure of the solution is the same irrespective of the complexity of data being stored within the collections. The iteration is being done at a much higher level of abstraction, independent of the types stored within the container. And as I mentioned above,
flatMapTo
is the secret sauce in the above solution structure that abstracts the building of the new container hiding the inner details. To get more into the innards of flatMapTo
and similar higher order abstractions, including the new form of Iterable[+T]
, have a look at the OOPSLA paper referenced above.Postscript: In all the snippets above, I have been explicit about all type signatures, just for the sake of easier understanding of my passionate reader. In reality, almost all of these will be inferred by Scala.