Compile-time safe models separation in Scala
David Geirola
Posted on December 12, 2021
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.
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 = ???
}
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] = ???
}
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:
-
ModelMapperK
a type class to define a scoped model mapper -
TypedScopeContext
a type to describe an available scope -
ScopeContext
a type to describe an available untyped scope -
Scope
an ADT to define available scopes
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 => ???)
}
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]
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]
}
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.
Posted on December 12, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.