Intro
I've been diving into functional programming in the last few months, and since I'm really intrigued by the Kotlin language, I've been using the Arrow library to toy around with a few things.
A few weeks ago, I've been researching for a guest lecture at university on Clean Architecture and while doing so, I stumbled upon this great blog Post by Mark Seemann, describing how using functional programming automatically leads to Clean Architecture (or with a language like Haskell may the compiler may even enforce Clean Architecture).
That inspired me to come up with a draft (checkout and build of the repo should be a breeze, if you're interested) of a restaurant reservation Software (staying true to Mark Seemann's domain ;) ). However, I'm not entirely sure if the use case layer in this draft can be called pure, and I wanted some feedback from people with more experience and knowledge with FP than myself.
Entity layer
A basic use case is trying to create a new reservation for a certain number of seats in our restaurant. I've modelled the entity layer for that the following way:
fun reservationPossible(
requestedSeats: Int,
reservedSeats: Int,
capacity: Int
): Either<RequestedTooManySeats, ReservationPossible> =
if (reservedSeats + requestedSeats <= capacity) {
ReservationPossible(requestedSeats + reservedSeats).right()
} else {
RequestedTooManySeats.left()
}
const val CAPACITY = 10
object RequestedTooManySeats : Error()
sealed class Error
data class ReservationPossible(val newNumberOfReservedSeats: Int)
Nothing too fancy going on here, just a function checking if a reservation with a certain number of requested seats would be possible or not. Some Error and Result classes are also down below as well as a (for the sake of simplicity) const val to model the capacity of our restaurant.
Frameworks/Adapters #1
To make sense in a real world application, some data would also need to be stored in and loaded from some sort of persistence layer. So, in the outermost layer of our onion architecture, there would be a Database which I mocked for this example:
suspend fun getCurrentlyReservedSeats(): Either<ReadError, Int> {
delay(1) // ... get stuff from db
return 4.right()
}
suspend fun saveReservation(value: String, reservationPossible: ReservationPossible): Either<WriteError, Long> {
delay(1) // ... writing something to db
return 42L.right() // newRecordId
}
abstract class DbError : Error()
object ReadError : DbError()
object WriteError : DbError()
Again, not too much going on here... Just stubs for Database read/write ops. Note however, that (by convention proposed by Arrow) these functions are marked with the suspend modifier as impure functions.
Use Case
So now for the use case, which basically describes our application flow:
- get number of currently reserved seats from DB
- check if the requested number of seats is still available
- if so, persist the new reservation
- and return the newly created reservation id
which is translated to code in the reservationUseCase function:
data class UseCaseData(
val requestedSeats: Int,
val reservationName: String,
val getCurrentlyReservedSeats: suspend () -> Either<ReadError, Int>,
val writeVal: suspend (String, ReservationPossible) -> Either<WriteError, Long>,
)
fun reservationUseCase(data: UseCaseData): suspend () -> Either<Error, UseCaseResultData> = {
data.getCurrentlyReservedSeats()
.flatMap { reservationPossible(data.requestedSeats, it, CAPACITY) }
.flatMap { data.writeVal(data.reservationName, it) }
.flatMap { UseCaseResultData(it).right() }
}
data class UseCaseResultData(val newRecordId: Long)
Here is the point, where it gets interesting: This function takes some UseCaseData as input and returns a suspend function to be executed at the program entry like this:
suspend fun main() {
reservationUseCase(
UseCaseData(
requestedSeats = 5,
reservationName = "John Dorian",
::getCurrentlyReservedSeats,
::saveReservation,
)
).invoke().fold(
ifLeft = { throw Exception(it.toString()) },
ifRight = { println(it.newRecordId) },
)
}
So now my questions are:
- Can the
reservationUseCasefunction itself be considered pure? I've read some blog post (taking F# as example language, however) suggesting that pure functions which receive impure functions as parameters could be pure, but cannot be guaranteed to be pure.reservationUseCasein this example clearly does receive impure functions with theUseCaseData. - If it can't be considered pure, how could one write a pure use case like the one described above in Kotlin and Arrow?