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:

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:

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