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?

+3


source to share


4 answers


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)

      

+3


source


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

      

+5


source


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)
      ^

      

+2


source


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)
                      ^

      

+1


source







All Articles