Compile-time safe models separation in Scala
Tags: scala, functional, design, architecture
Published: 2021-12-12 22:28:29

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/he9vamrfs52kpx3smihj.png)


## 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. 


```scala 
//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]`](https://typelevel.org/cats/datatypes/kleisli.html). 

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

```scala 
//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`](https://github.com/geirolz/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`](https://github.com/geirolz/scope/blob/main/core/src/main/scala/scope/ModelMapperK.scala) a type class to define a scoped model mapper 
- [`TypedScopeContext`](https://github.com/geirolz/scope/blob/main/core/src/main/scala/scope/TypedScopeContext.scala) a type to describe an available scope
- [`ScopeContext`](https://github.com/geirolz/scope/blob/main/core/src/main/scala/scope/ScopeContext.scala) a type to describe an available untyped scope
- [`Scope`](https://github.com/geirolz/scope/blob/main/core/src/main/scala/scope/Scope.scala) 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]`](https://github.com/geirolz/scope/blob/main/core/src/main/scala/scope/ModelMapperK.scala)

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**
```scala
//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**
```scala
//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`](https://github.com/geirolz/scope/blob/main/core/src/main/scala/scope/InScope.scala) passing the scope type 

```scala
//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.