1

I would like to achieve the following functionality:

case class ValidatorClean[+A](apply: A => A)

implicit val traversableValidatorClean = ValidatorClean[Traversable[String]](_.map(_.trim))

so that traversableValidatorClean gets picked whenever a ValidatorClean[Seq[String]] or ValidatorClean[List[String]] is needed, for example.

However, this doesn't compile, with the error

covariant type A occurs in contravariant position in type => A => A of value apply

I understand that a function is contravariant in its input and covariant in its output, but I want A to behave invariantly in apply. That is, the function in apply will always return the exact same type as its input.

Can this be achieved?

rpiaggio
  • 36
  • 2
  • 4
  • Anybody seen a question about going the other way (making an invariant covariant)? – combinatorist Jul 24 '21 at 03:21
  • 1
    @combinatorist `type G[+T] = F[_ <: T]`, `type G[-T] = F[_ >: T]` https://stackoverflow.com/questions/75035824/in-scala3-if-generic-type-arguments-is-mapped-to-dependent-type-how-are-cova – Dmytro Mitin Jan 13 '23 at 02:05

1 Answers1

2

It doesn't make sense for ValidatorClean to be covariant.

Lets say you have:

abstract class Animal

object Animal {
    def validate[A <: Animal : ValidatorClean](animal: A): Animal =
        implicitly[ValidatorClean[A]].apply(animal)
}

class Cat {
    def canMeow: Boolean = ???
}

class Dog {
    def canBark: Boolean = ???
}

By making ValidatorClean covariant, you're saying that ValidatorClean[Dog] is a sub-type of ValidatorClean[Animal], which means that if you need a ValidatorClean[Animal], you will also accept a ValidatorClean[Dog] or ValidatorClean[Cat] for that matter.

So let's suppose we have an Animal, but we don't know it's sub-type.

 val unknown: Animal = new Dog // perhaps the Animal really came from a List

Now I write:

Animal.validate(unknown)

What happens? If there is an implicit ValidatorClean[Dog] available, validate will gladly accept it. Perhaps it looks like:

implicit validateDog = ValidatorClean[Dog](dog => if (dog.canBark) dog else ???)

But how can this function that accepts a Dog and calls canBark also process an arbitrary Animal? It can't.

Similarly, a ValidatorClean[Traversable[String]] would also resolve for a ValidatorClean[Any], even though Any does not have a map method, so it cannot work.

Michael Zajac
  • 55,144
  • 7
  • 113
  • 138
  • Hey @mk thank you for your answer. I think I got covariance and contravariance mixed up. I am striving for `case class ValidatorClean[-A](apply: A => A)` then, so that a `ValidatorClean[Dog]` accepts a `ValidatorClean[Animal]`. This doesn't compile either, since `A` is also covariant in the function. (Also, I accept suggestions as to whether edit the original question, make a new one, or leave it just as it is; i want to honor your answer). – rpiaggio May 12 '16 at 04:42
  • 1
    It doesn't work if it's contravariant either. If you have a `ValidatorClean[Animal]` (which holds a function `Animal => Animal`) and you try to use it as a `ValidatorClean[Dog]` (which holds a function `Dog => Dog`, then somehow the `ValidatorClean[Animal]` needs to know how to produce a `Dog`. – Michael Zajac May 12 '16 at 12:46
  • I see. I still think there should be a way of providing a function that applies to `A` or any of its subtypes and produce another exact `A` (that's why I mentioned making it invariant in the subject). Like in the example, where `_.map(_.trim)` applies to `Traversable[String]` or any of its subclasses, and given a `Seq[String]` produces another `Seq[String]`, given a `List[String]` produces a `List[String]`, etc. – rpiaggio May 12 '16 at 14:39
  • @rpiaggio You can have `case class ValidatorClean[+A](apply: (_ <: A) => A)` or `case class ValidatorClean[+A, B <: A](apply: B => A)` or `case class ValidatorClean[+A, -B <: A](apply: B => A)` – Dmytro Mitin Jan 13 '23 at 02:01