Alex
Alex

Reputation: 53

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.

Upvotes: 1

Views: 434

Answers (1)

slouc
slouc

Reputation: 9698

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] {
      ...
  }

Upvotes: 5

Related Questions