by Brian McKenna on Jun 22, 2014
technical
Scalaz used to have a scalaz.Functor for scala.collection.Set but
it was eventually removed
because it relied on
Any’s == method. You
can read more about why Functor[Set] is a bad idea at
Fake Theorems for Free.
If Set had been truly parametric, we wouldn’t have been able to
define a Functor in the first place. Luckily, a truly parametric Set
has recently been added to Scalaz as scalaz.ISet, with preliminary
benchmarks also showing some nice performance improvements. I highly
recommend using ISet whenever you can!
Now we can see the problem more clearly; the type of map on ISet
is too restrictive to be used inside of a Functor because of the
scalaz.Order constraint:
def map[B: Order](f: A => B): ISet[B]
And it might seem like we’ve lost something useful by not having a
Functor available. For example, we can’t write the following:
val nes = OneAnd("2014-05-01", ISet.fromList("2014-06-01" :: "2014-06-22" :: Nil)) // a non-empty Set
val OneAnd(h, t) = nes.map(parseDate)
Which is because the map function on scalaz.OneAnd requires a
scalaz.Functor for the F[_] type parameter, which is ISet in the
above example.
But we have a solution! It’s called
Coyoneda
(also known as the Free Functor) and it’ll hopefully be able to
demonstrate why not having Functor[ISet] available has no
fundamental, practical consequences.
Coyoneda can be defined in Scala like so:
trait Coyoneda[F[_], A] {
type I
def k: I => A
def fi: F[I]
}
There are just three parts to it:
I - an existential typek - a mapping from I to Afi - a value of F[I]We can create a couple of functions to help with constructing a Coyoneda value:
def apply[F[_], A, B](fa: F[A])(_k: A => B): Coyoneda[F, B] { type I = A } =
new Coyoneda[F, B] {
type I = A
val k = _k
val fi = fa
}
def lift[F[_], A](fa: F[A]): Coyoneda[F, A] = Coyoneda(fa)(identity[A])
The constructors allow any type constructor to become a Coyoneda value:
val s: Coyoneda[ISet, Int] = Coyoneda.lift(ISet.fromList(1 :: 2 :: 3 :: Nil))
Now here’s the special part; we can define a Functor for all
Coyoneda values:
implicit def coyonedaFunctor[F[_]]: Functor[({type λ[α] = Coyoneda[F, α]})#λ] =
new Functor[({type λ[α] = Coyoneda[F,α]})#λ] {
def map[A, B](ya: Coyoneda[F, A])(f: A => B) = Coyoneda(ya.fi)(f compose ya.k)
}
What’s interesting is that the F[_] type does not have to have a
Functor defined for the Coyoneda to be mapped!
Let’s use this to try out our original example. We’ll define a type alias to make things a bit cleaner:
type ISetF[A] = Coyoneda[ISet, A]
And we can use this new type instead of a plain ISet:
// Scala has a really hard time with inference here, so we have to help it out.
val functor = OneAnd.oneAndFunctor[ISetF](Coyoneda.coyonedaFunctor[ISet])
import functor.functorSyntax._
val nes = OneAnd[ISetF, String]("2014-05-01", Coyoneda.lift(ISet.fromList("2014-06-01" :: "2014-06-22" :: Nil)))
val OneAnd(h, t) = nes.map(parseDate)
So we’ve been able to map the Coyoneda! But how do we do something useful with it?
We couldn’t define a Functor because it needs scalaz.Order on the
output type, but we can use the map method directly on ISet. We
can use that function by running the Coyoneda like so:
// Converts ISetF back to an ISet, using ISet#map with the Order constraint
val s = t.fi.map(t.k).insert(h)
And we’re done!
We’ve been able to use Coyoneda to treat an ISet as a Functor,
even though its map function is too constrained to have one defined
directly. This same technique applies to scala.collection.Set and
any other type-constructor which would otherwise require a
restricted Functor. I
hope this has demonstrated that Functor[Set] not existing has no
practical consequences, other than scalac not being as good at
type-inference.