StateT
API Documentation: IndexedStateT
StateT[F[_], S, A] is a data type that generalizes State with the
ability to compose with effects in F[_]. Because StateT is defined
in terms of F, it is a monad only if F is a monad. Additionally,
StateT may acquire new capabilities via F: for example, if F is
capable of error handling via MonadThrow[F], then Cats derives an
instance of MonadThrow[StateT[F, S, *]].
The type parameters are:
F[_]represents the effect in which the computation is performed.Srepresents the underlying state, shared between each step of the state machine.Arepresents the return value.
It can be seen as a way to add the capability to manipulate a shared
state to an existing computation in the context of F.
This definition could be confusing, but it will become clear after
learning the State monad and by the example below.
StateT and State Relationship
StateT is a monad transformer for
State, in
particular State is
defined as a StateT with
Eval as the effect
F.
import cats.data.StateT
import cats.Eval
type State[S, A] = StateT[Eval, S, A]
Therefore, StateT exposes the same methods of
State, such as:
modify, get and set. Plus additional methods, that handles
effectful computations, eg: modifyF, setF and liftF.
Example: Table Reservation System
In this example, we are going to model a Table Reservation System. To do so, we need to keep track of the tables and the current reservations for each of them. The end-user can then try to insert a booking for a specific table and time. If such a table is available, then the booking is placed and the state is updated, otherwise, an error is returned.
To simplify the logic, for each reservation we will just consider a
single LocalTime starting at the beginning of the hour.
Let's start with defining the type alias for the effect type:
import cats.data.{StateT, NonEmptyList}
import cats.syntax.all._
import java.time.LocalTime
type ThrowableOr[A] = Either[Throwable, A]
We will use an Either[Throwable, A] to model either success Right(a) or failure Left(ex).
Now, we need to implement/define:
- The type representing the reservation.
- The type representing the state of the Table Reservation System. It will wrap around a collection of Reservations.
- An initial state, that will be just empty (no reservations).
- A custom
Throwableto be used in case of an error. - The logic for the booking insertion. We can take advantage of the
method
modifyFlater on to apply it to the system state.
In addition, we can implement a simple function that will evaluate a
NonEmptyList of reservations, inserting them one by one.
object TableReservationSystem {
final case class ReservationId(tableNumber: Int, hour: LocalTime)
final case class Reservation(id: ReservationId, name: String)
final case class Reservations(reservations: List[Reservation]) {
def insert(reservation: Reservation): ThrowableOr[Reservations] =
if (reservations.exists(r => r.id == reservation.id))
Left(new TableAlreadyReservedException(reservation))
else Right(Reservations(reservations :+ reservation))
}
final class TableAlreadyReservedException(
reservation: Reservation
) extends RuntimeException(
s"${reservation.name} cannot be added because table number ${reservation.id.tableNumber} is already reserved for the ${reservation.id.hour}"
)
val emptyReservationSystem: Reservations = Reservations(List.empty)
def insertBooking(
reservation: Reservation
): StateT[ThrowableOr, Reservations, Unit] =
StateT.modifyF[ThrowableOr, Reservations](_.insert(reservation))
def processBookings(
bookings: NonEmptyList[Reservation]
): ThrowableOr[Reservations] =
bookings
.traverse_(insertBooking)
.runS(emptyReservationSystem)
}
That's it, we can finally test the code above providing a simple example input and print out the result.
val bookings = NonEmptyList.of(
TableReservationSystem.Reservation(
TableReservationSystem
.ReservationId(tableNumber = 1, hour = LocalTime.parse("10:00:00")),
name = "Gandalf"
),
TableReservationSystem.Reservation(
TableReservationSystem
.ReservationId(tableNumber = 2, hour = LocalTime.parse("10:00:00")),
name = "Legolas"
),
TableReservationSystem.Reservation(
TableReservationSystem
.ReservationId(tableNumber = 1, hour = LocalTime.parse("12:00:00")),
name = "Frodo"
),
TableReservationSystem.Reservation(
TableReservationSystem
.ReservationId(tableNumber = 2, hour = LocalTime.parse("12:00:00")),
name = "Bilbo"
),
TableReservationSystem.Reservation(
TableReservationSystem
.ReservationId(tableNumber = 3, hour = LocalTime.parse("13:00:00")),
name = "Elrond"
),
TableReservationSystem.Reservation(
TableReservationSystem
.ReservationId(tableNumber = 1, hour = LocalTime.parse("16:00:00")),
name = "Sauron"
),
TableReservationSystem.Reservation(
TableReservationSystem
.ReservationId(tableNumber = 2, hour = LocalTime.parse("16:00:00")),
name = "Aragorn"
),
TableReservationSystem.Reservation(
TableReservationSystem
.ReservationId(tableNumber = 2, hour = LocalTime.parse("18:00:00")),
name = "Gollum"
)
)
// bookings: NonEmptyList[TableReservationSystem.Reservation] = NonEmptyList(
// head = Reservation(
// id = ReservationId(tableNumber = 1, hour = 10:00),
// name = "Gandalf"
// ),
// tail = List(
// Reservation(
// id = ReservationId(tableNumber = 2, hour = 10:00),
// name = "Legolas"
// ),
// Reservation(
// id = ReservationId(tableNumber = 1, hour = 12:00),
// name = "Frodo"
// ),
// Reservation(
// id = ReservationId(tableNumber = 2, hour = 12:00),
// name = "Bilbo"
// ),
// Reservation(
// id = ReservationId(tableNumber = 3, hour = 13:00),
// name = "Elrond"
// ),
// Reservation(
// id = ReservationId(tableNumber = 1, hour = 16:00),
// name = "Sauron"
// ),
// Reservation(
// id = ReservationId(tableNumber = 2, hour = 16:00),
// name = "Aragorn"
// ),
// Reservation(
// id = ReservationId(tableNumber = 2, hour = 18:00),
// name = "Gollum"
// )
// )
// )
TableReservationSystem.processBookings(bookings)
// res1: ThrowableOr[TableReservationSystem.Reservations] = Right(
// value = Reservations(
// reservations = List(
// Reservation(
// id = ReservationId(tableNumber = 1, hour = 10:00),
// name = "Gandalf"
// ),
// Reservation(
// id = ReservationId(tableNumber = 2, hour = 10:00),
// name = "Legolas"
// ),
// Reservation(
// id = ReservationId(tableNumber = 1, hour = 12:00),
// name = "Frodo"
// ),
// Reservation(
// id = ReservationId(tableNumber = 2, hour = 12:00),
// name = "Bilbo"
// ),
// Reservation(
// id = ReservationId(tableNumber = 3, hour = 13:00),
// name = "Elrond"
// ),
// Reservation(
// id = ReservationId(tableNumber = 1, hour = 16:00),
// name = "Sauron"
// ),
// Reservation(
// id = ReservationId(tableNumber = 2, hour = 16:00),
// name = "Aragorn"
// ),
// Reservation(
// id = ReservationId(tableNumber = 2, hour = 18:00),
// name = "Gollum"
// )
// )
// )
// )
TableReservationSystem.processBookings(
bookings :+ TableReservationSystem.Reservation(
TableReservationSystem
.ReservationId(tableNumber = 1, hour = LocalTime.parse("16:00:00")),
name = "Saruman"
)
)
// res2: ThrowableOr[TableReservationSystem.Reservations] = Left(
// value = repl.MdocSession$MdocApp0$TableReservationSystem$TableAlreadyReservedException: Saruman cannot be added because table number 1 is already reserved for the 16:00
// )
The full source code of this example can be found at this gist or scastie