Connecting...

W1siziisimnvbxbpbgvkx3rozw1lx2fzc2v0cy9zawduawz5lxrly2hub2xvz3kvanbnl2jhbm5lci1kzwzhdwx0lmpwzyjdxq

Modeling and separation of concerns in functional programming by Andrea Vallotti

W1siziisijiwmtkvmdevmzevmtevmjqvntkvnty2l3blegvscy1wag90by01nzqwnzauanblzyjdlfsiccisinrodw1iiiwiotawedkwmfx1mdazzsjdxq

Take a walk through the steps to implement a simple domain logic with Software Architect, Andrea Vallotti

Using Scala and Cats library push side effects at the application boundaries and learn how to simplify the code.

 

'After my first experiment with functional programming, I decided to further study it in depth. Therefore, last March I attended “Lean and Functional Domain Modelling” workshop, organized by Avanscoperta, and held by Marcello Duarte. The workshop gave me good hints about functional modeling and fueled my curiosity to learn Scala and experiment more this paradigm.

In order to tackle this challenge I studied and practiced a lot. After some months, and several discussions with Matteo Baglini, I have been able to put together the puzzle, and I wrote this post. The main goal is to walk through the steps I took to implement a simple domain logic, described below, and the related persistence layer. Pushing side effects at the application boundaries, in order to create a pure domain logic, has been my North Star in this experiment.

As stated above, I used Scala as the programming language for the sample code. Moreover I used Cats library in order to obtain more functional abstraction than those available in the language itself.

As usual, the source code is on GitHub.

 

Domain definition

In this post I implement the domain used by Marcello Duarte in his workshop: the expense sheet process. This is the process followed by the employees of a company in order to request reimbursement for travel expenses.

Below are listed the three types of reimbursable expenses of this domain:

  • travel expenses, which need to specify departure and arrival cities;
  • food expenses, whose amount have to be less than a threshold defined by the company;
  • accommodation expenses, which need to specify the name of the hotel where the employee stayed.

An employee can claim reimbursement also for expenses other than those described above, but, in this case, she has to provide a detailed description. Finally, for all expenses, the date, antecedent to the filling of the expense sheet, and the due amount have to be specified.

In order to claim a reimbursement, the employee has to fill an expense sheet with her name and at least an expense. Once claimed, the expense sheet cannot be modified.

In this post, the approval process of the claim request is out of scope.

 

Roadmap

I am going to describe the approach I followed when developing the application. In particular:

  • how to implement the domain logic according to the pure functional paradigm;
  • how to use contract test in order to implement the persistence layer, which allowed me to create two completely exchangeable implementations: one that accesses PostgreSQL using Doobie, and an in-memory test double;
  • the implementation of the application services;
  • how to simplify the code by removing some of the effects previously introduced for error management.

 

Pure implementation of the domain logic

The first thing I did in order to implement the domain logic was to design the signatures of the functions of the domain algebra. Following the requirements described above, I came up with this:

1 object ExpenseService[Employee, Expense, OpenExpenseSheet, ClaimedExpenseSheet,
2   PendingClaim] {
3   def openFor(employee: Employee): ValidationResult[OpenExpenseSheet] = ???
4
5   def addExpenseTo(expense: Expense, expenseSheet: OpenExpenseSheet):
6     ValidationResult[OpenExpenseSheet] = ???
7 
8   def claim(expenseSheet: OpenExpenseSheet):
9     ValidationResult[(ClaimedExpenseSheet, PendingClaim)] = ???
10 }

At first I did not implement nor the operations neither the data types involved. Using the generic type and '???' notation of Scala, I just defined the signatures of the functions.

Since in pure functional programming functions should not have any effects except those declared in the function signature, you cannot use exceptions for error management. For this reason I used the effect 'ValidationResult' as the return type of all the functions.

'ValidationResult' is an alias of the generic class 'ValidateNel' provided by Cats. Such class is an applicative which could contain a valid result or a non empty list of errors. In this way, just looking at the function signature, the user could understand that the computations could return a valid result, e.g. 'OpenExpenseSheet' for 'openFor', or a list of errors.

After this first analysis, I decided to implement the data types needed by the above depicted operations. Therefore I defined the following classes and traits.

