Search code examples
scalafunctional-programmingfunctorscala-catstype-level-computation

Cats: Implementing Contravariant for Predicates without a type alias?


Say that a Predicate is a function A => Boolean, I want to implement an instance of Cats's "Contravariant Functor" type class for predicates. I've also got an implicit class PredicateOps that defines union and intersect operators for predicates.

I have been able to get the instance to work using a type alias:

type Predicate[A] = A => Boolean

implicit val predicateContra = new Contravariant[Predicate] {
  override def contramap[A, B](fa: Predicate[A])(f: B => A): Predicate[B] =
    (b: B) => fa(f(b))
}

But when I do that, I have to coerce all my predicate functions to the alias like this:

val even: Predicate[Int] = (i: Int) => i % 2 == 0

Which I find annoying. So I wondered whether, rather than use the type alias, I could define predicateContra directly for a Function1 from a type variable A to Boolean, but I couldn't get it to work. Both of the following ideas give me a compiler error:

implicit val predicateContra = new Contravariant[Function1[_, Boolean]] {
// "Function1[_, Boolean] takes no type parameters, expected: one"

implicit def predicateContra[A] = new Contravariant[Function1[A, Boolean]] {
// "A => Boolean takes no type parameters, expected: one"

How can I tell the compiler that the first parameter of my Function1 should remain a "hole", while the second should be fixed to boolean? Is that even possible? Looking at the source code for cats, I found asterisks as type parameters in a bunch of places, but that too didn't work for me.


Solution

  • You can use kind projector, which allows you to refer to the "type hole" with an asterisk (*).

    This enables very simple syntax for defining a type of kind * -> *, that is, a unary type constructor (takes a single type to produce type). For example, a type that takes some type A to produce the type Map[A, Int] can be written simply as Map[*, Int].

    Then your code becomes:

    val strToBool: String => Boolean = _.contains("1")
    val intToStr: Int => String = _.toString
    
    def predicateContra = 
      new Contravariant[Function1[*, Boolean]] {
        override def contramap[A, B](fa: A => Boolean)(f: B => A): B => Boolean = 
          (b: B) => fa(f(b))
      }
    
    predicateContra.contramap(strToBool)(intToStr)(42) // false 
    predicateContra.contramap(strToBool)(intToStr)(41) // true
    

    If you don't want to use extra libraries, you can do it in plain Scala in a somewhat uglier way by using a type lambda:

    def predicateContra =
      new Contravariant[({ type lambda[A] = Function1[A, Boolean] })#lambda] {
          ...
      }