Type-safe Scala sequence comprehensions
On page 85 of Scala By Example, the last paragraph states the following in reference to generalising sequence (aka for) comprehensions:
It would be attractive to enforce these types statically in the Scala compiler, for instance by requiring that any type supporting for-comprehensions implements a standard trait with these methods 1 . The problem is that such a standard trait would have to abstract over the identity of the class C, for instance by taking C as a type parameter. Note that this parameter would be a type constructor, which gets applied to several different types in the signatures of methods map and flatMap. Unfortunately, the Scala type system is too weak to express this construct, since it can handle only type parameters which are fully applied types.
This type system feature being described is called higher-kinded types. Sadly, none of the mainstream, statically-typed languages offer this feature, which leads to enormous amounts of repetition.
In fact, many of the arguments in favour of dynamic languages that I have seen, are refuted simply with the introduction of this very key, but relatively simple (to other type system features) feature. In other words, I only have to batter an eyelid before I have refuted those arguments, which brings me great suspicion of any underlying plausibility. Perhaps there are other arguments that are more plausible, but I have yet to hear any. I digress…
Higher-kinded types would be available in Java/C# if you could write something like this:
static <C<_>, A, B> C<B> map(Convert<A, B> c, C<A> container) {
...
Again, sadly, you cannot. But in Scala you can! In other words, the paragraph quoted above is, at least from how I interpret it, false. However, do understand that at the time the paragraph was written, it may have been true (I can neither confirm nor deny this), but my reckoning is that this piece of documentation needs updating at the least.
Without further ado, I introduce the my refutation:
trait Functor[f[_], a] {
def map[b](f: a => b)(ft: => f[a]): f[b]
}
trait Monad[m[_], a] {
def flatMap[b](ma: => m[a])(f: a => m[b]): m[b]
}
trait Filter[c[_], a] {
def filter(f: a => Boolean)(c: => c[a]): c[a]
}
My assertion now, is that a Scala sequence comprehension could indeed be written to be type-safe if it only accepted types that adhered to the above interfaces. Am I wrong and if so, what have I misunderstood?
Here are some example (incomplete) implementations of foreach:
object Comprehension {
type Comprehension[c[_], a] = Functor[c[a], a] with Monad[c[a], a] with Filter[c[a], a]
def foreach[c[_], a](c: c[a], e: a => Unit, p: a => Boolean)(implicit ca: Comprehension[c[a], a]): Unit = ca.map(a => if(p(a)) e(a))(c)
def foreach[c[_], a](c: c[a], e: a => Unit)(implicit ca: Comprehension[c[a], a]): Unit = foreach[c[a], a](c, e, (a: a) => true)
def foreach[c[_], a, b](c: c[a], f: a => b, p: a => Boolean)(implicit ca: Comprehension[c[a], a]): c[b] = ca.map(a => f(a))(ca.filter(p)(c))
def foreach[c[_], a, b](c: c[a], f: a => b)(implicit ca: Comprehension[c[a], a]): c[b] = ca.map(a => f(a))(c)
}
I await an explanation for what I think is the unwarranted undermining of Scala’s value by Scala’s own documentation!
September 14th, 2007 at 4:55 am
Nice post about an important topic.
I suspect the documentation is dated. Higher Order Kinds were just added this summer with 2.5. The crew have written a paper which discusses map, flatMap and filter; it’s at Adriaan Moors’ site: http://www.cs.kuleuven.be/~adriaan/files/higher.pdf
Your approach is more Haskellish than theirs. in an interesting way.
I haven’t tried your approach (yet), but I find things like this stress Scala’s type inference system… sometimes I’ve been unable to do what I want without losing type information.
September 14th, 2007 at 8:23 am
G’day Henry,
Yes I find it stresses Scala’s type inferencing as well, so I have to manually help it along. A good example above is passing the type parameters to foreach explicitly; without them, the code would not compile.
December 7th, 2007 at 12:56 pm
I tried your example but for the line:
type Comprehension[c[_], a] = Functor[c[a], a] with Monad[c[a], a] with Filter[c[a], a]The compiler complains with:c[a] takes no type parameters, expected: one
December 7th, 2007 at 1:00 pm
Sorry for the bad formatting, it seems that on submission formatting tags don’t work.
Maybe this is more readable:
type Comprehension[c[_], a] = Functor[c[a], a] with Monad[c[a], a] with Filter[c[a], a]
The compiler complains with:c[a] takes no type parameters, expected: one
December 7th, 2007 at 5:18 pm
Hi Oscar,
The example works for Scala 2.5.1, so I’ll have to update it for 2.6.0. The correction is actually a much more concise articulation of the intention. In the meantime, you might try replacing all occurrences of c[a] in those cases that the compiler complains with just c.