1 sealed case class Employee (id : EmployeeId, name: String, surname: String)
2 
3 sealed case class EmployeeId(uuid: UUID)
1 sealed trait Expense {
2   def cost: Money
3   def date: Date
4 }
5
6 case class TravelExpense(cost: Money, date: Date, from: String, to: String)
7   extends Expense
8 
9 case class FoodExpense(cost: Money, date: Date) extends Expense
10
11 case class AccommodationExpense(cost: Money, date: Date, hotel: String) extends Expense
12
13 case class OtherExpense(cost: Money, date: Date, description: String) extends Expense
1 sealed trait ExpenseSheet {
2   def id: ExpenseSheetId
3   def employee: Employee
4   def expenses: List[Expense]
5 }
6 
7 case class OpenExpenseSheet (id: ExpenseSheetId,
8                              employee: Employee,
9                              expenses:List[Expense]) extends ExpenseSheet
10 
11 case class ClaimedExpenseSheet (id: ExpenseSheetId,
12                                 employee: Employee,
13                                 expenses:List[Expense]) extends ExpenseSheet
14 
15 sealed case class ExpenseSheetId(uuid: UUID)
1 sealed trait Claim {
2   def id: ClaimId
3   def employee: Employee
4   def expenses: NonEmptyList[Expense]
5 }
6
7 case class PendingClaim (id: ClaimId, employee: Employee,
8   expenses: NonEmptyList[Expense]) extends Claim
9 
10 sealed case class ClaimId(uuid: UUID)

These classes have some interesting features which I would like to highlight:

  • all classes are 'case' classes. This allows, among other things, to use pattern matching on them;
  • traits are declared 'sealed'. This instructs Scala that all extending classes have to be placed in the same '.scala' file. This guarantees that the types used by the domain logic can be extended only from within the current project;
  • I defined an id case class for each classes that has one. By avoiding to directly use Java’s 'UUID', it is not possible, for example, to mistakenly use an id of 'ExpenseSheet' as an id of 'Claim'.

Using a 'sealed trait' with the related 'case class'es is useful for two purposes. Regarding 'ExpenseSheet', it allowed to define its feasible states ('Open' and 'Claimed'), while for the 'Expense', it allows define the allowed kinds of expense ('Travel', 'Accommodation', 'Food' and 'Other').

 

Smart constructor idiom

Once defined the data types, I implemented the business rules. Among them there are some which are related to the process, discussed further on, and others which are related to the validation of input data needed to create domain objects. For example:

  • for travel expenses is mandatory to specify the departure and arrival cities;
  • each expense item need to contain the amount and the date when it happened;
  • etc.
