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!")
}
}
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") // CompilessetValue(Set(1,2,3)) // CompilessetValue(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!
No comments:
Post a Comment