Spotlight @@ tagged types
in ScalaValentin Willscher
case class User(email: String, name: String)
val name = "John N. Types"
val email = "john@lovesjs.com"
val newUser = User(name, email) // \o/
def column(col: Map[String, String], name: String): Option[(String, String)] = ???
def column(col: Map[ColHead, RawData], name: ColHead): Option[(PrefixedColHead, Data)] = ???
case class UserName(value: String) extends AnyVal
case class Email(value: String) extends AnyVal
case class User(name: UserName, email: Email)
val name = UserName("John N. Types")
val email = Email("john@lovesjs.com")
val newUser = User(email, name) // Yay, compile error!
Why don't we always do this?
if(user.name.value.size > 20)
println("That's a very long name of yours")
(Or: many custom methods or implicit conversions)
type UserName = String
type Email = String
case class User(name: UserName, email: Email)
User("john@lovesjs.com", "John N. Types") // \o/
👎 No typesafety 👎
Show me your runtime-types
|
How many compiletime-types?
|
val x: String = "hi"
x.isInstanceOf[String] //true
val iAmNoString: Any = "hi".asInstanceOf[Any]
iAmNoString.isInstanceOf[CharSequence] //still true
iAmNoString.isInstanceOf[String] //still true
We can't change the runtime-type!
What about compiletime-types?
val x1: String = "hi"
val x2: Any = x1 //works
val x3: String = x1 //works
val x4: String = x2 //nope
//x2 is not of compiletime-type String
We made scalac lose types!
Can we also add types?
trait T
val x: String with T = "hi" //nope
Compiler knows that "hi"
is not of type T
trait T
val x: String with T =
"hi".asInstanceOf[String with T]
x.isInstanceOf[T] //false!
We just tagged String
with T
...the heck is that good for?
trait UserName
trait Email
case class User(name: String with UserName, email: String with Email)
Now, let's construct a user:
val user = User("user", "email") //Not even close
val name = "John".asInstanceOf[String with UserName]
val email = "john@type.safe".asInstanceOf[String with Email]
val user = User(name, email) //Good
val user = User(email, name) //Nope
object Email {
trait EmailTag
type Email = String with EmailTag
def apply(email: String): Email = email.asInstanceOf[Email]
def unapply(email: Email): Option[String] = Some(email)
}
//copy&paste for UserName
And now...
case class User(name: UserName, email: Email)
val user = User(UserName("John"), Email("john@type.safe"))
user match {
case User(UserName("John"), _) => println("Hi John")
case User(name, _) => println(s"You aren't John but $name")
}
println(user.name.size) // works: 4
How about methods?
case class Email(value: String) {
def isGerman: Boolean = value.toLowerCase.endsWith(".de")
}
Email("john@example.de").isGerman // true
The FP approach!
object Email {
trait EmailTag
type Email = String with EmailTag
def apply(email: String): Email = name.asInstanceOf[Email]
implicit class EmailOps(email: Email) {
def isGerman: Boolean = email.toLowerCase.endsWith(".de")
}
}
Email("john@example.de").isGerman // true
object Email {
trait EmailTag
type Email = String with EmailTag
def apply(email: String): Email = email.asInstanceOf[Email]
def subst[F[_]](fstr: F[String]) = fstr.asInstanceOf[F[Email]]
}
val strings: List[String] = List("a@b.de", "x@y.de")
val emails: List[Email] = Email.subst(strings)
val stringFuture: Future[String] = Future("a@b.de")
val emailFuture: Future[Email] = Email.subst(stringFuture)
Lists with millions of elements? Who cares!
trait EmailTag
type Email = String with EmailTag
trait PrivateTag
type PrivateEmail = Email with PrivateTag
trait PublicTag
type PublicEmail = Email with PublicTag
Composition rulez!
Any
... or better never do it
val anyList: List[Any] = List(42, Email("g@k.de"), "randstr")
anyList.foreach {
case int: Int => println(s"found Int: $int")
case email: Email => println(s"found Email: $email")
// case Email(value) => println(s"found Email: $email")
case string: String => println(s"found String: $string")
}
//Output:
//found Int: 42
//found String: g@k.de
//found String: randstr
val email: Email = Email("ab@c.de")
val emailAny: Any = Email("ab@c.de")
println( emailAny.isInstanceOf[Email] ) // false
println( email.isInstanceOf[Email] ) // true
val email: String = Email.apply("ab@c.de");
def email(): String = Main.this.email;
val emailAny: Object = Email.apply("ab@c.de");
def emailAny(): Object = Main.this.emailAny;
scala.this.Predef.println(scala.Boolean.box(
Main.this.emailAny().$isInstanceOf[String]()
.&&(
Main.this.emailAny().$isInstanceOf[Email.EmailTag]()
))
)
scala.this.Predef.println(scala.Boolean.box(true));
String @@ Email
syntax