Compile-time safe models separation in Scala

geirolz

David Geirola

Posted on December 12, 2021

Compile-time safe models separation in Scala

https://github.com/geirolz/scope

Every code architecture relies on developers common sense and accuracy, erring is human, in some cases, the compiler can help us


Overview

Let's take a persistence layer as an example.
You don't want that the Entity class model, used in the persistence layer, is used in the domain or even worse as REST contract. You want to separate those concepts using different models for each layer.

Image description

Mapper approach

To achieve this you need a Mapper to convert the model from one scope to another and vice-versa(when needed).

This Mapper can be done using a separated object, or better, defining a method into the companion object of the model, keeping the domain model clean.



//persistence
case class UserEntity(id: Long, name: String)
object UserEntity {
  def toDomain(entity: UserEntity) : User = ???
}

//domain
case class User(id: Long, name: String)

//endpoint
case class UserContract(id: Long, name: String)
object UserContract{
  def fromDomain(user: User) : UserContract = ???
}


Enter fullscreen mode Exit fullscreen mode

Advantages

  • It's trivial

Problems

  • Bad composability (especially when the mapper is effectful)
  • No guarantee that the users will use the mappers in the right scope

Functional approach with Kleisli

From a functional programming point of view, this mapper is a simple morphism A => B, that becomes A => F[B] if we want allowing effects.

This is a Kleisli[F, A, B].

So, let's improve our above solution using Kleisli.



//persistence
object UserEntity {
  val toDomain: Kleisli[Id, UserEntity, User] = ???
}

//endpoint
object UserContract{
  val fromDomain: Kleisli[Id, User, UserContract] = ???
}


Enter fullscreen mode Exit fullscreen mode

Advantages

  • Composability, even with effects is simpler

Problems

  • No guarantee that the users will use the mappers in the right scope

Functional approach with Scope

https://github.com/geirolz/scope

Scope is a lightweight library to enforce the model layer separation at compile-time.
To do this, Scope simply verify the presence of an implicit Scope that corresponds to the Scope of the mapper.

So, there are two main actor in scope:

For pure mappers, there is ModelMapper[S <: Scope, A, B] which is a type alias to ModelMapperK[Id, S <: Scope, A, B]

If there isn't the required ScopeContext instance in the class | object scope the compilation fails.

Since there can't be more than one implicit instance for type in the scala scope (class, object, etc...) the compilation fails in the case of multiple ScopeContext defined.

Definition



//persistence
object UserEntity {    
    implicit val userEntityToDomain: ModelMapper[Scope.Persistence, UserEntity, User] =
      ModelMapper.scoped[Scope.Persistence](userEntity => ???)
}

//endpoint
object UserContract {    
    implicit val userToUserContract: ModelMapper[Scope.Endpoint, User, UserContract] =
      ModelMapper.scoped[Scope.Endpoint](user => ???)
}


Enter fullscreen mode Exit fullscreen mode

Usage



//persistence
implicit val scopeCtx: TypedScopeContext[Scope.Persistence] = 
  ScopeContext.of[Scope.Persistence]

val userEntity : UserEntity = ???
val user: User = userEntity.scoped.as[User]


//endpoint
implicit val scopeCtx: TypedScopeContext[Scope.Endpoint] = 
  ScopeContext.of[Scope.Endpoint]

val user : User = ???
val userContract: UserContract = user.scoped.as[UserContract]


Enter fullscreen mode Exit fullscreen mode

If you don't want to explicitly declare the implicit variable for the scope you can simply extend the trait InScope passing the scope type



//persistence
object MyPersistence extends InScope[Scope.Persistence] {
val userEntity : UserEntity = ???
val user: User = userEntity.scoped.as[User]
}

//endpoint
object MyEndpoint extends InScope[Scope.Endpoint] {
val user : User = ???
val userContract: UserContract = user.scoped.as[UserContract]
}

Enter fullscreen mode Exit fullscreen mode




Advantages

  • Composability, even with effects is simpler
  • Compile-time check that the users are using the mappers in the right scope

Problems

No guarantee that the user correctly maps the scope, this reduces the problems. The scopes are less than mappers

Conclusions

Scope doesn't totally resolve the problem, but for sure can help offering one more tool to have a clean and structured code.

💖 💪 🙅 🚩
geirolz
David Geirola

Posted on December 12, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related