Using Java as the benchmark is one thing. Using a "principle" from
Effective Java is another again. The fact is, this is not a principle at
all, but simply yet another bad idea.
Let's analyse your question acknowledging my dismissal of this
pseudo-principle and hopefully make sense of why a function's argument
is contravariant (? super in Java syntax) and covariant (? extends in
Java syntax) in the return type. Note also that a function's argument is
often called its domain and its return called its codomain.
First, let's start with: what does it mean exactly for a functor to be
covariant? Which functor anyway? In the case of Function1, we are
talking specifically about the functor free in its return type and I
will invent a syntax to denote this with the ? being a free variable:
Function1[T1, ?]. There is valid Scala syntax for this, but it's a bit
hairy, so I will leave it out and go with my invented syntax.
A functor F is covariant if there exists an operation [A, B](A => B) =>
(F[A] => F[B]) for that functor satisfying a couple of laws, which we
will omit for now. Astute Scala users will notice that this is the
signature to the usual map method. So, is Function1[T, ?] a covariant
functor? That is to say, when we replace F with Function1[T, ?], do we
get to where we need to?
That question is equivalent to this question: can there exist a value
with this signature (+laws):
def IsItCovariant[A, B]: (A => B) => Function1[T, A] => Function1[T, B]
The answer is a clear yes -- this operation exists and indeed, is
already implemented in the standard library. It is called "compose" and
is a method on scala.Function1. In fact, as a useful coincidence, there
is *no other possible* method with this signature (some caveats aside)
except for what is commonly called "function composition." As a side
note, given that Scala has special for-comprehension syntax for methods
named map, it is a little annoying that this method is named compose and
not map! I suppose this is not a big deal in the absence of a flatMap
implementation too. I digress, but feel free to "add those methods"
yourself in the usual Scala way.
So, Function1[T, ?] is a covariant functor, because it meets the
definition of covariance -- it also meets the laws too, take my word for
it (happy to expand here if there is distrust or curiosity).
You will notice this function signature: (A => B) => (F[A] => F[B]) is
simply symbols for what is often stated in English:
"If A subclasses B, then F[A] subclasses F[B]." You can use the =>
symbol to denote both "if/then" and a "subclassing" relationship -- they
are equivalent.
Sometimes it is also written like this:
if A <: B then F[A] <: F[B]
or perhaps like this:
(A <: B) => (F[A] <: F[B])
Again, we are just replacing symbols with English -- the concept is the
same -- it is logical implication. Feel free to use your preferred
denotation and communication method, with the caveat that there are
implementation details that might throw it off a bit if you get too
loose with the informalities.
As for contravariance, we have a very similar question. First, what is a
contravariant functor? It is any functor giving rise to this operation:
[A, B](A => B) => F[B] => F[B]
There is the same correspondence with subtype/supertype relationships
and logical implication and all that. Different symbols or loose English
expressions, but it all boils down to the same principle.
Again, we have a couple (exactly 2) laws to satisfy. So now the
question, is Function1[?, R] a contravariant functor? Well, this is just
the same as asking, does this operation exist (+laws):
def IsItContravariant[A, B]: (A => B) => Function1[B, R] => Function1[A, R]
Again, the answer is yes and again, coincidentally, there is only one
possible implementation of this method (and it satisfies our unstated
laws). It is implemented in the Scala standard library as the
scala.Function1#andThen method. Look it up if you like!
Now, if you apply the same reasoning and switch the variance around on
the function's domain/codomain, you will find that you can *not*
implement those methods (and so there is point testing for any laws!).
Try it if you like. That is to say, the functor of a function free in
its domain is contravariant and only contravariant, while free in its
codomain is covariant and only covariant.
Since you seem to be a fan of principled reasoning, I hope this
"mechanical test" for whether a functor is covariant or contravariant
(or perhaps neither, being invariant aka exponential) will help you.
Instead of "PECS principle", let's call it "algebra."
You can apply this test to *any* functor in our category. For example,
suppose the following data structure:
case class ReaderState[R, S, A](run: R => S => (A, S))
Is ReaderState[R, S, ?] covariant, invariant or contravariant? What
about the ReaderState[R, ?, A] or ReaderState[?, S, A] functors? I will
leave these three as reader's exercises, but just apply the mechanical
test (although, you will need those laws to truly get the answer with
total confidence).
As for the laws that I left out, I did this for the purpose of
explanation, not because of intent to deceive or because they are
unimportant, quite the contrary. Only because I didn't think stating the
laws explicitly is necessary to this introductory explanation. If you'd
like to go the next step and explore this subject further, I'd be happy
to do so and we'd have to take a look at those laws -- in fact, an
excellent exercise might be to express these laws in Scala using the
ScalaCheck library (another reader exercise?). These laws are called
"identity law" and "composition law" and apply to both covariant and
contravariant functors if you'd like to search for them and read further.
As always, hope that helps!
--
Tony Morris
http://tmorris.net/