Skip to content

Instantly share code, notes, and snippets.

@devshorts
Last active April 11, 2018 21:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save devshorts/32f216295d1acf064863d919a51e2c8b to your computer and use it in GitHub Desktop.
Save devshorts/32f216295d1acf064863d919a51e2c8b to your computer and use it in GitHub Desktop.
/**
* Encoding for "A is not a subtype of B"
* @note original credit: https://gist.github.com/milessabin/c9f8befa932d98dcc7a4
*/
trait NotTypeOf[A, B]
trait NotTypeImplicits {
// Uses ambiguity to rule out the cases we're trying to exclude
implicit def allowedType[A, B] : A NotTypeOf B = null
implicit def typeSuperTypeOfInvalid[A, B >: A] : A NotTypeOf B = null
implicit def typeSuperTypeOfInvalidAmbiguous[A, B >: A] : A NotTypeOf B = null
}
object NotTypeImplicits extends NotTypeImplicits
import org.scalatest.{FlatSpec, Matchers}
class NotTypeTest extends NotTypeImplicits {
def isNotType[T : Manifest: Not[Long]#Evidence](): Boolean = manifest[T].runtimeClass != classOf[Long]
}
class SomethingTest {
def isSomething[T : Manifest : NotNothing](): Boolean = manifest[T].runtimeClass != classOf[Nothing]
}
class NotUnitTest {
def isNotUnit[T : Manifest : NotUnit](): Boolean = manifest[T].runtimeClass != classOf[Unit]
}
class NotUnitNotNothingTest {
def isNotUnitOrNothing[T : Manifest : NotUnit : NotNothing](): Boolean =
manifest[T].runtimeClass != classOf[Unit] && manifest[T].runtimeClass != classOf[Nothing]
}
class NotTypeSpec extends FlatSpec with Matchers with NotTypeImplicits {
"Not type" should "allow" in {
val holder = new NotTypeTest
assert(holder.isNotType[String]())
}
it should "enforce" in {
"new NotTypeTest().isNotType[Long]()" shouldNot compile
}
"Not nothing" should "allow" in {
val holder = new SomethingTest
assert(holder.isSomething[String]())
}
it should "enforce" in {
"new SomethingTest().isSomething()" shouldNot compile
}
"Not unit" should "allow" in {
val holder = new NotUnitTest
assert(holder.isNotUnit[String]())
}
it should "enforce" in {
"new NotUnitTest().isNotUnit[Unit]()" shouldNot compile
}
"Not unit or nothing" should "allow" in {
val holder = new NotUnitNotNothingTest
assert(holder.isNotUnitOrNothing[String]())
}
it should "enforce for unit" in {
"new NotUnitOrNothingTest().isNotUnitOrNothing[Unit]()" shouldNot compile
}
it should "enforce for nothing" in {
"new NotUnitOrNothingTest().isNotUnitOrNothing()" shouldNot compile
}
}
package object types {
type Id[+A] = A
type Not[T] = {
type Evidence[U] = U NotTypeOf T
}
type NotNothing[T] = Not[Nothing]#Evidence[T]
type NotUnit[T] = Not[Unit]#Evidence[T]
}
@devshorts
Copy link
Author

devshorts commented Apr 11, 2018

How does this work? You're leveraging typeclass ambiguities to break the compiler. Walk through this

implicit def allowedType[A, B] : A NotTypeOf B = null
implicit def typeSuperTypeOfInvalid[A, B >: A] : A NotTypeOf B = null
implicit def typeSuperTypeOfInvalidAmbiguous[A, B >: A] : A NotTypeOf B = null

The first line is an allowed implicit if A and B are unrelated, we return an instance of NotTypeOf[A, B] (which in shorthand is writing with infix of A NotTypeOf B).

If B is a superclass of A the next two implicits match. When multiple implicits match the compiler throws an ambiguious implicits error and fails. If someone is asking for an instance of NotTypeOf[A, B] these two lines are hit and they become an error.

Now, how to specifically say "I dont want a Unit type"? All I have to do is ask for a NotTypeOf[Unit, Unit] which will cause the compiler to fail. Look at this:

def noUnits[T](implicit n: NotTypeOf[T, Unit]) = ???

I want some T where there is a typeclass that the compiler can generate that gives me NotTypeOf[T, Unit]. What happens if T is a Long?

noUnits[Long]

Long is not a subtype of Unit, so the first implicit gets hit:

implicit def allowedType[A, B] : A NotTypeOf B = null

And the typeclass (n) is null. Since we dont' really care what the value of n is this code compiles!

Now lets say that we do:

noUnits[Unit]

We're asking for an implicit n of type NotTypeOf[Unit, Unit]. But Unit IS a subtype of Unit! Any type is also its subtype and supertype. For example

object X {
  def t[T, Y >: T]()= ???

  def callsT = t[Unit, Unit]
}

compiles fine

Ok, back to it. We're asking for NotTypeOf[Unit, Unit] so the second AND third implicits are invoked! That causes a compiler error.

A few last caveats for the non-scala, the syntax of

def x[T : NotUnit]

Really desugars into

def x[T](implicit _: NotUnit[T])

Which desugars into

def x[T](implicit _: Not[Unit]#Evidence[T])

Which desguars into

def x[T](implicit _: NotTypeOf[Unit, T])

Why is all this shit cool? Cause you can now require generics to be passed in for methods that otherwise take no arguments. You can do stuff like:

def get[T : Manifest : NotNothing](key: String) = // do something with T

For example, imagine a redis client that has the method:

redis.get[YourObject](key = "your.redis.key")

In java you'd have to do

redis.get("your.redis.key", YourObject.class)

But even if you dont need the class object, you can still force type safety:

Imagine

def get[T: NotNothing]() = ???

In java you could call a method that requires a generic but not pass in the generic type, and java will infer Object as the type. So everything typechecks because it has no types. In Scala however, we can force typechecking and limit certain types on the generics.

--

Also, hi patrick!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment