Last active
April 7, 2026 10:33
-
-
Save ghostdogpr/3920bec1c7eaf0ea556f5831624e82e7 to your computer and use it in GitHub Desktop.
Event Sourcing with PureLogic
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //> 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