Parameter Type Covariance in Specializations

Parameter type covariance in specializations

To properly answer this question, we must really take a step back and look at the problem you're trying to solve in a more general manner (and your question was already pretty general).

The Real Problem

The real problem is that you're trying to use inheritance to solve a problem of business logic. That's never going to work because of LSP violations and -more importantly- tight coupling your business logic to the application's structure.

So inheritance is out as a method to solve this problem (for the above, and the reasons you stated in the question). Fortunately, there are a number of compositional patterns that we can use.

Now, considering how generic your question is, it's going to be very hard to identify a solid solution to your problem. So let's go over a few patterns and see how they can solve this problem.

Strategy

The Strategy Pattern is the first that came to my mind when I first read the question. Basically, it separates the implementation details from the execution details. It allows for a number of different "strategies" to exist, and the caller would determine which to load for the particular problem.

The downside here is that the caller must know about the strategies in order to pick the correct one. But it also allows for a cleaner distinction between the different strategies, so it's a decent choice...

Command

The Command Pattern would also decouple the implementation just like Strategy would. The main difference is that in Strategy, the caller is the one that chooses the consumer. In Command, it's someone else (a factory or dispatcher perhaps)...

Each "Specialized Consumer" would implement only the logic for a specific type of problem. Then someone else would make the appropriate choice.

Chain Of Responsibility

The next pattern that may be applicable is the Chain of Responsibility Pattern. This is similar to the strategy pattern discussed above, except that instead of the consumer deciding which is called, each one of the strategies is called in sequence until one handles the request. So, in your example, you would take the more generic argument, but check if it's the specific one. If it is, handle the request. Otherwise, let the next one give it a try...

Bridge

A Bridge Pattern may be appropriate here as well. This is in some sense similar to the Strategy pattern, but it's different in that a bridge implementation would pick the strategy at construction time, instead of at run time. So then you would build a different "consumer" for each implementation, with the details composed inside as dependencies.

Visitor Pattern

You mentioned the Visitor Pattern in your question, so I'd figure I'd mention it here. I'm not really sure it's appropriate in this context, because a visitor is really similar to a strategy pattern that's designed to traverse a structure. If you don't have a data structure to traverse, then the visitor pattern will be distilled to look fairly similar to a strategy pattern. I say fairly, because the direction of control is different, but the end relationship is pretty much the same.

Other Patterns

In the end, it really depends on the concrete problem that you're trying to solve. If you're trying to handle HTTP requests, where each "Consumer" handles a different request type (XML vs HTML vs JSON etc), the best choice will likely be very different than if you're trying to handle finding the geometric area of a polygon. Sure, you could use the same pattern for both, but they are not really the same problem.

With that said, the problem could also be solved with a Mediator Pattern (in the case where multiple "Consumers" need a chance to process data), a State Pattern (in the case where the "Consumer" will depend on past consumed data) or even an Adapter Pattern (in the case where you're abstracting a different sub-system in the specialized consumer)...

In short, it's a difficult problem to answer, because there are so many solutions that it's hard to say which is correct...

Why is the parameter type generalized for method specialization on override?

Implementation of variance in programming languages

...when we override methods in subclasses, parameter types may be generalized (covariance), and result types may be specialized (contravariance)

Even though this can be true, it depends on the specific language, whether it actually implements this functionality. I have found examples on wiki:

  • C++ and Java implement only covariant return types and the method
    argument types are invariant.

  • C# does not implement either variancy (so both are invariant).

  • There is an example of a language (Sather) with contravariant argument type and covariant return type - this is what you mentioned.

  • However, there is also one (Eiffel) with covariant return & argument type, but this can normally cause runtime errors.

Controlling and "left-over" arguments

There is also a nice paragraph dividing arguments between controlling arguments and "left-over" arguments. The controlling ones are covariant and the non-controlling ones are contravariant. This is regarding multiple dispatch langauges, although most certainly you were referring to a single dispatch language. But even there is a single controlling argument (self/this).

Here is the paragraph (I did not have time to study the paper it is referring to, please feel free to read it if you have the time and post your findings):

Giuseppe Castagna[3] observed that in a typed language with multiple dispatch, a generic function can have some arguments which control dispatch and some "left-over" arguments which do not. Because the method selection rule chooses the most specific applicable method, if a method overrides another method, then the overriding method will have more specific types for the controlling arguments. On the other hand, to ensure type safety the language still must require the left-over arguments to be at least as general. Using the previous terminology, types used for runtime method selection are covariant while types not used for runtime method selection of the method are contravariant.

Conventional single-dispatch languages like Java also obey this rule: there only one argument is used for method selection (the receiver object, passed along to a method as the hidden argument this), and indeed the type of this is more specialized inside overriding methods than in the superclass.

The problem

According to the paragraph I assume that the self argument in its nature is not a regular method argument (which may be contravariant), because self is an another kind of argument - controlling argument - which are covariant.

...even though we just established that, as a consequence of the substitution principle, arguments of overridden methods need to be contravariant.

Well, it looks like not all of them.

Covariance in generic parameter and convention based on parameter type

I found one workaround which avoids using Expression which is obviously the issue here. The downfall of it is that we lose property name and we have to configure message manually.

 public interface IMyValidator<out T>
{
void AddMyRule<TProperty>(Func<T, TProperty> expression, string message);
}

public abstract class MyBaseValidator<T> : AbstractValidator<T>, IMyValidator<T>
{
public void AddMyRule<TProperty>(Func<T, TProperty> expression, string message)
{
var exp = FuncToExpression(expression);
RuleFor(exp).NotEmpty().WithMessage(message);
}

private static Expression<Func<T, P>> FuncToExpression<T, P>(Func<T, P> f) => x => f(x);
}

public static class Ext
{
public static void ValidateAll<T>(this AbstractValidator<T> validator)
{
(validator as IMyValidator<IWithPropertyA>)?.AddMyRule(x => x.PropertyA, "PropA Cant be empty");
(validator as IMyValidator<IWithPropertyB>)?.AddMyRule(x => x.PropertyB, "PropB Cant be empty");
}
}

public class Handler1 : MyBaseValidator<Handler1Data>
{
public Handler1()
{
this.ValidateAll();
}
}
public class Handler2 : MyBaseValidator<Handler2Data> { }

Type Members and Covariance

Box is invariant in its type T, but that doesn't mean there's nothing to see.

abstract class Box {
type T
def get: T
}
type InvariantBox = Box { type T = AnyRef }
type SortofCovariantBox = Box { type T <: AnyRef }

What alters the variance situation is the degree to which the type is exposed and the manner it is done. Abstract types are more opaque. But you should play with these issues in the repl, it's quite interesting.

# get a nightly build, and you need -Ydependent-method-types
% scala29 -Ydependent-method-types

abstract class Box {
type T
def get: T
}
type InvariantBox = Box { type T = AnyRef }
type SortofCovariantBox = Box { type T <: AnyRef }

// what type is inferred for f? why?
def f(x1: SortofCovariantBox, x2: InvariantBox) = List(x1, x2)

// how about this?
def g[U](x1: Box { type T <: U}, x2: Box { type T >: U}) = List(x1.get, x2.get)

And etc.

Scala class with covariance in derivable type of parameter

The query parameter is already covariant. It seems you have a problem like this:

class TableQuery[T]
class TableQuerySub[T] extends TableQuery[T]
class ModelTable[T]
class ModelTableSub[T] extends ModelTable[T]

class Model[U](val query: TableQuery[ModelTable[U]])

val x1: TableQuerySub[ModelTable[Int]] = ???
val x2: TableQuery[ModelTableSub[Int]] = ???
new Model(x1)
new Model(x2) // does not compile

Here x2 does not compile with the following message:

type mismatch;
found : TableQuery[ModelTableSub[Int]]
required: TableQuery[ModelTable[?]]
Note: ModelTableSub[Int] <: ModelTable[?],
but class TableQuery is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)

Here the compiler tells us that it does not know for sure that TableQuery[ModelTableSub[_]] extends TableQuery[ModelTable[_]]. In order to tell the compiler that this is actually the case we need to make the parameter of the TableQuery covariant

class TableQuery[+T]

If this does not help you at all, please include the use case in your question.


Edit

Adding a second type parameter still allows you to specify a context bound. The context bound syntax is nothing more than syntactic sugar. The following statements are equal:

