How to improve type safety for String or Double values?
I am wondering what is the best way to achieve type safety with my code, where all values ββcan be String
or Double
s, but are still incompatible. For example, I may have units in pounds and kilograms, but I am not allowed to assign them to another. Likewise, I can have a person ID like the String
animal ID lookup table like Map[String,Int]
, but I should be prohibited from looking at a person in the animals table.
Conceptually I'm looking for something like this:
class PersonId extends String
class AnimalId extends String
var p : PersonId = "1234"
var tab : Map[AnimalId,Int] = Map("foo" -> 5, "bar" -> 6)
tab.get(p) // Want this to cause a compile error
But there are several problems that don't work. Suggestions for something that fits the spirit?
source to share
You cannot expand String
for obvious reasons. I suggest using case classes for this:
case class PersonId(id:String)
case class AnimalId(id:String)
The syntax gets a little tricky, but not that much. And you can easily use case classes when matching patterns!
var p: PersonId = PersonId("1234")
var tab: Map[AnimalId,Int] = Map(AnimalId("foo") -> 5, AnimalId("bar") -> 6)
source to share
I would use value classes for this. It behaves almost exactly like a normal case class, but the compiler imposes some restrictions on it and usually it never has to waste time / memory creating the wrapper object - it can usually use the underlying value directly.
case class Person(value: String) extends AnyVal
case class Animal(value: String) extends AnyVal
source to share
One simple solution is to just use
case class PersonId(id:String)
case class AnimalId(id:String)
This solution is usually good enough.
If you want to play around a little with a Scala type system, you can do something like this -
trait Person
trait Animal
case class IdOf[T](s: String) extends AnyVal
implicit def string2idOf[T](s: String): IdOf[T] = IdOf(s)
var p: IdOf[Person] = "1234"
var tab: Map[IdOf[Animal], Int] = Map(("foo": IdOf[Animal]) -> 5, ("bar": IdOf[Animal]) -> 6)
tab.get(p)
// Error:(25, 11) type mismatch;
// found : com.novak.Program.IdOf[com.novak.Program.Person]
// required: com.novak.Program.IdOf[com.novak.Program.Animal]
// tab.get(p)
^
source to share
Another option is Scalaz tagged type . May be useful in some cases , because it allows you to combine your type with some other type without creating a new instance of that other type (value classes are similar for primitive types); however the new Scalaz requires you to explicitly remove it (s Tag.unwrap
), so it is not as useful as you might expect.
Example:
trait Person
val Person = Tag.of[Person]
val person = Prsn("Me")
Person.unwrap(person)
trait Animal
val Animal = Tag.of[Animal]
val animal = Anml("Me")
Animal.unwrap(person) //error
Animal.unwrap(animal)
Just quotes:
Suppose we need a way to express mass using kilogram, since kg is the international standard for the unit. We usually go to Double and call it day, but we cannot tell this from other Double values. Can we use a case class for this?
case class KiloGram(value: Double)
Even though it adds type safety, it's not fun to use because we have to call x.value every time to extract a value from it. Tagged type for rescue.
scala> sealed trait KiloGram defined trait KiloGram scala> def KiloGram[A](a: A): A @@ KiloGram = Tag[A, KiloGram](a) KiloGram: [A](a: A)scalaz.@@[A,KiloGram] scala> val mass = KiloGram(20.0) mass: scalaz.@@[Double,KiloGram] = 20.0 scala> sealed trait JoulePerKiloGram defined trait JoulePerKiloGram scala> def JoulePerKiloGram[A](a: A): A @@ JoulePerKiloGram = Tag[A, JoulePerKiloGram](a) JoulePerKiloGram: [A](a: A)scalaz.@@[A,JoulePerKiloGram] scala> def energyR(m: Double @@ KiloGram): Double @@ JoulePerKiloGram = JoulePerKiloGram(299792458.0 * 299792458.0 * Tag.unsubst[Double, Id, KiloGram](m)) energyR: (m: scalaz.@@[Double,KiloGram])scalaz.@@[Double,JoulePerKiloGram] scala> energyR(mass) res4: scalaz.@@[Double,JoulePerKiloGram] = 1.79751035747363533E18 scala> energyR(10.0) <console>:18: error: type mismatch; found : Double(10.0) required: scalaz.@@[Double,KiloGram] (which expands to) AnyRef{type Tag = KiloGram; type Self = Double} energyR(10.0) ^
source to share