In order to implement this kind of rule and to ensure that the domain entities used by the application are valid, it is really useful the “smart constructor idiom” pattern described in “Functional and Reactive Domain Modeling”. In order to apply the pattern I just declared the above class constructors as 'private' and defined, in the related companion objects, the needed factory methods. These functions are responsible to validate data before creating the expected instance. The code below shows an example of this pattern:
1 object Expense {
2   private def validateDate(date: Date): ValidationResult[Date] = {
3     if (date == null || date.after(Calendar.getInstance.getTime))
4       "date cannot be in the future".invalidNel
5     else date.validNel
6 
7   private def validateCost(cost: Money): ValidationResult[Money] =
8     if (cost.amount <= 0) "cost is less or equal to zero".invalidNel
9     else cost.validNel
10 
11   private def maxCostLimitValidation(cost: Money): ValidationResult[Money] =
12     if (cost.amount >= 50) "cost is greater than or equal to 50".invalidNel
13     else cost.validNel
14 
15   def createFood(cost: Money, date: Date): ValidationResult[FoodExpense] =
16     (validateCost(cost), validateDate(date), maxCostLimitValidation(cost))
17       .mapN((c, d, _) => FoodExpense(c, d))
18 }

The code above is a typical usage of 'ValidationResult' applicative. The three required validations ('validateCost', 'validateDate' and 'maxCostLimitValidation') are independently executed and, thanks to Cats’ function 'mapN', the instance of 'ExpenseFood' is created only if all the validations successfully complete. On the other hand, if one or more validations fail, the result of 'mapN' will be an 'Invalid' containing the list of found errors. See 'Validated' for more details.

I implemented the smart constructors of other entities in the same way.

 

Domain service

 

Once defined all the data types of the domain and the related smart constructors, the implementation of the domain service described above has been straightforward.

1 object ExpenseService {
2   def openFor(employee: Employee): ValidationResult[OpenExpenseSheet] =
3     ExpenseSheet.createOpen(employee, List[Expense]())
4
5   def addExpenseTo(expense: Expense, expenseSheet: OpenExpenseSheet):
6     ValidationResult[OpenExpenseSheet] =
7     ExpenseSheet.createOpen(expenseSheet.id,
8                             expenseSheet.employee,
9                             expenseSheet.expenses :+ expense)
10 
11   def claim(expenseSheet: OpenExpenseSheet):
12     ValidationResult[(ClaimedExpenseSheet, PendingClaim)] =
13     expenseSheet.expenses match {
14       case h::t =>
15         (ExpenseSheet.createClaimed(expenseSheet.id,
16                                     expenseSheet.employee,
17                                     expenseSheet.expenses),
18         PendingClaim.create(expenseSheet.employee, NonEmptyList(h, t))).mapN((_, _))
19       case _ => "Cannot claim empty expense sheet".invalidNel
20    }
21 }

As already stated before, in order to have a pure functional domain logic is mandatory to avoid hidden side effects. For this reason the function 'claim' return a pair. The first is the claimed expense sheet, thus no more modifiable, while the second is the pending claim, which will follow the related approval process.

 

Database access

In order to implement the data access layer, I decided to use the repository pattern and the contract test approach to simultaneously develop a in-memory test double, to be used in the application service tests, and a real version which access PostgreSQL, to be used in the real application. I used ScalaTest as test library.

Let’s start from the trait which defines the function provided by the 'Employee' repository.

1 trait EmployeeRepository[F[_]] {
2   def get(id: EmployeeId) : F[ApplicationResult[Employee]]
3   def save(employee: Employee): F[ApplicationResult[Unit]]
4 }

The repository provides two simple operations: 'get' e 'save'. Moreover, it is generic w.r.t. the effect 'F[_]' which will be defined by the concrete implementations. As shown below, this allows to use different effects in the real and in-memory implementations.

In the signature of the methods I also used a concrete effects: 'ApplicationResult'. The latter is an alias of the generic type 'Either' of Scala, which is used when a computation may succeed or not. E.g., the 'get' function will return a 'Right' of 'Employee' if it will find the employee, otherwise it will return a 'Left' of 'ErrorList'. Unlike 'ValidateNel', Either is a monad, this will allow to write the application service more concisely.

Once defined the interface of the repository, I wrote the first test.

1 abstract class EmployeeRepositoryContractTest[F[_]](implicit M:Monad[F])
2   extends FunSpec with Matchers {
3
4   describe("get") {
5     it("should retrieve existing element") {
6       val id : EmployeeId = UUID.randomUUID()
7       val name = s"A $id"
8       val surname = s"V $id"
9       val sut = createRepositoryWith(List(Employee(id, name, surname)))
10 
11       run(sut.get(id)) should be(Right(Employee(id, name, surname)))
12     }
13   }
14
15   def createRepositoryWith(employees: List[Employee]): EmployeeRepository[F]
16 
17   def run[A](toBeExecuted: F[A]) : A
18 }

The test is defined in an abstract class since, to be actually run, it needs two support functions which will be defined differently for each concrete implementation:

  • 'createRepositoryWith', which allows to initialize the persistence (DB or memory) with the needed data, and returns a concrete instance of 'EmployeeRepository';
  • 'run', which allows to actually run the effects returned by the methods of the repository.

Moreover, the abstract class requires that a monad exists for the effect 'F'. The 'implicit' keyword instructs Scala to automatically look for a valid instance of 'Monad[F]' when creating an instance of the repository.

Now let’s see the in-memory implementation of the test.

1 object AcceptanceTestUtils {
2   case class TestState(employees: List[Employee],
3                        expenseSheets: List[ExpenseSheet],
4                        claims: List[Claim])
5
6   type Test[A] = State[TestState, A]
7 }
1 class InMemoryEmployeeRepositoryTest extends EmployeeRepositoryContractTest[Test]
2   implicit var state : TestState = _
3
4   override def createRepositoryWith(employees: List[Employee]):
5     EmployeeRepository[Test] = {
6     state = TestState(
7       employees,
8       List(),
9       List())
10     new InMemoryEmployeeRepository
11   }
12
13   override def run[A](executionUnit: Test[A]): A = executionUnit.runA(state).value
14 }

