The 'Given Defaults Pattern': How to Implement Class Defaults with a Macro in Scala 3 :: Jun 23, 2023

The database client Magnum has Repositories, which derive common SQL statements like findById, insert, and update at compile-time.

@Table(PostgresDbType, SqlNameMapper.CamelToSnakeCase)
case class User(
  @Id id: Long,
  firstName: Option[String],
  lastName: String,
  created: OffsetDateTime
) derives DbCodec

val userRepo = Repo[User, User, Long]

val countAfterUpdate = transact(ds):
  userRepo.deleteById(2L)
  userRepo.count

How does this work? One might think that Repo.apply is a macro, but it’s not. Instead, Repo is an open class designed for extension. Users can subclass Repo (or its supertype ImmutableRepo) to encapsulate and organize their queries:

class UserRepo extends ImmutableRepo[User, Long]:
  def firstNamesForLast(lastName: String)(using DbCon): Vector[String] =
    sql"""
      SELECT DISTINCT first_name
      FROM user
      WHERE last_name = $lastName
      """.query[String].run()
        
  // other User-related queries here

The way Repositories work in Magnum is by the ‘Given Defaults’ pattern. Looking at the definition of ImmutableRepo, we see it requires a given RepoDefaults parameter.

open class Repo[EC, E, ID](using defaults: RepoDefaults[EC, E, ID])
    extends ImmutableRepo[E, ID]:

  /** Deletes an entity using its id */
  def delete(entity: E)(using DbCon): Unit = defaults.delete(entity)

  /** Deletes an entity using its id */
  def deleteById(id: ID)(using DbCon): Unit = defaults.deleteById(id)

  /** Deletes ALL entities */
  def truncate()(using DbCon): Unit = defaults.truncate()

  // and so on

RepoDefaults is a trait that implements the default behavior of the Repo. If a user seeks to override any methods, they are free to do so.

trait RepoDefaults[EC, E, ID]:
  def count(using DbCon): Long
  def existsById(id: ID)(using DbCon): Boolean
  def findAll(using DbCon): Vector[E]
  // and so on

When compiling val userRepo = Repo[User, User, Long] or similar, Scala’s implicit search looks for a given RepoDefault in the companion object, and finds the Scala 3 macro.

object RepoDefaults:
  inline given genRepo[
      EC: DbCodec: Mirror.Of,
      E: DbCodec: Mirror.Of,
      ID: ClassTag
  ]: RepoDefaults[EC, E, ID] = ${ genImpl[EC, E, ID] }

Conclusions

The UX of this pattern is incredible and is appropriate for any Macro that wants to allow user extension.