The beauty of properly used statically typed languages

The real power of programming in Scala is not mimicking Haskell and overusing monads, but in taking advantage of its type system.

In Scala, we don’t have an excuse to write “Stringly typed” code, because we can easily create a class for every type in our application using value classes.

class FirstName(val value: String) extends AnyVal
class LastName(val value: String) extends AnyVal
class EmailAddress(val value: String) extends AnyVal

case class User(firstName: FirstName, lastName: LastName, emailAddress: EmailAddress)

Unlike type aliases, those are real classes (at least during compilation) and can be used to increase the readability and enforce the correctness of our code.

Would you like to help fight youth unemployment while getting mentoring experience?

Develhope is looking for tutors (part-time, freelancers) for their upcoming Data Engineer Courses.

The role of a tutor is to be the point of contact for students, guiding them throughout the 6-month learning program. The mentor supports learners through 1:1 meetings, giving feedback on assignments, and responding to messages in Discord channels—no live teaching sessions.

Expected availability: 15h/week. You can schedule the 1:1 sessions whenever you want, but the sessions must happen between 9 - 18 (9 am - 6 pm) CEST Monday-Friday.

Check out their job description.

(free advertisement, no affiliate links)

Obviously, when we need the underlying value, we have to write something like: firstName.value, lastName.value, etc. It gets annoying really quickly. What can we do instead of that? If we use Shapeless, we can move the additional code to where we create new instances:

import shapeless.tag
import shapeless.tag.@@

trait FirstNameTag
trait LastNameTag
trait EmailAddressTag

type FirstName = String @@ FirstNameTag
type LastName = String @@ LastNameTag
type EmailAddress = String @@ EmailAddressTag

case class User(firstName: FirstName, lastName: LastName, emailAddress: EmailAddress)

val user = User(
  firstName = tag[FirstNameTag][String]("first name"),
  lastName = tag[LastNameTag][String]("last name"),
  emailAddress = tag[EmailAddressTag][String]("")

s"${user.firstName} ${user.lastName}"

How many times did we pass function arguments in the wrong order? Sometimes tests can prevent it, sometimes they don’t. What happens when we have a real domain model? The invalid code does not even compile! It is much better than tests.

Can we get rid of even more tests and be sure there are no errors? What if we not only create a separate class or type for every domain type, but also for every state or context of a type?

Imagine that the users of my application can request an email confirmation. Some part of my code can look like that:

val recipient = tag[EmailAddressTag][String]("some string that is not an email address but I received it from the client")
def sendEmail(emailAddress: EmailAddress): Unit = ???

Will it compile? Unfortunately yes. Will it pass the tests? Maybe, it shouldn’t, but bad tests happen. Will it work correctly? Hell no.

Of course, we can validate the email address. we can even do it twice and repeat the validation in the sendEmail function, hopefully using the same validator. We can, but we all know repeating the validation in two or more places will be the cause of an error at some point.

def sendEmail(emailAddress: ValidEmailAddress): Unit = ???
def validateEmail(emailAddress: EmailAddress): Either[InvalidEmailAddress, ValidEmailAddress] = ???

validateEmail(recipient) match {
  case Left(error) => ???
  case Right(email) => sendEmail(email)

Is it better now? Not quite, I can still send spam to everyone because there is nothing to check whether the user verified his/her email address. But types can do even that!

def sendEmail(emailAddress: VerifiedEmailAddress): Unit = ???
Static typing is not about using primitive types (does anyone know the source of that illustration? I saw it on a wall in the office and just took a picture of it)
Static typing is not about using primitive types (does anyone know the source of that illustration? I saw it on a wall in the office and just took a picture of it)

Of course, it is not a silver bullet. Now we need separate DTO classes for our API, data model classes for data storage, and some custom code to map between them and the rich domain model. I think it is worth all of the effort because we encapsulate all of the complexity in types.

That code is boring

Sure, that code does not look impressive; it’s not complicated; we can hire a new programmer, and that person will quickly be productive because the code is self-documenting.

We tend to be excited by complexity; just look at functional programming conference talks. Does anyone want to talk about something simple? No, the more complicated or abstract concept, the better. Really, the better?

Domain-driven design or creating domain models seems to be too simple. Not easy, just simple. In my opinion, that is exactly why it is so beautiful and useful.

Remember to share on social media!
If you like this text, please share it on Facebook/Twitter/LinkedIn/Reddit or other social media.

If you want to contact me, send me a message on LinkedIn or Twitter.

Bartosz Mikulski
Bartosz Mikulski * MLOps Engineer / data engineer * conference speaker * co-founder of Software Craft Poznan & Poznan Scala User Group

Subscribe to the newsletter and get access to my free email course on building trustworthy data pipelines.