The test uses the 'State' monad with the state 'TestState' in order to simulate the persistence. 'State' is a structure used in functional programming to functionally express computations which requires changing the application state. This let us observe, for example, the changed application state when the 'save' function is used.

The 'InMemoryEmployeeRepository' is really simple. It uses the State functions to represent the desired elaboration.

1 class InMemoryEmployeeRepository extends EmployeeRepository[Test] {
2   override def get(id: EmployeeId): Test[ApplicationResult[Employee]] =
3     State {
4       state => (state, state.employees.find(_.id == id)
5                          .orError(s"Unable to find employee $id"))
6     }
7 
8   override def save(employee: Employee): Test[ApplicationResult[Unit]] =
9     State {
10       state => (state.copy(employees = employee :: state.employees), Right(()))
11     }
12 }

Analyzing the 'get' function, you can notice that the state does not change after the elaboration and the returned result is the required employee, if present. On the other hand, for the 'save' function the returned state is a copy of the previous one, with the new employee added to the corresponding list, while the return value is just 'Right' of 'Unit'. As you can see from the code, the initial instance of 'TestState' is never modified, instead a new instance is always created.

Once verified the correct behavior of 'InMemoryRepository', lets see the implementation of the test and production classes to access the data on PostgreSQL.

1 class DoobieEmployeeRepositoryTest
2   extends EmployeeRepositoryContractTest[ConnectionIO] {
3   implicit var xa: Aux[IO, Unit] = _
4 
5   override protected def beforeEach(): Unit = {
6     super.beforeEach()
7     xa = Transactor.fromDriverManager[IO](
8       "org.postgresql.Driver",
9       "jdbc:postgresql:postgres",
10       "postgres",
11       "p4ssw0r#"
12     )
13   }
14 
15   override def createRepositoryWith(employees: List[Employee]):
16     EmployeeRepository[ConnectionIO] = {
17     val employeeRepository = new DoobieEmployeeRepository
18 
19     employees.traverse(employeeRepository.save(_))
20       .transact(xa).unsafeRunSync()
21 
22     employeeRepository
23   }
24 
25   def run[A](toBeExecuted: ConnectionIO[A]): A =
26     toBeExecuted.transact(xa).unsafeRunSync
27 }
1 class DoobieEmployeeRepository extends EmployeeRepository[ConnectionIO] {
2   override def get(id: EmployeeId): ConnectionIO[ApplicationResult[Employee]] =
3     sql"select * from employees where id=$id".query[Employee]
4       .unique
5       .attempt
6       .map(_.leftMap({
7         case UnexpectedEnd => ErrorList.of(s"Unable to find employee $id")
8         case x => x.toError
9       }))
10 
11   override def save(employee: Employee): ConnectionIO[ApplicationResult[Unit]] =
12     sql"insert into employees (id, name, surname) values (${employee.id}, ${employee.name}, ${employee.surname})"
13       .update.run.attempt.map(_.map(_ =>()).leftMap(_.toError))
14 }

Beyond the particularities due to the use of Doobie for accessing the database, what is more interesting in this implementation is the usage of the effect 'ConnectionIO'. The latter is just an alias of 'Free' monad provided by Cats. 'Free' is a structure of the functional programming used to represent the side effects in a pure way. E.g., accessing a database, writing a log. etc.

'ConnectionIO' is a specialization of 'Free', provided by Doobie, to represent the interaction with databases. As shown in the 'run' method of the 'DoobieEmployeeRepositoryTest' class, the execution of this monad is unsafe since, interacting with an external system, exception can be thrown during its execution. This is clearly depicted in the name of the function 'unsafeRunSync'. What is more fascinating of this approach is that everything, except the method 'run', is purely functional and the error management can be done in a single place.

 

Application services

Once implemented the domain logic and data access layer, all I needed to do is to put everything together by implementing an application service. In order to verify the correct behavior of the latter, I created some tests which use the in-memory test doubles of the repositories. Lets see an example.

