Getting started

libraryDependencies += "io.github.timwspence" %% "cats-stm" % "0.11.0"

Scastie

The quickest way to try it out is via Scastie

Defining a transaction

Transactions have type Txn[A] and are composed of operations on mutable TVar references.

import cats.effect.IO
import io.github.timwspence.cats.stm._

def wibble(stm: STM[IO])(tvar: stm.TVar[Int]): stm.Txn[Int] = {
  for {
    current <- tvar.get
    updated = current + 1
    _ <- tvar.set(updated)
  } yield updated
}

TVar is effectively parameterized on an effect F[_]. We make the API nicer to work with by making it a dependent type of the STM runtime STM[F] (see the stm.TVar[Int] above).

Committing a transaction

Transactions are commited via STM[F]#commit.

val run: IO[Int] = {
  def run(stm: STM[IO]): IO[Int] = {
    import stm._
    
    for {
      tvar <- stm.commit(TVar.of[Int](0))
      res  <- stm.commit(wibble(stm)(tvar))
    } yield res
  }
  
  STM.runtime[IO].flatMap(run(_))
}

Retrying a transaction

Retries can be introduced via the check combinator. If this transaction is committed then it will retry the commit until the predicate passed to check is satisfied.


def waitTillValueExceeds100(tvar: TVar[Int]): IO[Int] =
  stm.commit(
    for {
      current <- tvar.get
      _       <- stm.check(current > 100)
    } yield current
  )

Alternatives

The combinator orElse introduces an alternative transaction to try in the event that one retries.

def transferAvailableFunds(from1: TVar[Int], from2: TVar[Int], to: TVar[Int]): IO[Unit] =
  def transferFrom(from: TVar[Int]): Txn[Unit] =
    for {
      current <- from.get
      _       <- stm.check(current > 100)
      _       <- from.modify(_ - 100)
      _       <- to.modify(_ + 100)
    } yield ()

  stm.commit(
    transferFrom(from1).orElse(transferFrom(from2))
  )