Thursday, February 16, 2017

Recently I learned... Union Types

Union types can add powerful functionality in cases where it would be easy to give up and lose type safety.

Suppose you have a typed configuration key -- for example, myIds.
myIds stores a value of type Set[Int], but because it's a configuration key, sometimes you might set it in code with a Set[Int], but sometimes you might want to set it from a String value from a config file.

So let's say myIds has a setValue method. We would like to be able to call setValue with a Set[Int] or with a String, but nothing else, and it should be safe at compile-time.

e.g.,:
myIds.setValue(Set(1, 2, 3))
myIds.setValue("1,2,3")

We could give up type-safety:

private var internalValue: Set[Int]
def setValue(value: Object): Union = {
  if (value.isInstanceOf[String]) {
    internalValue = convertFromString(value)
  } else if (value.isInstanceOf[Set]) { // Set of what? Grr type erasure is so evil.
    internalValue = value
  } else {
    throw new UglyAndHorribleRuntimeException("Wish we could have prevented this at compile-time!")
  }
}

How can we make it compile-time safe instead of accepting Object?

Enter Union Types!
Scala has intersect types -- just using "with" and you get a type made by intersecting two types.
It is possible to do Union types as well, but it's not built into the language so it's a bit more work.

Ideally we want to do something like:

type SetOrString = Set[Int] ∨ String
def setValue(value: SetOrString): Unit
...

Thanks to some help from StackOverflow, this can be accomplished! It's not 100% clean, but pretty good.

// http://stackoverflow.com/questions/3508077/how-to-define-type-disjunction-union-types// http://www.edofic.com/posts/2013-01-27-union-types.html// http://milessabin.com/blog/2011/06/09/scala-union-types-curry-howard/
// We want to be able to define union types so we can have, for example, a method that// accepts either an Int or a String, and for this method to be type-safe at compile-time,// and for it to work on primitives rather than having to use a boxed type such as Either.// Scala allows us to define type intersection using the "with" operator.// Scala does not have a union operator.// It's possible to define union using DeMorgan's Law: X or Y = NOT(NOT(X) AND NOT(Y))// Another way to think about it - from StackOverflow.com:// Given type ¬[-A] which is contravariant on A, by definition given A <: B we can write ¬[B] <: ¬[A],// inverting the ordering of types.// Given types A, B, and X, we want to express X <: A || X <: B.// Applying contravariance, we get ¬[A] <: ¬[X] || ¬[B] <: ¬[X].// This can in turn be expressed as ¬[A] with ¬[B] <: ¬[X] in which one of A or B must be a supertype// of X or X itself (think about function arguments).
import scala.language.higherKinds
/**  * Negation  * @tparam A Type to negate  */sealed trait ¬[-A]

/**  * Type set  */sealed trait TSet {
  type Compound[A]
  type Map[F[_]] <: TSet
}

/**  * Null type set  */sealed trait extends TSet {
  type Compound[A] = A  type Map[F[_]] = ∅
}

/**  * Type union  * Note that this type is left-associative for the sake of concision.  * @tparam T  * @tparam H  */sealed trait ∨[T <: TSet, H] extends TSet {
  // Given a type of the form `∅ ∨ A ∨ B ∨ ...` and parameter `X`, we want to produce the type  // `¬[A] with ¬[B] with ... <:< ¬[X]`.  type Member[X] = T#Map[¬]#Compound[¬[H]] <:< ¬[X]

  // This could be generalized as a fold, but for concision we leave it as is.  type Compound[A] = T#Compound[H with A]

  type Map[F[_]] = T#Map[F] ∨ F[H]
}


type UnionType = ∅ ∨ T ∨ String
def setValue[SetOrString : UnionType#Member](value: SetOrString)
  if (value.isInstanceOf[String]) {
    internalValue = convertFromString(value)
  } else { // Set[Int]
    internalValue = value
  }
}

No more runtime exception!
setValue("1,2,3") // Compiles
setValue(Set(1,2,3)) // Compiles
setValue(1) // Does not compile!
In my production code, I have a generic type Key that has a generic type parameter, so in my case instead of Set[Int] I would have some generic type T that is different based on whether it's an IntKey or an IntSetKey or something more complicated. The generics all work with this, and as a bonus, StringKey works as well (so String union String still works).
Understanding the set theory behind this is not a prerequisite for using it, so try it out and have some clean, type-safe code!