1 class ExpenseApplicationServiceTest extends FunSpec with Matchers {
2   implicit val er: InMemoryEmployeeRepository = new InMemoryEmployeeRepository()
3   implicit val esr: InMemoryExpenseSheetRepository =
4     new InMemoryExpenseSheetRepository()
5   implicit val cr: InMemoryClaimRepository = new InMemoryClaimRepository()
6
7   describe("addExpenseTo") {
8     it("should add an expense to an open expense sheet") {
9       val employee = Employee.create("A", "V").toOption.get
10       val expense = Expense.createTravel(
11         Money(1, "EUR"), new Date(), "Florence", "Barcelona").toOption.get
12       val expenseSheet = ExpenseSheet.createOpen(employee, List()).toOption.get
13 
14       val newState = ExpenseApplicationService
15         .addExpenseTo[Test](expense, expenseSheet.id)
16         .runS(TestState(List(employee), List(expenseSheet), List())).value
17 
18       newState.expenseSheets should be(
19         List(OpenExpenseSheet(expenseSheet.id, employee, List(expense))))
20     }
21   }
22 }

In order to let the test be more realistic I took advantage of the smart constructor previously defined. I used 'toOption.get' method to obtain the instance of the domain entities built by the smart constructors. This should never happen in a functional program. In fact, if the result of the smart constructor was of type 'Invalid', calling 'toOption.get' method on it would raise an exception. This would break the referential transparency of the code, which will not be pure anymore. I did it in the test code just because I was sure that data were valid.

The flow of the test above is quite simple:

  • arrange the application state as expected by the test;
  • invoke the application service function under test, using the 'runS' method of 'State' to actually execute the operations;
  • verify that the new application state matches the expected one.

Let’s see the complete implementation of the application service 'ExpenseApplicationService'.

1 object ExpenseApplicationService {
2   def openFor[F[_]](id: EmployeeId)
3     (implicit M:Monad[F],
4       er: EmployeeRepository[F],
5       esr: ExpenseSheetRepository[F]) : F[ApplicationResult[Unit]] =
6     (for {
7       employee <- er.get(id).toEitherT
8       openExpenseSheet <- ExpenseService.openFor(employee).toEitherT[F]
9       result <- esr.save(openExpenseSheet).toEitherT
10     } yield result).value
11
12   def addExpenseTo[F[_]](expense: Expense, id: ExpenseSheetId)
13     (implicit M:Monad[F],
14       esr: ExpenseSheetRepository[F]) : F[ApplicationResult[Unit]] =
15     (for {
16       openExpenseSheet <- getOpenExpenseSheet[F](id)
17       newOpenExpenseSheet <- ExpenseService.addExpenseTo(expense, openExpenseSheet)
18                                .toEitherT[F]
19       result <- esr.save(newOpenExpenseSheet).toEitherT
20     } yield result).value
21 
22   def claim[F[_]](id: ExpenseSheetId)
23     (implicit M:Monad[F],
24       esr: ExpenseSheetRepository[F],
25       cr: ClaimRepository[F]) : F[ApplicationResult[Unit]] =
26     (for {
27       openExpenseSheet <- getOpenExpenseSheet[F](id)
28       pair <- ExpenseService.claim(openExpenseSheet).toEitherT[F]
29       (claimedExpenseSheet, pendingClaim) = pair
30       _ <- esr.save(claimedExpenseSheet).toEitherT
31       _ <- cr.save(pendingClaim).toEitherT
32     } yield ()).value
33
34   private def getOpenExpenseSheet[F[_]](id: ExpenseSheetId)
35     (implicit M:Monad[F], esr: ExpenseSheetRepository[F]) :
36     EitherT[F, ErrorList, OpenExpenseSheet] =
37     for {
38       expenseSheet <- esr.get(id).toEitherT
39       openExpenseSheet <- toOpenExpenseSheet(expenseSheet).toEitherT[F]
40     } yield openExpenseSheet
41 
42   private def toOpenExpenseSheet(es: ExpenseSheet) :
43     ApplicationResult[OpenExpenseSheet] = es match {
44     case b: OpenExpenseSheet => Right(b)
45     case _ => Left(ErrorList.of(s"${es.id} is not an open expense sheet"))
46   }
47 }

As previously explained for the repository traits, the implementation of the application service is generic w.r.t. the effect 'F[_]'. Therefore also this piece of code is purely functional even if it interacts with the persistence.

