This document is going to describe the most important aspects of the code. I will start from top to bottom. It only contains the CLI part.
In the main method of the CLI you can start with:
cli[Id , InMemoryRepository].unsafeRunSync()
or
cli[Future , H2Repository].unsafeRunSync()
As you can see cli returns cats-effect's IO monad so nothing is happening unless you call unsafeRunSync() method.
InMemoryRepository and H2Repository are two different implementation of Repository trait:
trait Repository[F[_]] {
def getAllShowsOpenForSale(queryDate: java.sql.Date): F[MultiResult[Show]]
def findShowsAtSpecificDate(date: java.sql.Date): F[MultiResult[Show]]
def getSoldTickets(showId: Int, dayNumber: Int): F[MultiResult[SoldTicket]]
def insertShow(show: Show): F[ActionResult]
def insertSoldTicket(ticket: SoldTicket): F[ActionResult]
}
As you can see I used higher-kinded type for parameterizing the return type of methods so each implementation of
Repository
can have their own wrapper(effect). For instance I used cat's Id type for
InMemoryRepository
and Future for
H2Repository.
In this design we can easily add or change the return type instead of sticking to the fixed one like Future.
Also we can use the sync version Id in the tests and use the async version like Future or Monix's Task in the production.
I wanted to read the content of a CSV file, and insert the content to the database( In-memory or real). All the interaction with database should be done using a concrete instance of Repository. In this demo there are two instance but we can add more. How should I write a code that mutate database without caring the implementation of database or number of repositories? Meet DbInitializer.
object DbInitializer {
def initialize[F[_](shows: List[(String,String,String)], repo: Repository[F]): F[Either[Throwable,Unit]] = ???
}
Look at the return type of the initialize method. I wanted if all the content of the file, successfully written to the
database then return Unit otherwise return an exception.
Inside the initialize method, for converting List[F[Either[Throwable,Int]]] to F[List[Either[Throwable,Unit]]] I used
sequence method of cat's extension methods that requires a proper instance of Applicative type class:
def initialize[F[_] : Applicative](...) = {
…
val f_andList: F[List[ErrorOr[Int]]] = listOfF.sequence
val f_andEither: F[ErrorOr[List[Int]]] = f_andList.map( _.sequence )
...
}
There is a distinction between database models and business models. All repositories work with database model.
You can find database models in: Core/tickets4sale.core.db.models and business models in Core/tickets4sale.core.models.!

Here is the end result of inserting into database from raw data. Imaging we have a show like this (read from CSV file):
"AS YOU LIKE IT " , 2018-03-16 , "DRAMA"
This is the end result in the database:

All the logic is happening inside DbInitializer.
In this application we want two functionality:
- Get shows report
- Buy tickets Since this project is only CLI so I did not add buy ticket functionality to the UI but the code can handle buying tickets and generate appropriate report based on that. The gateway to all business logic is TicketingService:
class TicketingService[F[_]:Functor](repo: Repository[F]) {
def getReport(queryDate: String,showDate:String):F[Either[Throwable,Report]]
}
Since I needed to map the value inside the context of F, I requested the Scala compiler using context bound F[_] : Functor to bring an instance of Functor of F.
One of the most important thing inside business layer is finding the day of the show. Take the for example:
"AS YOU LIKE IT " , 2018-03-16 , "DRAMA"
For the query like this:
query date: 2018-09-09
show date: 2018-06-18

First I check that 2018-06-18 is between 2018-03-16 (openingDay) and 2018-06-23 (lastDay). If it is between these two dates then I calculate the day number. By calculating the day number I can easily find the show's place (BigHall or SmallHall) and the price of the show. When database's show model converted to business's show model I can easily call the show's instance methods. One of the useful method defined in Show is getReport:
def getReport(queryDate: LocalDate): ShowReport
I used the idea of some type for generating reports. The position of the query-date can produce 4 possible outcome:

The OpenForSale section is a little bit trickier than other 3 ones. Constructing ShowReport from 3 others is relatively simple, just need to change the status text, but in the OpenForSale section we need to calculate the available tickets and tickets left , so a little bit code needed. If the query-date is in the OpenForSale section something like this could be happen:

All the logic of calculating tickets number in the OpenForSale section is in the OpenForSale object and I used the concept of Monoid to sum the tickets. (using cat's foldMap extension method):
- Line number 31
- Line number 47
I used cat's Validated for validating file and date. You can find it in Core/tickets4sale.core.util.Validation. Also I composed 2 of these functions to simulate AND behavior in Core/tickets4sale.cli.Main; Line 49.
Like fabulous cat library, I also moved all the extension methods to syntax package. Why I need extension methods? Well, for instance LocalDate has isBefore or isAfter method but has not isBeforeOrEqual or isAfterOrEqual. You can find extension methods in Core/tickets4sale.core.syntax.
Let's focus on some important codes in CLI.
The interesting part is in the CLI method where I used for comprehension to define the application:
for {
_ <- printLogo()
_ <- printGreeting()
filename <- filenameLoop()
queryDate <- dateLoop("Enter query date:")
showDate <- dateLoop("Enter show date:")
repository <- createRepository[F,R](filename)
service = createService[F,R](repository)
report = getReport[F](service, queryDate, showDate)
} yield getJson[F](report)
And because the return value is wrapped in IO nothing will be happened until you call one of the unsafe methods.
When the user enters some invalid data, after prompting the error message, user will be asked to enter again. This mechanisms is achieved through recursive call. filenameLoop and dateLoop are recursive functions but they are not stack safe.
Inside for comprehension, all the monads must be the same. In a filenameLoop or dateLoop, the first monad is IO but after validation we ended up with Validated which is not monad. So I lifted the Validated to IO[Boolean].
implicit class ValidatedOps[A,B](value: Validated[A,B]) {
def liftToIO(implicit S: Show[A]): IO[Boolean] = value match {
case Valid(data) => IO.pure(true)
case Invalid(data) => IO {
println( S.show(data) )
false
}
}
}
Note: I used cat's Show type class to convert type B to string representation.
In the createRepository method, I should create a repository and pass an instance of it to DbInitializer. For creating an instance of a repository I created a type class Builder:
trait Builder[T] {
def build(): T
}
But DbInitializer only accepts a type that is a subtype of a Repository. To overcome this problem I used an upper type bound in the method signature. So overall we have:
def createRepository[F[_]:Applicative, R <: Repository[F]](csvFilename: String)(implicit repoBuilder: Builder[R]): IO[F[Either[Throwable, R]]] = {
}
Thank you for reading.