This lesson in Scala generics shows how to return the current type of a flow in Scala with looks at f-bounds, comonads, and other tricks for the compiler.
In my odyssey to understand Comonads, the first thing I did after reading about them was to implement a series of tests that would make them a little bit clearer, and I did it using the NonEmptyList implementation of the scalaz library.
But obviously, testing a specific implementation wouldn’t get me to the end of it, so I decided to implement an IdentityComonad by myself, a Comonad without added functionality.
I ended up with something like this:
The tests I had done for the NonEmptyList also passed, and in my opinion, it didn’t seem that bad, so I thought about abstracting an interface to be able to do more Comonads, you know… just for fun.
And the first version of the interface, designing all the methods that I had already defined, was this:
So the IdentityComonad evolved to this:
And right here, we found the first problem: Until this point, we only found IdentityComonad (IComonad in the trait) in the return types, but…
Oh, in the cobind method, the type is also present in the input parameter (f), and I am anticipating from now that this will not compile, because IdentityComonad[A] isn’t IComonad[A].
This is when we tell ourselves, compilation or death, and we change the cobind to use IComonad:
Hey! Everything compiles, and it even works… cool, right?
The problem is if we leave this method signature for cobind in the interface:
We could accept any other Comonad that extends from IComonad as a parameter in f. We need to change IComonad to ensure that the type used in f will be exactly the subclass that we are implementing and not a generic IComonad.
But it doesn’t all end here… The return type of the map method is also IComonad… Ouch.
This means that in our implementation of map in IdentityComonad, we could return any other implementation of IComonad, not necessarily IdentityComonad, which would make our Comonad stop making sense.
Getting at this phase, you remember having read about something called F-bounded types, so we look for information and try to use it:
If you are not very used to generics… right now, you might be getting a strong migraine. This way, IComonad uses A, just like before, but now it also uses F[A], determining the type used in the implementation.
It may seem very confusing and messy, but to sum up, the implementation tells its interface who it is, so that the interface can force the types.
The implementation could look like that:
Great, huh!? Now the return type must be exactly the type of the class that we are implementing (IdentityComonad). We no longer have the same problem we had before (being able to use sibling IdentityComonad types).
But… do you smell that? I smell it too… I am not being forced to tell IComonad that T is IdentityComonad.
I mean, IdentityComonad could extend IComonad [A, Something] (being Something another generic class that accepts [A]), and everything would work out (using Something as F)
It’s kind of hard to explain, so I’ll draw a picture to illustrate it:
They may not be the same class, and it would still work! Look at this example with a “ListComonad” (not a valid implementation):
See? Now I have a ListComonad — not bad, huh? But this isn’t really a valid list Comonad. I am not returning a Comonad, so I can’t chain them either.
Also, the ListComonad parameter is not of type A, it is of type List[A], and this is a problem, because ListComonad should not depend of List (which is a monad).
But it doesn’t matter, let’s keep focus, the fact is that I just demonstrated that I can fool the interface to use types that I shouldn’t use (or do not want to use, it all depends on how you look at it).
And so we made it to the final implementation, the best I’ve arrived to until now (without using typeclasses).
Now we try to force F[A] in IComonad to be a subtype of IComonad (F[A]