It’s worth to point out that each function provided by the application service gets the needed repositories as implicit parameters. As shown in the test code, using the keyword 'implicit' makes the invocation of the functions easier since the caller does not need to explicitly pass the repository as parameters. Scala is responsible to locate valid instances, if any, and pass them to the function.

Using the for comprehensions notation of Scala the body of the functions are really clean. It looks like reading an imperative program. We are actually looking to the description of a computation, which will be executed only when the monad’s will be unwrapped (e.g. using the method 'run' of the 'State' monad).

Using the monads with for comprehensions let the computation fails as soon as any of the instruction fails, i.e. a function returns 'Left[T]'. In this case the computation stops and the error is returned to the client code.

 

Usage of EitherT monad trasformer

You probably noticed the use of 'toEitherT' methods almost everywhere. This is due to the fact that the for comprehensions notation works with one monad at a time. The functions involved instead use more monads. For example, the 'get' function of 'EmployeeRepository' returns 'F[ApplicationResult[Employee]]' while 'openFor' of 'ExpenseService' returns 'ValidationResult[OpenExpenseSheet]' which, to be precise, is not even a monad.

That’s why I decided to use the 'EitherT' monad transformer. Through this structure it is possible to combine an 'Either' monad with any other monad, in our case 'F', obtaining a monad whose effect is the composition of the effects of the original monads.

The 'toEitherT' functions that are seen in the code are used to transform all the types used in 'EitherT[F, _, ErrorList]'. In this way the for comprehensions can be used effectively and the code is much cleaner.

You can see the code before and after using the monad transformer by browsing the GitHub repository.

In the next section we will see how, by modifying the application code, it is possible to eliminate the use of 'EitherT' and further improve the readability of the application service.

 

Removing nested effects

As anticipated, in the code there are nested effects. This is due to the fact that I developed the application trying to use the appropriate effect for each layer. Once completed, the redundancy/verbosity of the code was evident due to the accumulation of these effects. Therefore, it was appropriate a refactoring of the program to simplify it as much as possible.

The 'ApplicationResult' type, which is simply an alias of the 'EitherT' monad, was introduced to handle application errors at the 'ExpenseApplicationService' service level. On the other hand, the 'ConnectionIO' monad, used by Doobie, also has the ability to handle errors. Obviously, the application logic can not directly use 'ConnectionIO' because this would make it unusable in different contexts (e.g. with another database access library). What would be needed is to guarantee that the generic effect 'F[_]' has the ability to handle errors. This would allow, for example, to simplify the type of return of the functions of 'ExpenseApplicationService' from so 'F[ApplicationResult[_]]' to so 'F[_]'.

To obtain the necessary guarantee, it was enough to request that a 'MonadError' exists for 'F'(see, line 3 below) instead of requesting just a 'Monad' as previously seen.

1 object ExpenseApplicationService {
2   def openFor[F[_]](id: EmployeeId)
3     (implicit ME:MonadError[F, Throwable],
4       er: EmployeeRepository[F],
5       esr: ExpenseSheetRepository[F]) : F[ExpenseSheetId] =
6     for {
7       employee <- er.get(id)
8       openExpenseSheet <- ExpenseService.openFor(employee)
9       _ <- esr.save(openExpenseSheet)
10     } yield openExpenseSheet.id
11 
12   def addExpenseTo[F[_]](expense: Expense, id: ExpenseSheetId)
13     (implicit ME:MonadError[F, Throwable],
14       esr: ExpenseSheetRepository[F]) : F[Unit] =
15     for {
16       openExpenseSheet <- getOpenExpenseSheet[F](id)
17       newOpenExpenseSheet <- ExpenseService.addExpenseTo(expense, openExpenseSheet)
18       result <- esr.save(newOpenExpenseSheet)
19     } yield result
20 
21   def claim[F[_]](id: ExpenseSheetId)
22     (implicit ME:MonadError[F, Throwable],
23       esr: ExpenseSheetRepository[F],
24       cr: ClaimRepository[F]) : F[ClaimId] =
25     for {
26       openExpenseSheet <- getOpenExpenseSheet[F](id)
27       pair <- ExpenseService.claim(openExpenseSheet)
28       (claimedExpenseSheet, pendingClaim) = pair
29       _ <- esr.save(claimedExpenseSheet)
30       _ <- cr.save(pendingClaim)
31     } yield pendingClaim.id
32 
33   private def getOpenExpenseSheet[F[_]](id: ExpenseSheetId)
34     (implicit ME:MonadError[F, Throwable],
35       esr: ExpenseSheetRepository[F]): F[OpenExpenseSheet] =
36     for {
37       expenseSheet <- esr.get(id)
38       openExpenseSheet <- toOpenExpenseSheet(expenseSheet)
39     } yield openExpenseSheet
40
41   private def toOpenExpenseSheet[F[_]](es: ExpenseSheet)
42     (implicit ME:MonadError[F, Throwable]) : F[OpenExpenseSheet] =
43     es match {
44       case b: OpenExpenseSheet => ME.pure(b)
45       case _ => ME.raiseError(new Error(s"${es.id} is not an open expense sheet"))
46     }
47 }

