Using context bounds “negatively” to ensure type class instance is absent from scope

I eventually solved this using an ambiguity-based solution that doesn’t require prioritizing using inheritance.

Here is my attempt at generalizing this.

We use the type Not[A] to construct negative type classes:

import scala.language.higherKinds

trait Not[A]

trait Monoid[_] // or import scalaz._, Scalaz._
type NotMonoid[A] = Not[Monoid[A]] 

trait Functor[_[_]] // or import scalaz._, Scalaz._
type NotFunctor[M[_]] = Not[Functor[M]]

…which can then be used as context bounds:

def foo[T: NotMonoid] = ...

We proceed by ensuring that every valid expression of Not[A] will gain at least one implicit instance.

implicit def notA[A, TC[_]] = new Not[TC[A]] {}

The instance is called ‘notA’ — ‘not’ because if it is the only instance found for ‘Not[TC[A]]’ then the negative type class is found to apply; the ‘A’ is commonly appended for methods that deal with flat-shaped types (e.g. Int).

We now introduce an ambiguity to turn away cases where the undesired type class is applied:

implicit def notNotA[A : TC, TC[_]] = new Not[TC[A]] {}

This is almost exactly the same as ‘NotA’, except here we are only interested in types for which an instance of the type class specified by ‘TC’ exists in implicit scope. The instance is named ‘notNotA’, since by merely matching the implicit being looked up, it will create an ambiguity with ‘notA’, failing the implicit search (which is our goal).

Let’s go over a usage example. We’ll use the ‘NotMonoid’ negative type class from above:

implicitly[NotMonoid[java.io.File]] // succeeds
implicitly[NotMonoid[Int]] // fails

def showIfNotMonoid[A: NotMonoid](a: A) = a.toString

showIfNotMonoid(3) // fails, good!
showIfNotMonoid(scala.Console) // succeeds for anything that isn't a Monoid

So far so good! However, types shaped M[_] and type classes shaped TC[_[_]] aren’t supported yet by the scheme above. Let’s add implicits for them as well:

implicit def notM[M[_], TC[_[_]]] = new Not[TC[M]] {}
implicit def notNotM[M[_] : TC, TC[_[_]]] = new Not[TC[M]] {}

implicitly[NotFunctor[List]] // fails
implicitly[NotFunctor[Class]] // succeeds

Simple enough. Note that Scalaz has a workaround for the boilerplate resulting from dealing with several type shapes — look for ‘Unapply’. I haven’t been able to make use of it for the basic case (type class of shape TC[_], such as Monoid), even though it worked on TC[_[_]] (e.g. Functor) like a charm, so this answer doesn’t cover that.

If anybody’s interested, here’s everything needed in a single snippet:

import scala.language.higherKinds

trait Not[A]

object Not {
  implicit def notA[A, TC[_]] = new Not[TC[A]] {}
  implicit def notNotA[A : TC, TC[_]] = new Not[TC[A]] {}

  implicit def notM[M[_], TC[_[_]]] = new Not[TC[M]] {}
  implicit def notNotM[M[_] : TC, TC[_[_]]] = new Not[TC[M]] {}
}

import Not._

type NotNumeric[A] = Not[Numeric[A]]
implicitly[NotNumeric[String]] // succeeds
implicitly[NotNumeric[Int]] // fails

and the pseudo code I asked for in the question would look like so (actual code):

// NotFunctor[M[_]] declared above
def notFunctor[M[_] : NotFunctor](m: M[_]) = s"$m is not a functor"

Update: Similar technique applied to implicit conversions:

import scala.language.higherKinds

trait Not[A]

object Not {
  implicit def not[V[_], A](a: A) = new Not[V[A]] {}
  implicit def notNot[V[_], A <% V[A]](a: A) = new Not[V[A]] {}
}

We can now (e.g.) define a function that will only admit values if their types aren’t viewable as Ordered:

def unordered[A <% Not[Ordered[A]]](a: A) = a

Leave a Comment