Skip to content

Instantly share code, notes, and snippets.

@ghostdogpr
Last active April 7, 2026 10:33
Show Gist options
  • Select an option

  • Save ghostdogpr/3920bec1c7eaf0ea556f5831624e82e7 to your computer and use it in GitHub Desktop.

Select an option

Save ghostdogpr/3920bec1c7eaf0ea556f5831624e82e7 to your computer and use it in GitHub Desktop.
Event Sourcing with PureLogic
//> using dep "io.github.kitlangton::neotype:0.4.10"
//> using dep "com.github.ghostdogpr::purelogic:0.2.0"
import neotype.*
import purelogic.*
import purelogic.syntax.*
import scala.math.Ordering.Implicits._
/**************************************************
****************** CAPABILITIES ******************
**************************************************/
trait Transition[Ev, S, Err] {
def run(ev: Ev): (State[S], Abort[Err]) ?=> Unit
}
trait EventSourcing[Ev, S, Err] {
protected given state: State[S]
protected given writer: Writer[Ev]
def writeEvent(event: Ev)(using transition: Transition[Ev, S, Err], abort: Abort[Err]): Unit = {
transition.run(event)
write(event)
}
def replayEvents(events: Iterable[Ev])(using transition: Transition[Ev, S, Err], abort: Abort[Err]): Unit =
events.foreach(transition.run)
}
object EventSourcing {
def apply[Ev, S, Err, A](body: EventSourcing[Ev, S, Err] ?=> A)(using s: State[S], w: Writer[Ev]): A = {
val eventSourcing = new EventSourcing[Ev, S, Err] {
protected given state: State[S] = s
protected given writer: Writer[Ev] = w
}
body(using eventSourcing)
}
}
inline def writeEvent[Ev, S, Err](event: Ev)(using transition: Transition[Ev, S, Err], es: EventSourcing[Ev, S, Err], abort: Abort[Err]) =
es.writeEvent(event)
inline def replayEvents[Ev, S, Err](
events: Iterable[Ev]
)(using transition: Transition[Ev, S, Err], es: EventSourcing[Ev, S, Err], abort: Abort[Err]): Unit = es.replayEvents(events)
def runProgram[A](account: Account, config: Config)(program: Program[A]): Either[String, (Vector[AccountEvent], Account, A)] =
Abort {
val (events, (newState, a)) = Reader(config)(Writer(State(account)(EventSourcing(program))))
(events, newState, a)
}
/**************************************************
****************** DOMAIN MODEL ******************
**************************************************/
type Amount = Amount.Type
object Amount extends Newtype[Int] {
override inline def validate(value: Int) = value >= 0
extension (amount: Amount) {
def +(other: Amount): Amount = Amount.unsafeMake(amount.unwrap + other.unwrap)
def -(other: Amount): Option[Amount] = Amount.make(amount.unwrap - other.unwrap).toOption
}
given Ordering[Amount] = Ordering.by[Amount, Int](_.unwrap)
}
case class Account(balance: Amount)
enum AccountEvent {
case Deposited(amount: Amount)
case Withdrawn(amount: Amount)
}
case class Config(maxDeposit: Amount, maxWithdrawal: Amount)
given Transition[AccountEvent, Account, String] with {
def run(ev: AccountEvent): (State[Account], Abort[String]) ?=> Unit =
ev match {
case AccountEvent.Deposited(amount) =>
val newBalance = get.balance + amount
set(Account(newBalance))
case AccountEvent.Withdrawn(amount) =>
val newBalance = (get.balance - amount).orFail("Insufficient balance")
set(Account(newBalance))
}
}
type Program[A] =
(
Reader[Config],
StateReader[Account],
Abort[String],
EventSourcing[AccountEvent, Account, String]
) ?=> A
/**************************************************
****************** DOMAIN LOGIC ******************
**************************************************/
def deposit(amount: Amount): Program[Unit] = {
ensure(amount <= read.maxDeposit, "Amount exceeds maximum deposit")
writeEvent(AccountEvent.Deposited(amount))
}
def withdraw(amount: Amount): Program[Unit] = {
ensure(amount <= read.maxWithdrawal, "Amount exceeds maximum withdrawal")
ensure(amount <= get.balance, "Insufficient balance")
writeEvent(AccountEvent.Withdrawn(amount))
}
/**************************************************
****************** TESTING ************************
**************************************************/
println(
runProgram(Account(Amount(100)), Config(Amount(1000), Amount(100))) {
deposit(Amount(50))
withdraw(Amount(30))
deposit(Amount(100))
}
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment