Modeling domain objects


Spotlight @@ tagged types

in Scala



Valentin Willscher

Mars Climate Orbiter

1998 ⇾ Today


						case class User(email: String, name: String)
						

						val name  = "John N. Types"
						val email = "john@lovesjs.com"
						val newUser = User(name, email) // \o/
						
What does that function do?

							def column(col: Map[String, String], name: String): Option[(String, String)] = ???
							

							def column(col: Map[ColHead, RawData], name: ColHead): Option[(PrefixedColHead, Data)] = ???
							

Value case classes


						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!
						
  • ✔ Typesafe
  • ✔ Speaking types instead of "String"
  • ✔ Extensible

Why don't we always do this?

  • More typing
  • Naming things is hard
  • Runtime performance (boxing)


...and all over the place:

							if(user.name.value.size > 20)
							   println("That's a very long name of yours")
							

(Or: many custom methods or implicit conversions)

Type aliases

Lightweight type aliases

						type UserName = String
						type Email = String
						case class User(name: UserName, email: Email)
					
  • ✔ Little overhead
  • ✔ Speaking types
  • ✔ Same runtime performance as primitives

						User("john@lovesjs.com", "John N. Types") // \o/
					

👎 No typesafety 👎

Two worlds

Show me your runtime-types

										val x = "hi"
										

										x.isInstanceOf[Any]
										x.isInstanceOf[AnyRef]
										x.isInstanceOf[Object]
										x.isInstanceOf[java.io.Serializable]
										x.isInstanceOf[CharSequence]
										x.isInstanceOf[Comparable[_]]
										x.isInstanceOf[String]
										
How many compiletime-types?

										val x0 = "hi"
										val x1: Any = x0
										val x2: AnyRef = x0
										val x3: Object = x0
										val x4: java.io.Serializable = x0
										val x5: CharSequence = x0
										val x6: Comparable[_] = x0
										val x6b: Comparable[String] = x0
										val x7: String = x0
										
val x8: ??? = x0 val x9: ??? = x0

Changing 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!

Adding 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?

Tagged types in action


						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
  • ✔ Typesafe
  • ✔ As fast as primitves
  • ✔ Speaking types... but ugly 👎

						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
						

Functionality

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
					

Conversions


					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!

tagging tagged types

We can still mix up...
  • Private/hidden emails with public emails
  • Guestuser emails with admin emails or technical emails
  • Admin's private emails with admin's public emails

					trait EmailTag
					type Email = String with EmailTag

					trait PrivateTag
					type PrivateEmail = Email with PrivateTag

					trait PublicTag
					type PublicEmail = Email with PublicTag
					

Composition rulez!

The dark side

Be careful matching on 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
What does scalac say? (cleaned)

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

Type tagging summary

  • Very powerful and flexible concept
  • Top runtime performance
  • Minor edgecases
  • No constructor-constraints possible
  • Available in scalaz or shapeless
    • additional safety
    • more tooling, e.g. String @@ Email syntax
    • better type inference

Cheat sheet


Thank you & Questiontime

Contact: valentin@willscher.de
More stuff: github.com/valenterry