Txn

Overview

Txn is a monad which describes transactions involving TVars. It is executed via STM#commit:

import cats.implicits._
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import io.github.timwspence.cats.stm.STM

val stm = STM.runtime[IO].unsafeRunSync()
// stm: STM[IO] = io.github.timwspence.cats.stm.STM$Make$$anon$1$$anon$2@d28bcb6
import stm._

val prog: IO[(Int, Int)] = for {
  to   <- stm.commit(TVar.of(0))
  from <- stm.commit(TVar.of(100))
  _  <- stm.commit {
    for {
      balance <- from.get
      _       <- from.modify(_ - balance)
      _       <- to.modify(_ + balance)
    } yield ()
  }
  v   <- stm.commit((to.get, from.get).tupled)
} yield v
// prog: IO[(Int, Int)] = FlatMap(
//   ioe = FlatMap(
//     ioe = Uncancelable(
//       body = cats.effect.IO$$$Lambda$11093/1455923929@3b1cc7f2,
//       event = cats.effect.tracing.TracingEvent$StackTrace
//     ),
//     f = io.github.timwspence.cats.stm.STM$Make$$anon$1$$anon$2$$Lambda$11094/1107757704@3266cd4c,
//     event = cats.effect.tracing.TracingEvent$StackTrace
//   ),
//   f = <function1>,
//   event = cats.effect.tracing.TracingEvent$StackTrace
// )

val result = prog.unsafeRunSync()
// result: (Int, Int) = (100, 0)

Retries

STM#commit supports the concept of retries, which can be introduced via STM.check:

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import io.github.timwspence.cats.stm.STM

val stm = STM.runtime[IO].unsafeRunSync()
// stm: STM[IO] = io.github.timwspence.cats.stm.STM$Make$$anon$1$$anon$2@55f69474
import stm._

val to   = stm.commit(TVar.of(1)).unsafeRunSync()
// to: TVar[Int] = io.github.timwspence.cats.stm.STMLike$TVar@1d734f70
val from = stm.commit(TVar.of(0)).unsafeRunSync()
// from: TVar[Int] = io.github.timwspence.cats.stm.STMLike$TVar@408d287b

val txn: IO[Unit]  = stm.commit {
  for {
    balance <- from.get
    _       <- stm.check(balance > 100)
    _       <- from.modify(_ - 100)
    _       <- to.modify(_ + 100)
  } yield ()
}
// txn: IO[Unit] = FlatMap(
//   ioe = Uncancelable(
//     body = cats.effect.IO$$$Lambda$11093/1455923929@4405818b,
//     event = cats.effect.tracing.TracingEvent$StackTrace
//   ),
//   f = io.github.timwspence.cats.stm.STM$Make$$anon$1$$anon$2$$Lambda$11094/1107757704@2531f84b,
//   event = cats.effect.tracing.TracingEvent$StackTrace
// )

txn.unsafeRunSync() will block until the transaction succeeds (or throws an exception!). Internally, this is implemented by keeping track of which TVars are involved in a transaction and retrying any pending transactions every time a TVar is committed.

OrElse

STM.orElse is built on top of the retry logic and allows you to attempt an alternative action if the first retries:

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import io.github.timwspence.cats.stm.STM

val stm = STM.runtime[IO].unsafeRunSync()
// stm: STM[IO] = io.github.timwspence.cats.stm.STM$Make$$anon$1$$anon$2@223e2b52
import stm._

val to   = stm.commit(TVar.of(1)).unsafeRunSync()
// to: TVar[Int] = io.github.timwspence.cats.stm.STMLike$TVar@bd9019e
val from = stm.commit(TVar.of(0)).unsafeRunSync()
// from: TVar[Int] = io.github.timwspence.cats.stm.STMLike$TVar@6338c0b7

val transferHundred: Txn[Unit] = for {
  b <- from.get
  _ <- stm.check(b > 100)
  _ <- from.modify(_ - 100)
  _ <- to.modify(_ + 100)
} yield ()
// transferHundred: Txn[Unit] = Bind(
//   txn = Get(tvar = io.github.timwspence.cats.stm.STMLike$TVar@6338c0b7),
//   f = <function1>
// )

val transferRemaining: Txn[Unit] = for {
  balance <- from.get
  _       <- from.modify(_ - balance)
  _       <- to.modify(_ + balance)
} yield ()
// transferRemaining: Txn[Unit] = Bind(
//   txn = Get(tvar = io.github.timwspence.cats.stm.STMLike$TVar@6338c0b7),
//   f = <function1>
// )

val txn  = for {
  _    <- transferHundred.orElse(transferRemaining)
  f    <- from.get
  t    <- to.get
} yield f -> t
// txn: Txn[(Int, Int)] = Bind(
//   txn = OrElse(
//     txn = Bind(
//       txn = Get(tvar = io.github.timwspence.cats.stm.STMLike$TVar@6338c0b7),
//       f = <function1>
//     ),
//     fallback = Bind(
//       txn = Get(tvar = io.github.timwspence.cats.stm.STMLike$TVar@6338c0b7),
//       f = <function1>
//     )
//   ),
//   f = <function1>
// )

val result = stm.commit(txn).unsafeRunSync()
// result: (Int, Int) = (0, 1)

Aborting

Transactions can be aborted via STM.abort:

import cats.effect.IO
import cats.effect.unsafe.implicits.global
import io.github.timwspence.cats.stm.STM

val stm = STM.runtime[IO].unsafeRunSync()
// stm: STM[IO] = io.github.timwspence.cats.stm.STM$Make$$anon$1$$anon$2@49e19f3b
import stm._

val to   = stm.commit(TVar.of(1)).unsafeRunSync()
// to: TVar[Int] = io.github.timwspence.cats.stm.STMLike$TVar@1ad649ee
val from = stm.commit(TVar.of(0)).unsafeRunSync()
// from: TVar[Int] = io.github.timwspence.cats.stm.STMLike$TVar@59406edb

val txn  = for {
  balance <- from.get
  _       <- if (balance < 100)
               stm.abort(new RuntimeException("Balance must be at least 100"))
             else
               stm.unit
  _ <- from.modify(_ - 100)
  _ <- to.modify(_ + 100)
} yield ()
// txn: Txn[Unit] = Bind(
//   txn = Get(tvar = io.github.timwspence.cats.stm.STMLike$TVar@59406edb),
//   f = <function1>
// )

val result = stm.commit(txn).attempt.unsafeRunSync()
// result: Either[Throwable, Unit] = Left(
//   value = java.lang.RuntimeException: Balance must be at least 100
// )

Note that aborting a transaction will not modify any of the TVars involved.