Reputation: 9540
I have the following data structure:
class MyDaSt[A]{
def map[B: ClassTag](f: A => B) = //...
}
I'd like to implement a Functor
instance for to be able to use ad-hoc polymorphism. The obvious attempt would be as follows:
implicit val mydastFunctor: Functor[MyDaSt] = new Functor[MyDaSt] {
override def map[A, B](fa: MyDaSt[A])(f: A => B): MyDaSt[B] = fa.map(f) //compile error
}
It obviously does not compile because we did not provide an implicit ClassTag[B]
. But would it be possible to use map
only with functions f: A => B
such that there is ClassTag[B]
. Otherwise compile error. I mean something like that:
def someFun[A, B, C[_]: Functor](cc: C[A], f: A => B) = cc.map(f)
val f: Int => Int = //...
val v: MyDaSt[Int] = //...
someFunc(v, f) //fine, ClassTag[Int] exists and in scope
I cannot change its implementation in anyway, but I can create wrappers (which does not look helpful through) or inheritance. I'm free to use shapeless
of any version.
I currently think that shapeless is a way to go in such case...
Upvotes: 0
Views: 125
Reputation: 7353
I'll expand on what comments touched:
cats.Functor
describes an endofunctor in a category of Scala types - that is, you should be able to map
with a function A => B
where A
and B
must support any Scala types.
What you have is a mathematical functor, but in a different, smaller category of types that have a ClassTag
. These general functors are somewhat uncommon - I think for stdlib types, only SortedSet
can be a functor on a category of ordered things - so it's fairly unexplored territory in Scala FP right now, only rumored somewhat in Scalaz 8.
Cats does not have any tools for abstracting over such things, so you won't get any utility methods and ecosystem support. You can use that answer linked by @DmytroMitin if you want to roll your own
Coyoneda
can make an endofunctor on Scala types from any type constructor F[_]
. The idea is simple:
F[Initial]
Initial => A
map
with A => B
, you don't touch initial value, but simply compose the functions to get Initial => B
You can lift any F[A]
into cats.free.Coyoneda[F, A]
. The question is how to get F[A]
out.
If F
is a cats.Functor
, then it is totally natural that you can use it's native map
, and, in fact, there will not be any difference in result with using Coyoneda
and using F
directly, due to functor law (x.map(f).map(g) <-> x.map(f andThen g)
).
In your case, it's not. But you can tear cats.free.Coyoneda
apart and delegate to your own map
:
def coy[A](fa: MyDaSt[A]): Coyoneda[MyDaSt, A] = Coyoneda.lift(fa)
def unCoy[A: ClassTag](fa: Coyoneda[MyDaSt, A]): MyDaSt[A] =
fa.fi.map(fa.k) // fi is initial value, k is the composed function
Which will let you use functions expecting cats.Functor
:
def generic[F[_]: Functor, A: Show](fa: F[A]): F[String] = fa.map(_.show)
unCoy(generic(coy(v))) // ok, though cumbersome and needs -Ypartial-unification on scala prior to 2.13
(runnable example on scastie)
An obvious limitation is that you need to have a ClassTag[A]
in any spot you want to call unCo
- even if you did not need it to create an instance of MyDaSt[A]
in the first place.
The less obvious one is that you don't automatically have that guarantee about having no behavioral differences. Whether it's okay or not depends on what your map
does - e.g. if it's just allocating some Array
s, it shouldn't cause issues.
Upvotes: 3