With this simple change, I was able to remove all invocation to 'toEitherT' from the code. At line 45 you can see how, using the 'MonadError', the way to notify errors to the caller is changed. The application service does not know how this happens, it only knows that the effect 'F' has this capability.

Obviously I had to adapt the rest of the code to this change, for example, I could simplify the 'DoobieEmployeeRepository' because I no longer need to map the exceptions in the 'ApplicationResult' type.

1 class DoobieEmployeeRepository(implicit ME: MonadError[ConnectionIO, Throwable]) 
2   extends EmployeeRepository[ConnectionIO] {
3   override def get(id: EmployeeId): ConnectionIO[Employee] =
4     sql"select * from employees where id=$id".query[Employee]
5       .unique
6       .recoverWith({
7         case UnexpectedEnd => ME.raiseError(new Error(s"Unable to find employee $id"))
8       })
9 
10   override def save(employee: Employee): ConnectionIO[Unit] =
11     sql"insert into employees (id, name, surname) values (${employee.id}, ${employee.name}, ${employee.surname})"
12       .update.run.map(_ => ())
13 }

The only exception still mapped is 'UnexpectedEnd' because in this case I wanted the repository to throw an exception with a more meaningful message for the domain.

It was not easy to find a refactoring method that would allow to maintain the code compilable, the tests green and, at the same time, would allow to replace the effect used by the functions in small steps. In fact, changing the effect at one point in the code inevitably led me to change the majority of the code. This made the code non-compilable, preventing me from performing the tests, and then verifying the correctness of the changes, for unacceptable periods.

For this reason, I decided to tackle the refactoring by duplication, namely:

  • for each set of functions and the related tests (e.g. 'ExpenseService' e 'ExpenseServiceTest'):

I created copies with suffix ME (Monad Error);

I modified the copied tests and functions to make them work correctly with the new effect;

  • once the whole production code has been duplicated and the correct behaviors of both versions has been verified through tests, I have been able to eliminate the old functions and rename the new ones by eliminating the suffix ME.

This process allowed me to refactor the code incrementally avoiding spending a lot of time without verifying the outcome of the made changes.

Conclusions

This experiment was very useful for several aspects. In particular, it let me:

  • improve the approach to functional domain modeling;
  • experiment and use some common effects of functional programming, and understand their possible applications;
  • understand to what extent the side effects, that inevitably a real software produces, can be pushed to the boundaries of the application.

Moreover, from a practical point of view I realized the difficulty in doing refactoring, especially when I modified the used effects. I am now convinced that in functional programming it is better to dedicate more attention to the design phase, at least for the effects, compared to OOP.

Overall this experiment was challenging. In fact, during the different development phases, I had to consider and deal with three distinct aspects (each equally important):

  • Scala syntax;
  • the concepts of functional programming;
  • the implementation provided by Cats and Scala of these concepts.

There are still aspects that I intend to deepen in the future. The most important is the composition of effects. In fact, in the above example the only used effect is 'ConnectionIO' to allow access to the DB, but a more complex application may require the use of other effects: write/read on filesystems, access resources using HTTP requests, etc. There are various approaches to dealing with these scenarios and I would like to try them out to understand their applicability.

I conclude by thanking Matteo Baglini for the passion he always demonstrates when explaining functional programming concepts, and for his precious suggestions that have been very useful to clean up the code and to better understand what I was doing.

Full speed ahead!'

 

This article was written by Andrea Vallotti and posted originally on Andreavallotti.tech