def f[U : Model](u:U) = {
val m = implicitly[Model[U]]
???
}
def f[U](u:U)(implicit m:Model[U]) = ???

With this knowledge we can add a context bound for a context with more than one parameter:

class Model2[A, B]

def f[U](u:U)(implicit m:Model[_, U]) = ???

If you really want to specify it using the context bound syntax, you could do this

type AnyModel2[B] = Model2[_, B]
def f[U : AnyModel2](u:U) = ???

Without type alias you would get (in my opinion less readable)

def f[U : ({type T[x] = Model2[_, x]})#T](u:U) = ??? 

How to check covariant and contravariant position of an element in the function?

TL;DR:

  • Your Pets class can produce values of type A by returning the member variable pet, so a Pet[VeryGeneral] cannot be a subtype of Pet[VerySpecial], because when it produces something VeryGeneral, it cannot guarantee that it is also an instance of VerySpecial. Therefore, it cannot be contravariant.

  • Your Pets class can consume values of type A by passing them as arguments to add. Therefore a Pet[VerySpecial] cannot be a subtype of pet Pet[VeryGeneral], because it will choke on any input that is not VerySpecial. Therefore, your class cannot be covariant.

The only remaining possibility is: Pets must be invariant in A.


###An illustration: Covariance vs. Contravariance:

I'll use this opportunity to present an improved and significantly more
rigorous version of this comic. It is an illustration of the covariance and contravariance
concepts for programming languages with subtyping and declaration-site variance annotations
(apparently, even Java people found it sufficiently enlightening,
despite the fact that the question was about use-site variance).

First, the illustration:

covariance-contravariance-comic

Now a more detailed description with compilable Scala code.

###Explanation for Contravariance (left part of Figure 1)

Consider the following hierarchy of energy sources, from very general, to very specific:

class EnergySource
class Vegetables extends EnergySource
class Bamboo extends Vegetables

Now consider a trait Consumer[-A] that has a single consume(a: A)-method:

trait Consumer[-A] {
def consume(a: A): Unit
}

Let's implement a few examples of this trait:

object Fire extends Consumer[EnergySource] {
def consume(a: EnergySource): Unit = a match {
case b: Bamboo => println("That's bamboo! Burn, bamboo!")
case v: Vegetables => println("Water evaporates, vegetable burns.")
case c: EnergySource => println("A generic energy source. It burns.")
}
}

object GeneralistHerbivore extends Consumer[Vegetables] {
def consume(a: Vegetables): Unit = a match {
case b: Bamboo => println("Fresh bamboo shoots, delicious!")
case v: Vegetables => println("Some vegetables, nice.")
}
}

object Panda extends Consumer[Bamboo] {
def consume(b: Bamboo): Unit = println("Bamboo! I eat nothing else!")
}

Now, why does Consumer have to be contravariant in A? Let's try to instantiate
a few different energy sources, and then feed them to various consumers:

val oilBarrel = new EnergySource
val mixedVegetables = new Vegetables
val bamboo = new Bamboo

Fire.consume(bamboo) // ok
Fire.consume(mixedVegetables) // ok
Fire.consume(oilBarrel) // ok

GeneralistHerbivore.consume(bamboo) // ok
GeneralistHerbivore.consume(mixedVegetables) // ok
// GeneralistHerbivore.consume(oilBarrel) // No! Won't compile

Panda.consume(bamboo) // ok
// Panda.consume(mixedVegetables) // No! Might contain sth Panda is allergic to
// Panda.consume(oilBarrel) // No! Pandas obviously cannot eat crude oil

The outcome is: Fire can consume everything a GeneralistHerbivore can consume,
and in turn GeneralistHerbivore can consume everything a Panda can eat.
Therefore, as long as we care only about the ability to consume energy sources,
Consumer[EnergySource] can be substituted where a Consumer[Vegetables] is required,
and
Consumer[Vegetables] can be substituted where a Consumer[Bamboo] is required.
Therefore, it makes sense that Consumer[EnergySource] <: Consumer[Vegetables] and
Consumer[Vegetables] <: Consumer[Bamboo], even though the relationship between
the type parameters is exactly the opposite:

type >:>[B, A] = A <:< B

implicitly: EnergySource >:> Vegetables
implicitly: EnergySource >:> Bamboo
implicitly: Vegetables >:> Bamboo

implicitly: Consumer[EnergySource] <:< Consumer[Vegetables]
implicitly: Consumer[EnergySource] <:< Consumer[Bamboo]
implicitly: Consumer[Vegetables] <:< Consumer[Bamboo]

###Explanation for Covariance (right part of Figure 1)

Define a hierarchy of products:

class Entertainment
class Music extends Entertainment
class Metal extends Music // yes, it does, seriously^^

Define a trait that can produce values of type A:

trait Producer[+A] {
def get: A
}

Define various "sources"/"producers" of varying levels of specialization:

object BrowseYoutube extends Producer[Entertainment] {
def get: Entertainment = List(
new Entertainment { override def toString = "Lolcats" },
new Entertainment { override def toString = "Juggling Clowns" },
new Music { override def toString = "Rick Astley" }
)((System.currentTimeMillis % 3).toInt)
}

object RandomMusician extends Producer[Music] {
def get: Music = List(
new Music { override def toString = "...plays Mozart's Piano Sonata no. 11" },
new Music { override def toString = "...plays BBF3 piano cover" }
)((System.currentTimeMillis % 2).toInt)
}

object MetalBandMember extends Producer[Metal] {
def get = new Metal { override def toString = "I" }
}

The BrowseYoutube is the most generic source of Entertainment: it could give you
basically any kind of entertainment: cat videos, juggling clowns, or (accidentally)
some music.
This generic source of Entertainment is represented by the archetypical jester in the Figure 1.

The RandomMusician is already somewhat more specialized, at least we know that this object
produces music (even though there is no restriction to any particular genre).

Finally, MetalBandMember is extremely specialized: the get method is guaranteed to return
only the very specific kind of Metal music.

Let's try to obtain various kinds of Entertainment from those three objects:

val entertainment1: Entertainment = BrowseYoutube.get   // ok
val entertainment2: Entertainment = RandomMusician.get // ok
val entertainment3: Entertainment = MetalBandMember.get // ok

// val music1: Music = BrowseYoutube.get // No: could be cat videos!
val music2: Music = RandomMusician.get // ok
val music3: Music = MetalBandMember.get // ok

// val metal1: Metal = BrowseYoutube.get // No, probably not even music
// val metal2: Metal = RandomMusician.get // No, could be Mozart, could be Rick Astley
val metal3: Metal = MetalBandMember.get // ok, because we get it from the specialist

We see that all three Producer[Entertainment], Producer[Music] and Producer[Metal] can produce some kind of Entertainment.
We see that only Producer[Music] and Producer[Metal] are guaranteed to produce Music.
Finally, we see that only the extremely specialized Producer[Metal] is guaranteed to
produce Metal and nothing else. Therefore, Producer[Music] and Producer[Metal] can be substituted
for a Producer[Entertainment]. A Producer[Metal] can be substituted for a Producer[Music].
In general, a producer of
a more specific kind of product can be subsituted for a less specialized producer:

implicitly:          Metal  <:<          Music
implicitly: Metal <:< Entertainment
implicitly: Music <:< Entertainment

implicitly: Producer[Metal] <:< Producer[Music]
implicitly: Producer[Metal] <:< Producer[Entertainment]
implicitly: Producer[Music] <:< Producer[Entertainment]

The subtyping relationship between the products is the same as the subtyping relationship between
the producers of the products. This is what covariance means.


Related links

  1. A similar discussion about ? extends A and ? super B in Java 8:
    Java 8 Comparator comparing() static function

  2. Classical "what are the right type parameters for flatMap in my own Either implementation" question: Type L appears in contravariant position in Either[L, R]


Why does a type of T[keyof T] prevent covariance in an interface

Adding a generic type U extends T on the compare() method of the interface seems to resolve the errors:

type Animal = {
legs: boolean;
}

type Dog = Animal & {
golden: boolean;
}

interface CovariantComparer<T> {
compare<U extends T>(a: T, b: U[keyof T]): number;
}

declare let animalComparer: CovariantComparer<Animal>;
declare let dogComparer: CovariantComparer<Dog>;

animalComparer = dogComparer;
dogComparer = animalComparer;

TypeScript Playground Link



Related Topics



Leave a reply



Submit