COMPOSABLE EVENT SOURCING WITH MONADS Daniel Krzywicki Étienne Vallette d’Osia

@eleaar @dohzya

Scala.io 2017-10-03 https://github.com/dohzya/scalaio-2017-esmonad

The following is based on a true story

//

3

Outline

01 //

Minimal model for event sourcing

02 //

The problem of composition

03 //

The Functional approach

04 //

Further possibilities

//

4

Introduction to Event Sourcing

//

5

Instead of storing state, store changes to the state

//

6

Introduction :: event store

Event store

0

Events n

Changes to the system state are reified as events and appended to an event store. //

7

Introduction :: replaying state

Event store

0

Events

State n

The system state is said to be projected/replayed from the store using event handlers //

8

Introduction :: computing new events

Event store

0

Events

State n

The state can be used to compute new events, in response to either external signals or internal logic //

9

Introduction :: partial replays

Event store

0

State(t2)

2

State(tn) n

We can easily replay only a part of the events to know the state of the system at any point in time // 10

Introduction :: multiple handlers

Event store

0

n

We can also use different handlers to interpret the events

// 11

Introduction :: distributed integration

System A

System C System B

By projecting events between distributed systems, we can easily have an architecture which is reactive, fault-tolerant, and scalable. // 12

What the talk is not about

-

Event sourcing frameworks Infrastructure (Kafka, MongoDB, …) Architecture (Event Store, sharding, partitioning) Error handling

// 13

What the talk is about

Track theme: Type & Functional Programming Functional => Focus on composability Programming => Focus on model and dev API

// 14

01 //

Minimal Model for Event Sourcing

// 15

01.1 //

Modeling the domain - Turtles!

// 16

Basic model

case class Turtle(id: String, pos: Position, dir: Direction) case class Position(x: Int, y: Int) { val zero = Position(0, 0) def move(dir: Direction, distance: Int): Position = { … } } sealed trait Direction { def rotate(rot: Rotation): Direction = { … } } case object North extends Direction ; case object South extends Direction case object East extends Direction ; case object West extends Direction sealed trait Rotation case object ToLeft extends Rotation ; case object ToRight extends Rotation // 17

Basic model

case class Turtle(id: String, pos: Position, dir: Direction) case class Position(x: Int, y: Int) { val zero = Position(0, 0) def move(dir: Direction, distance: Int): Position = { … } } sealed trait Direction { def rotate(rot: Rotation): Direction = { … } } case object North extends Direction ; case object South extends Direction case object East extends Direction ; case object West extends Direction sealed trait Rotation case object ToLeft extends Rotation ; case object ToRight extends Rotation // 18

Basic model

case class Turtle(id: String, pos: Position, dir: Direction) case class Position(x: Int, y: Int) { val zero = Position(0, 0) def move(dir: Direction, distance: Int): Position = { … } } sealed trait Direction { def rotate(rot: Rotation): Direction = { … } } case object North extends Direction ; case object South extends Direction case object East extends Direction ; case object West extends Direction sealed trait Rotation case object ToLeft extends Rotation ; case object ToRight extends Rotation // 19

Basic model

case class Turtle(id: String, pos: Position, dir: Direction) case class Position(x: Int, y: Int) { val zero = Position(0, 0) def move(dir: Direction, distance: Int): Position = { … } } sealed trait Direction { def rotate(rot: Rotation): Direction = { … } } case object North extends Direction ; case object South extends Direction case object East extends Direction ; case object West extends Direction sealed trait Rotation case object ToLeft extends Rotation ; case object ToRight extends Rotation // 20

Basic model

case class Turtle(id: String, pos: Position, dir: Direction) case class Position(x: Int, y: Int) { val zero = Position(0, 0) def move(dir: Direction, distance: Int): Position = { … } } sealed trait Direction { def rotate(rot: Rotation): Direction = { … } } case object North extends Direction ; case object South extends Direction case object East extends Direction ; case object West extends Direction sealed trait Rotation case object ToLeft extends Rotation ; case object ToRight extends Rotation // 21

Basic model

case class Turtle(id: String, pos: Position, dir: Direction) case class Position(x: Int, y: Int) { val zero = Position(0, 0) def move(dir: Direction, distance: Int): Position = { … } } sealed trait Direction { def rotate(rot: Rotation): Direction = { … } } case object North extends Direction ; case object South extends Direction case object East extends Direction ; case object West extends Direction sealed trait Rotation case object ToLeft extends Rotation ; case object ToRight extends Rotation // 22

Domain logic

object Turtle { def create(id: String, pos: Position, dir: Direction): Either[String, Turtle] = if (tooFarAwayFromOrigin(pos)) Either.left("Too far away") else Either.right(Turtle(id, pos, dir)) def turn(rot: Rotation)(turtle: Turtle): Either[String, Turtle] = Right(turtle.copy(dir = turtle.dir.rotate(rot))) def walk(dist: Int)(turtle: Turtle): Either[String, Turtle] = { val newPos = turtle.pos.move(turtle.dir, dist) if (tooFarAwayFromOrigin(newPos)) Either.left("Too far away") else Either.right(turtle.copy(pos = newPos)) } } // 23

Domain logic

object Turtle { // commands have validation, so they can return an error def create(id: String, pos: Position, dir: Direction): Either[String, Turtle] = if (tooFarAwayFromOrigin(pos)) Either.left("Too far away") else Either.right(Turtle(id, pos, dir)) def turn(rot: Rotation)(turtle: Turtle): Either[String, Turtle] = Right(turtle.copy(dir = turtle.dir.rotate(rot))) def walk(dist: Int)(turtle: Turtle): Either[String, Turtle] = { val newPos = turtle.pos.move(turtle.dir, dist) if (tooFarAwayFromOrigin(newPos)) Either.left("Too far away") else Either.right(turtle.copy(pos = newPos)) } } // 24

Domain logic

object Turtle { // example of a command def create(id: String, pos: Position, dir: Direction): Either[String, Turtle] = if (tooFarAwayFromOrigin(pos)) Left("Too far away") else Right(Turtle(id, pos, dir)) def turn(rot: Rotation)(turtle: Turtle): Either[String, Turtle] = Right(turtle.copy(dir = turtle.dir.rotate(rot))) def walk(dist: Int)(turtle: Turtle): Either[String, Turtle] = { val newPos = turtle.pos.move(turtle.dir, dist) if (tooFarAwayFromOrigin(newPos)) Either.left("Too far away") else Either.right(turtle.copy(pos = newPos)) } } // 25

Domain logic

object Turtle { def create(id: String, pos: Position, dir: Direction): Either[String, Turtle] = if (tooFarAwayFromOrigin(pos)) Either.left("Too far away") else Either.right(Turtle(id, pos, dir)) // curried command are like already-configured command def turn(rot: Rotation)(turtle: Turtle): Either[String, Turtle] = Right(turtle.copy(dir = turtle.dir.rotate(rot))) def walk(dist: Int)(turtle: Turtle): Either[String, Turtle] = { val newPos = turtle.pos.move(turtle.dir, dist) if (tooFarAwayFromOrigin(newPos)) Either.left("Too far away") else Either.right(turtle.copy(pos = newPos)) } } // 26

Basic model - demo

def walkRight(dist: Int)(state: Turtle) = for { state1 <- Turtle.walk(dist)(state) state2 <- Turtle.turn(ToRight)(state1) } yield state2 val state = for { state1 <- Turtle.create("123", Position.zero, North) state2 <- walkRight(1)(state1) state3 <- walkRight(1)(state2) state4 <- walkRight(2)(state3) state5 <- walkRight(2)(state4) } yield state5 state shouldBe Right(Turtle("123", Position(-1, -1), North)) // 27

Basic model - demo

// let’s focus on this code val state = for { state1 <- Turtle.create("123", Position.zero, North) state2 <- walkRight(1)(state1) state3 <- walkRight(1)(state2) state4 <- walkRight(2)(state3) state5 <- walkRight(2)(state4) } yield state5

// 28

Basic model - demo

// We have to propagate the state manually - verbose and error-prone val state = for { state1 <- Turtle.create("123", Position.zero, North) state2 <- walkRight(1)(state1) state3 <- walkRight(1)(state2) state4 <- walkRight(2)(state3) state5 <- walkRight(2)(state4) } yield state5

// 29

Basic model - demo

// We can flatMap to avoid passing the state explicitly // (it’s not perfect, but it works for now) val state = Turtle.create("123", Position.zero, North) .flatMap(walkRight(1)) .flatMap(walkRight(1)) .flatMap(walkRight(2)) .flatMap(walkRight(2))

// 30

We have a model now How can we event source it?

// 31

01.2 //

Event sourcing the domain

// 32

Modeling events

// We can represent the result of our commands as events sealed trait TurtleEvent { def id: String } case class Created(id: String, pos: Position, dir: Direction) extends TurtleEvent case class Turned(id: String, rot: Rotation) extends TurtleEvent case class Walked(id: String, dist: Int) extends TurtleEvent

// 33

Modeling events

// We can represent the result of our commands as events sealed trait TurtleEvent { def id: String } // we store the turtle’s id directly in the events case class Created(id: String, pos: Position, dir: Direction) extends TurtleEvent case class Turned(id: String, rot: Rotation) extends TurtleEvent case class Walked(id: String, dist: Int) extends TurtleEvent // it’s usually done by wrapping events

// 34

Event handler for creation events

// an event handler is a function allowing to folf a sequence of events type EventHandler0[STATE, EVENT] = (Option[STATE], EVENT) => Some[STATE] val handler1: EventHandler0[Turtle, TurtleEvent] = { case (None, Created(id, pos, dir)) => Some(Turtle(id, pos, dir)) case (Some(turtle), Turned(id, rot)) if id == turtle.id => Some(turtle.copy(dir = Direction.rotate(turtle.dir, rot))) case (Some(turtle), Walked(id, dist)) if id == turtle.id => Some(turtle.copy(pos = Position.move(turtle.pos, turtle.dir, dist))) case (state, event) => sys.error(s"Invalid event $event for state $state") } // 35

Event handler for creation events

// we accept an option to handle creation event (where there is no state yet) type EventHandler0[STATE, EVENT] = (Option[STATE], EVENT) => Some[STATE] val handler1: EventHandler0[Turtle, TurtleEvent] = { case (None, Created(id, pos, dir)) => Some(Turtle(id, pos, dir)) case (Some(turtle), Turned(id, rot)) if id == turtle.id => Some(turtle.copy(dir = Direction.rotate(turtle.dir, rot))) case (Some(turtle), Walked(id, dist)) if id == turtle.id => Some(turtle.copy(pos = Position.move(turtle.pos, turtle.dir, dist))) case (state, event) => sys.error(s"Invalid event $event for state $state") } // 36

Event handler for creation events

// we do know we have a state to return, so let’s return a Some directly type EventHandler0[STATE, EVENT] = (Option[STATE], EVENT) => Some[STATE] val handler1: EventHandler0[Turtle, TurtleEvent] = { case (None, Created(id, pos, dir)) => Some(Turtle(id, pos, dir)) case (Some(turtle), Turned(id, rot)) if id == turtle.id => Some(turtle.copy(dir = Direction.rotate(turtle.dir, rot))) case (Some(turtle), Walked(id, dist)) if id == turtle.id => Some(turtle.copy(pos = Position.move(turtle.pos, turtle.dir, dist))) case (state, event) => sys.error(s"Invalid event $event for state $state") } // 37

Event handler for creation events

type EventHandler0[STATE, EVENT] = (Option[STATE], EVENT) => Some[STATE] // this handler throws an exception in case of invalid transition val handler1: EventHandler0[Turtle, TurtleEvent] = { case (None, Created(id, pos, dir)) => Some(Turtle(id, pos, dir)) case (Some(turtle), Turned(id, rot)) if id == turtle.id => Some(turtle.copy(dir = Direction.rotate(turtle.dir, rot))) case (Some(turtle), Walked(id, dist)) if id == turtle.id => Some(turtle.copy(pos = Position.move(turtle.pos, turtle.dir, dist))) case (state, event) => sys.error(s"Invalid event $event for state $state") } // 38

Event handler usage :: demo

val initialState = Option.empty[Turtle] val events = Seq( Created("123", Position.zero, North), Walked("123", 1), Turned("123", ToRight), ) // note that we use Some.value instead of Option.get val finalState = events.foldLeft(initialState)(handler0).value finalState shouldBe Turtle("123", Position(0, 1), Est)

// 39

Syntactic sugar for handler definition

// However, there is more boilerplate when defining the handler val handler0: EventHandler0[Turtle, TurtleEvent] = { case (None, Created(id, pos, dir)) => Some(Turtle(id, pos, dir)) case (Some(t), Turned(id, rot)) if id == t.id => Some(t.copy(dir = Direction.rotate(t.dir, rot))) case (Some(t), Walked(id, dist)) if id == t.id => Some(t.copy(pos = Position.move(t.pos, t.dir, dist))) case (state, event) => sys.error(s"Invalid event $event for state $state") }

// 40

Syntactic sugar for handler definition

// However, there is more boilerplate when defining the handler val handler0: EventHandler0[Turtle, TurtleEvent] = { case (None, Created(id, pos, dir)) => Some(Turtle(id, pos, dir)) case (Some(t), Turned(id, rot)) if id == t.id => Some(t.copy(dir = Direction.rotate(t.dir, rot))) case (Some(t), Walked(id, dist)) if id == t.id => Some(t.copy(pos = Position.move(t.pos, t.dir, dist))) case (state, event) => sys.error(s"Invalid event $event for state $state") }

// 41

Syntactic sugar for handler definition

// Let’s reduce boilerplate by creating an event handler from a partial function case class EventHandler[STATE, EVENT]( fn: PartialFunction[(Option[STATE], EVENT), STATE] ) { def apply(state: Option[STATE], event: EVENT): Some[STATE] = { val input = (state, event) if (fn.isDefinedAt(input)) Some(fn(input)) else sys.error(s"Invalid event $event for state $state") } }

// 42

Syntactic sugar for handler definition

// A neat final handler val handler = EventHandler[Turtle, TurtleEvent] { case (None, Created(id, pos, dir)) => Turtle(id, pos, dir) case (Some(t), Turned(id, rot)) if id == t.id => t.copy(dir = Direction.rotate(t.dir, rot)) case (Some(t), Walked(id, dist)) if id == t.id => t.copy(pos = Position.move(t.pos, t.dir, dist)) }

// 43

Domain logic - revisited

// The commands now return events object Turtle { def create(id: String, pos: Position, dir: Direction) = if (tooFarAwayFromOrigin(pos)) Left("Too far away") else Right(Created(id, pos, dir)) def turn(rot: Rotation)(turtle: Turtle): Either[String, TurtleEvent] = Right(Turned(turtle.id, rot)) def walk(dist: Int)(turtle: Turtle): Either[String, TurtleEvent] = { val newPos = turtle.pos.move(turtle.dir, dist) if (tooFarAwayFromOrigin(newPos)) Left("Too far away") else Right(Walked(turtle.id, dist)) } } // 44

01.3 //

Persisting and replaying events

// 45

Event Journal

// journal to write events trait WriteJournal[EVENT] { def persist(event: EVENT): Future[Unit] } def persist[EVENT: WriteJournal](events:EVENT)): Future[Unit] trait Hydratable[STATE] { def hydrate(id: String): Future[Option[STATE]] } def hydrate[STATE: Hydratable](id: String): Future[Option[STATE]]

implicit object TurtleJournal extends WriteJournal[TurtleEvent] with Hydratable[Turtle] { … } // 46

Event Journal

trait WriteJournal[EVENT] { def persist(events: EVENT): Future[Unit] } def persist[EVENT: WriteJournal](events:EVENT)): Future[Unit] // hydratable fetch the events and use them to build the state trait Hydratable[STATE] { def hydrate(id: String): Future[Option[STATE]] } def hydrate[STATE: Hydratable](id: String): Future[Option[STATE]]

implicit object TurtleJournal extends WriteJournal[TurtleEvent] with Hydratable[Turtle] { … } // 47

Event Journal

trait WriteJournal[EVENT] { def persist(events: EVENT): Future[Unit] } def persist[EVENT: WriteJournal](events:EVENT)): Future[Unit] trait Hydratable[STATE] { def hydrate(id: String): Future[Option[STATE]] } def hydrate[STATE: Hydratable](id: String): Future[Option[STATE]] // let’s assume we have instance for those implicit object TurtleJournal extends WriteJournal[TurtleEvent] with Hydratable[Turtle] { … } // 48

Wrapping up

does not compile // Simple example which creates and retrieve a turtle using the journal for { event = Turtle.create("123", Position(0, 1), North) _ <- persist(event) state <- hydrate[Turtle]("123") } yield state shouldBe Turtle("123", Position(0, 1), North)

// 49

// The same example but which actually compiles :-) // (thanks to ScalaZ/cats’s monad transformer) (for { event <- EitherT.fromEither(Turtle.create("123", zero, North)) _ <- EitherT.right(persist(event)) state <- OptionT(hydrate[Turtle]("123")).toRight("not found") } yield state).value.map { _ shouldBe Right(Turtle("123", zero, North)) }

// 50

What we have seen so far

// 51

What we have seen so far

- modeling the domain - defining events and event handlers - persisting events and replaying state

// 52

What more could we want?

// 53

// 54

02 //

The problem of composition

// 55

Composition in event sourcing

Composing event handlers is easy - they’re just plain functions

Composing commands is less trivial - what events should we create?

// 56

Why would we want to compose commands in the first place?

// 57

Basic model - demo

// Remember this one in the basic model? It’s actually a composite command def walkRight(dist: Int)(state: Turtle) = for { state1 <- Turtle.walk(dist)(state) state2 <- Turtle.turn(ToRight)(state1) } yield state2 // How do we event source it?

// 58

Composing commands

How about these? def turnAround()(turtle: Turtle): Either[String, Turtle] = ??? def makeUTurn(radius: Int)(turtle: Turtle): Either[String, Turtle] = ???

// 59

Composing commands

// The CISC approach: let’s just create more event types // So far we create ---> walk ---> turn ---> // So that walkRight turnAround makeUTurn

had Created Walked Turned

would ---> ---> --->

give us WalkedRight TurnedAround MadeUTurn

// 60

Composing commands

// The CISC approach: let’s just create more event types // So far we create ---> walk ---> turn ---> // So that walkRight turnAround makeUTurn

had Created Walked Turned

would ---> ---> --->

give us WalkedRight TurnedAround MadeUTurn

// Problem: extensivity

// 61

Composing commands

// Consider we might have additional handlers def turtleTotalDistance(id: String) = EventHandler[Int, TurtleEvent] { case (None, Created(turtleId, _, _)) if id == turtleId => 0 case (Some(total), Walked(turtleId, dist)) if id == turtleId => total + dist case (maybeTotal, _) => maybeTotal }

// 62

Composing commands

// Adding new event types forces up to update every possible interpreter def turtleTotalDistance(id: String) = EventHandler[Int, TurtleEvent] { case (None, Created(turtleId, _, _)) if id == turtleId => 0 case (Some(total), Walked(turtleId, dist)) if id == turtleId => total + dist case (Some(total), WalkedRight(turtleId, dist)) if id == turtleId => total + dist case (Some(total), MadeUTurn(turtleId, radius)) if id == turtleId => total + 3 * radius case (maybeTotal, _) => maybeTotal }

// 63

Events with overlapping semantics are leaky

// 64

How about composition?

// 65

Composing commands

// The RISC approach: let’s compose existing event types // So far we create ---> walk ---> turn ---> // So that walkRight turnAround makeUTurn

had Created Walked Turned

would ---> ---> --->

give us Walked + Turned Turned + Turned Walked + Turned + Walked + Turned + Walked

// 66

Composing commands

// That’s what we did without event sourcing: composition def walkRight(dist: Int)(state: Turtle) = for { state1 <- Turtle.walk(dist)(state) state2 <- Turtle.turn(ToRight)(state1) } yield state2 // Why should it be any different now?

// 67

Gimme some real-life example, will you?

// 68

Use case: appointments app (doctoLib, etc)

Main domain object: appointments between users.

// 69

Use case: appointments app (doctoLib, etc)

Main domain object: appointments between users. Users with a pending common appointment can send messages to each other: postMessage ---> MessagedPosted

// 70

Use case: appointments app (doctoLib, etc)

Main domain object: appointments between users. Users with a pending common appointment can send messages to each other: postMessage ---> MessagedPosted New messages update a chat-like view

// 71

Use case: appointments app (doctoLib, etc)

New feature: users can now cancel the meeting and provide an optional custom message when doing so.

// 72

Use case: appointments app (doctoLib, etc)

New feature: users can now cancel the meeting and provide an optional custom message when doing so. -

We don’t want to deliver the message if the cancellation is not persisted. We don’t want the message to be lost

// 73

Use case: appointments app (doctoLib, etc)

New feature: users can now cancel the meeting and provide an optional custom message when doing so. -

We don’t want to deliver the message if the cancellation is not persisted. We don’t want the message to be lost We don’t want (or can’t) update the chat handler to handle a new type of event

cancelAppointment(Option[Message]) ---> AppointmentCanceled [+ MessagedPosted]

// 74

Use case: appointments app (doctoLib, etc)

New feature: users can now cancel the meeting and provide an optional custom message when doing so. -

We don’t want to deliver the message if the cancellation is not persisted. We don’t want the message to be lost We don’t want (or can’t) update the chat handler to handle a new type of event

cancelAppointment(Option[Message]) ---> AppointmentCanceled [+ MessagedPosted]

// 75

02.1 //

Dealing with multiple events

// 76

Composing commands

// So how could we try to compose this: def walkRight(dist: Int)(state: Turtle) = for { event1 <- Turtle.walk(dist)(state) event2 <- Turtle.turn(ToRight)(???) } yield ???

// 77

Composing commands

// We need a state here def walkRight(dist: Int)(state: Turtle) = for { event1 <- Turtle.walk(dist)(state) event2 <- Turtle.turn(ToRight)(???) } yield ???

// 78

Composing commands

// We can use our handler to replay the first event def walkRight(dist: Int)(state: Turtle) = for { event1 <- Turtle.walk(dist)(state) state2 = Turtle.handler(Some(state), event1).value event2 <- Turtle.turn(ToRight)(state2) } yield ???

// 79

Composing commands

// We can use our handler to replay the first event def walkRight(dist: Int)(state: Turtle) = for { event1 <- Turtle.walk(dist)(state) state2 = Turtle.handler(Some(state), event1).value event2 <- Turtle.turn(ToRight)(state2) } yield ???

// 80

Composing commands

// We’ll need to return both events def walkRight(dist: Int)(state: Turtle) = for { event1 <- Turtle.walk(dist)(state) state2 = Turtle.handler(Some(state), event1).value event2 <- Turtle.turn(ToRight)(state2) } yield Seq(event1, event2)

// 81

Persisting multiple events atomically

// 82

Event journal - revisited

// Obviously, we’ll need to be able to persist multiple events together trait WriteJournal[EVENT] { // Saving the batch of events must be atomic def persist(events: Seq[EVENT]): Future[Unit] } def persist[EVENT: WriteJournal](events: Seq[EVENT])): Future[Unit]

// 83

Persisting multiple events

Persisting multiple events may seem odd to some. Others do that as well: Greg Young’s Event Store has a concept of atomic “commits” which contain multiple events.

// 84

Persisting multiple events :: demo

for { state <- OptionT(hydrate[Turtle]("123")).toRight("not found") events <- EitherT.fromEither(Turtle.walkRight(1)(state)) _ <- EitherT.right(persist(events)) } yield ()

// 85

Are we good already?

// 86

02.2 //

The limits of an imperative approach

// 87

An imperative approach problems

// This imperative approach... for { event1 <- Turtle.create("123", zero, North) state1 = Turtle.handler(None, event1).value event2 <- Turtle.walk(1)(state1) } yield Seq(event1, event2)

// 88

An imperative approach problems:: does not scale

// This imperative approach… does not scale! for { event1 <- Turtle.create("123", zero, North) state1 = Turtle.handler(None, event1).value event2 <- Turtle.walk(1)(state1) state2 = Turtle.handler(Some(state1), event2).value event3 <- Turtle.walk(1)(state2) state3 = Turtle.handler(Some(state2), event3).value event4 <- Turtle.walk(1)(state3) state4 = Turtle.handler(Some(state3), event4).value event5 <- Turtle.walk(1)(state4) state5 = Turtle.handler(Some(state4), event5).value event6 <- Turtle.walk(1)(state5) } yield Seq(event1, event2, event3, event4, event5, event6) // 89

An imperative approach problems :: replaying events

// We need to manually replay at each step for { event1 <- Turtle.create("123", zero, North) state1 = Turtle.handler(None, event1).value event2 <- Turtle.walk(1)(state1) state2 = Turtle.handler(Some(state1), event2).value event3 <- Turtle.walk(1)(state2) state3 = Turtle.handler(Some(state2), event3).value event4 <- Turtle.walk(1)(state3) state4 = Turtle.handler(Some(state3), event4).value event5 <- Turtle.walk(1)(state4) state5 = Turtle.handler(Some(state4), event5).value event6 <- Turtle.walk(1)(state5) } yield Seq(event1, event2, event3, event4, event5, event6) // 90

An imperative approach problems:: accumulating events

// Accumulating events - so error-prone! for { event1 <- Turtle.create("123", zero, North) state1 = Turtle.handler(None, event1).value event2 <- Turtle.walk(1)(state1) state2 = Turtle.handler(Some(state1), event2).value event3 <- Turtle.walk(1)(state2) state3 = Turtle.handler(Some(state2), event3).value event4 <- Turtle.walk(1)(state3) state4 = Turtle.handler(Some(state3), event4).value event5 <- Turtle.walk(1)(state4) state5 = Turtle.handler(Some(state4), event5).value event6 <- Turtle.walk(1)(state5) } yield Seq(event1, event2, event3, event4, event5, event6) // 91

An imperative approach problems:: propagating events and state

// Propagating events and state - repetitive and so error-prone for { event1 <- Turtle.create("123", zero, North) state1 = Turtle.handler(None, event1).value event2 <- Turtle.walk(1)(state1) state2 = Turtle.handler(Some(state1), event2).value event3 <- Turtle.walk(1)(state2) state3 = Turtle.handler(Some(state2), event3).value event4 <- Turtle.walk(1)(state3) state4 = Turtle.handler(Some(state3), event4).value event5 <- Turtle.walk(1)(state4) state5 = Turtle.handler(Some(state4), event5).value event6 <- Turtle.walk(1)(state5) } yield Seq(event1, event2, event3, event4, event5, event6) // 92

03 //

A functional approach

// 93

Quick recap - Problems left

Problems we need to solve yet when composing commands: -

replaying previous events accumulating new events propagating new state

// 94

03.1 //

Replaying events automatically

// 95

Replaying events manually - recap

def walkRight(dist: Int)(state: Turtle) = for { event1 <- Turtle.walk(dist)(state) state1 = Turtle.handler(Some(state), event1).value event2 <- Turtle.turn(ToRight)(state1) } yield Seq(event1, event2) for { event1 <- Turtle.create("123", Position.zero, North) state1 = Turtle.handler(None, event1).value events2 <- walkRight(1)(state1) state2 = events.foldLeft(Some(state1))(Turtle.handler).value events3 <- walkRight(1)(state2) } yield event1 +: events2 ++ events2 // 96

What could we do to automate this?

// 97

Replaying events automatically with helpers

// Let’s use helpers to compute the new state along with every new event def sourceNew(block: Either[String, TurtleEvent]) = block.map { event => event -> Turtle.handler(None, event).value } def source(block: Turtle => Either[String, TurtleEvent]) = (state: Turtle) => block(state).map { event => event -> Turtle.handler(Some(state), event).value }

// 98

Replaying events automatically with helpers - types

// These helpers only “lift” creation and update functions def sourceNew: Either[String, TurtleEvent] => Either[String, (TurtleEvent, Turtle)]

def source: (Turtle => Either[String, TurtleEvent]) => (Turtle => Either[String, (TurtleEvent, Turtle)])

// 99

Replaying events automatically with helpers - comparison

// Before: manually replaying state def walkRight(dist: Int)(state: Turtle) = for { event1 <- Turtle.walk(dist)(state) state1 = Turtle.handler(Some(state), event1).value event2 <- Turtle.turn(ToRight)(state1) } yield Seq(event1, event2) // After: automatically replaying state def walkRight(dist: Int)(state: Turtle) = for { (event1, state1) <- source(Turtle.walk(dist))(state) (event2, state2) <- source(Turtle.turn(ToRight))(state1) } yield (Seq(event1, event2), state2) // 100

value withFilter is not a member of Either[...]

// 101

Replaying events automatically with helpers - demo

// Our example rewritten using the helper functions def walkRight(dist: Int)(state: Turtle) = for { (event1, state1) <- source(Turtle.walk(dist))(state) (event2, state2) <- source(Turtle.turn(ToRight))(state1) } yield (Seq(event1, event2), state2) for { (event1, state1) <- sourceNew(Turtle.create("123", Position.zero, North)) (events2, state2) <- walkRight(1)(state1) (events3, state3) <- walkRight(1)(state2) } yield (event1 +: events2 ++ events2, state3)

// 102

Problems left

Problems we need to solve yet when composing commands: -

replaying previous events accumulating new events propagating new state

// 103

// We still need to emit events in the right order at the end for { (event1, state1) <- sourceNew(Turtle.create("123", Position.zero, North)) (events2, state2) <- walkRight(1)(state1) (events3, state3) <- walkRight(1)(state2) } yield (event1 +: events2 ++ events2, state3) // What if we could accumulate them at each step of the for-comprehension?

// 104

03.2 //

Accumulating events automatically

// 105

Sourced class

// Remember our helper? def sourceNew: Either[String, TurtleEvent] => Either[String, (TurtleEvent, Turtle)]

// 106

Sourced class

// Remember our helper? def sourceNew: Either[String, TurtleEvent] => Either[String, (TurtleEvent, Turtle)] // We wrap our result into a case class, so that we try to write a flatMap case class Sourced[STATE, EVENT](run: Either[String, (Seq[EVENT], STATE)]) // We use a Seq as we will be accumulating events

// 107

Sourced class

case class Sourced[STATE, EVENT](run: Either[String, (Seq[EVENT], STATE)] { def events: Either[String, Seq[EVENT]] = run.map { case (events, _) => events } def flatMap[B](fn: STATE => Sourced[B, EVENT]): Sourced[B, EVENT] = Sourced[B, EVENT]( for { (oldEvents, oldState) <- this.run (newEvents, newState) <- fn(oldState).run } yield (oldEvents ++ newEvents, newState) )

// 108

Sourced class

// We can update our helpers. They really feel like “lifting” now. def sourceNew: Either[String, TurtleEvent] => Sourced[Turtle, TurtleEvent] def source: (Turtle => Either[String, TurtleEvent]) => (Turtle => Sourced[Turtle, TurtleEvent])

// 109

Sourced class

// Event sourcing with the Sourced monad def walkRight(dist: Int)(state: Turtle) = for { state1 <- source(Turtle.walk(dist))(state) state2 <- source(Turtle.turn(ToRight))(state1) } yield state2 // Without event sourcing def walkRight(dist: Int)(state: Turtle) = for { state1 <- Turtle.walk(dist)(state) state2 <- Turtle.turn(ToRight)(state1) } yield state2 // 110

Sourced class

(for { state1 <- sourceNew(Turtle.create("123", Position.zero, North)) state2 <- walkRight(1)(state1) state3 <- walkRight(1)(state2) } yield state3).events

// 111

Problems left

Problems we need to solve yet when composing commands: -

replaying previous events

- accumulating new events -

propagating new state

// 112

Rings a bell?

case class Sourced[A, EVENT](run: Either[String, (Seq[EVENT], A)] { def flatMap[B](fn: A => Sourced[B, EVENT]): Sourced[B, EVENT] = Sourced[B, EVENT](run.flatMap { case (oldEvents, oldState) => fn(oldState).run.map { case (newEvents, newState) => (oldEvents ++ newEvents, newState) } }) }

// 113

Writer monad

Sourced[STATE, EVENT] // is equivalent to WriterT[Either[String, ?], Seq[EVENT], STATE]

// 114

Problems left

Problems we need to solve yet when composing commands: -

replaying previous events accumulating new events

- propagating new state

// 115

Sourced model demo

// Using for-comprehension for { state1 <- sourceNew[Turtle](Turtle.create("123", Position.zero, North)) state2 <- walkRight(1)(state1) state3 <- walkRight(1)(state2) } yield state3 // Using flatMap sourceNew[Turtle](Turtle.create("123", Position.zero, North)) .flatMap(walkRight(1)) .flatMap(walkRight(1))

// 116

Problems left

Problems we need to solve yet when composing commands: -

replaying previous events accumulating new events

- propagating new state (?)

// 117

Writer monad

// this code might seem fine (no visible state)... sourceNew[Turtle](Turtle.create("123", Position.zero, North)) .flatMap(walkRight(1)) .flatMap(walkRight(1))

// 118

Writer monad

// Limitation: no guarantee that we are actually using the event sourced state sourceNew[Turtle](Turtle.create("123", Position.zero, North)) .flatMap(walkRight(1)) .flatMap(walkRight(1)) .map(state => state.copy(pos = Position(99, 99)))

// 119

Problems left

Problems we need to solve yet when composing commands: -

replaying previous events accumulating new events

-

propagating state (kind of, but in a very unsafe way)

// 120

Creations and updates are different beasts

// 121

Updates are actually monadic and can be composed: Update THEN Update = Update

Creation can only be composed with later updates: Create THEN Update = Create

// 122

03.4 //

Beyond monads

// 123

Propagating state - once again

// Let’s go back to our original helpers for the Sourced result monad.

def sourceNew: Either[String, TurtleEvent] => Sourced[Turtle, TurtleEvent] def source: (Turtle => Either[String, TurtleEvent]) => (Turtle => Sourced[Turtle, TurtleEvent])

// 124

Propagating state - once again

// Let’s go back to our original helpers for the Sourced result monad. // What if we lifted creation and updating commands into types with appropriate // combination operators? def sourceNew: Either[String, TurtleEvent] => SourcedCreation[Turtle, TurtleEvent] def source: (Turtle => Either[String, TurtleEvent]) => SourcedUpdate[Turtle, TurtleEvent, Turtle])

// 125

Reifying commands

case class SourcedCreation[STATE, EVENT](run: Sourced[STATE, EVENT]) { def events: Either[String, Seq[EVENT]] = run.events def andThen[A](other: SourcedUpdate[STATE, EVENT, A]) = SourcedCreation[A, EVENT] { this.run.flatMap(other.run) } } case class SourcedUpdate[STATE, EVENT, A](run: (STATE) => Sourced[A, EVENT]) { def events(initialState: STATE): Either[String, Seq[EVENT]] = run(state).events def andThen[B](other: SourcedUpdate[A, EVENT, B]) = SourcedUpdate[STATE, EVENT, B] { initialState => this.run(initialState).flatMap(other.run) } // 126

Reifying commands

case class SourcedCreation[STATE, EVENT](run: Sourced[STATE, EVENT]) { def events: Either[String, Seq[EVENT]] = run.events def andThen[A](other: SourcedUpdate[STATE, EVENT, A]) = SourcedCreation[A, EVENT] { this.run.flatMap(other.run) } } case class SourcedUpdate[STATE, EVENT, A](run: (STATE) => Sourced[A, EVENT]) { def events(initialState: STATE): Either[String, Seq[EVENT]] = run(state).events def andThen[B](other: SourcedUpdate[A, EVENT, B]) = SourcedUpdate[STATE, EVENT, B] { initialState => this.run(initialState).flatMap(other.run) } // 127

Reifying commands

case class SourcedCreation[STATE, EVENT](run: Sourced[STATE, EVENT]) { def events: Either[String, Seq[EVENT]] = run.events def andThen[A](other: SourcedUpdate[STATE, EVENT, A]) = SourcedCreation[A, EVENT] { this.run.flatMap(other.run) } } case class SourcedUpdate[STATE, EVENT, A](run: (STATE) => Sourced[A, EVENT]) { def events(initialState: STATE): Either[String, Seq[EVENT]] = run(state).events def andThen[B](other: SourcedUpdate[A, EVENT, B]) = SourcedUpdate[STATE, EVENT, B] { initialState => this.run(initialState).flatMap(other.run) } // 128

Reifying commands

case class SourcedCreation[STATE, EVENT](run: Sourced[STATE, EVENT]) { def events: Either[String, Seq[EVENT]] = run.events def andThen[A](other: SourcedUpdate[STATE, EVENT, A]) = SourcedCreation[A, EVENT] { this.run.flatMap(other.run) } } case class SourcedUpdate[STATE, EVENT, A](run: (STATE) => Sourced[A, EVENT]) { def events(initialState: STATE): Either[String, Seq[EVENT]] = run(state).events def andThen[B](other: SourcedUpdate[A, EVENT, B]) = SourcedUpdate[STATE, EVENT, B] { initialState => this.run(initialState).flatMap(other.run) } // 129

Reifying commands

case class SourcedCreation[STATE, EVENT](run: Sourced[STATE, EVENT]) { def events: Either[String, Seq[EVENT]] = run.events def andThen[A](other: SourcedUpdate[STATE, EVENT, A]) = SourcedCreation[A, EVENT] { this.run.flatMap(other.run) } } case class SourcedUpdate[STATE, EVENT, A](run: (STATE) => Sourced[A, EVENT]) { def events(initialState: STATE): Either[String, Seq[EVENT]] = run(state).events def andThen[B](other: SourcedUpdate[A, EVENT, B]) = SourcedUpdate[STATE, EVENT, B] { initialState => this.run(initialState).flatMap(other.run) } // 130

Reifying commands :: demo

// We no longer need for-comprehension nor flatMaps def walkRight(dist: Int) = { source(Turtle.walk(dist)) andThen source(Turtle.turn(ToRight)) } (

sourceNew[Turtle](Turtle.create("123", Position.zero, North)) andThen walkRight(1) andThen walkRight(1) andThen source(Turtle.walk(2)) ).events // 131

Kleisli

SourcedUpdate[STATE, EVENT, A] // is equivalent to

Kleisli[Sourced[?, EVENT], STATE, A]

// 132

Problems left

Problems we need to solve yet when composing commands: -

replaying previous events accumulating new events

-

propagating state

// 133

04 //

Further possibilities

// 134

Nice syntax

// 135

Can we do better?

// boilerplate, let’s make the commands return Sourced instances directly def walkRight(dist: Int) = source(Turtle.walk(dist)) andThen source(Turtle.turn(ToRight)) (

sourceNew[Turtle](Turtle.create("123", Position.zero, North)) andThen walkRight(1) andThen walkRight(1) andThen source(Turtle.walk(2)) ).events

// 136

Can we do better?

// better, is it the best we can do? def walkRight(dist: Int) = Turtle.walk(dist) andThen Turtle.turn(ToRight) ( Turtle.create("123", Position.zero, North) andThen walkRight(1) andThen walkRight(1) andThen Turtle.walk(2) ).events

// 137

Can we do better?

// there is no more difference between walkRight and other commands def walkRight(dist: Int) = Turtle.walk(dist) andThen Turtle.turn(ToRight) ( Turtle.create("123", Position.zero, North) andThen walkRight(1) andThen walkRight(1) andThen Turtle.walk(2) ).events

// 138

Pimped commands

// this code is not only simpler, // but it does not make any difference (nor should) // between original commands and composite ones ( Turtle.create("123", Position.zero, North) andThen Turtle.walkRight(1) andThen Turtle.walkRight(1) andThen Turtle.walk(2) ).events

// 139

More combinators

// 140

ReaderWriterState :: bonus

// let’s allow optional calls of a command def when[STATE] = new whenPartiallyApplied[STATE] final class whenPartiallyApplied[STATE] { def apply[EVENT]( predicate: (STATE) => Boolean, block: STATE => SourceUpdated[STATE, EVENT, STATE] )(implicit handler: EventHandler[STATE, EVENT]) = SourcedUpdate[STATE, EVENT, STATE](Kleisli { state => if (predicate(state)) block(state).run else WriterT[Either[String, ?], Vector[EVENT], STATE] { Right(Vector.empty -> state) // no-op } }) } // 141

ReaderWriterState :: bonus

// the predicate is called only when evaluating commands // (not when replaying the emitted events) ( Turtle.create("123", zero, North) andThen Turtle.walkRight(1) andThen Turtle.walkRight(1) andThen when[Turtle](_.dir == North, Turtle.walk(1)) andThen Turtle.walk(2) ) events

// 142

What more?

// 143

Updating multiple instances

// 144

Updating multiple aggregates

// The weakness of the initial monad is also its strength def together(turtle1: Turtle, turtle2: Turtle) (update: Turtle => Sourced[Turtle, TurtleEvent]) : Sourced[(Turtle, Turtle), TurtleEvent] = for { updated1 <- update(turtle1) updated2 <- update(turtle2) } yield (updated1, updated2)

// 145

Updating multiple aggregates

// The weakness of the initial monad is also its strength def together(turtle1: Turtle, turtle2: Turtle) (update: Turtle => Sourced[Turtle, TurtleEvent]) : Sourced[(Turtle, Turtle), TurtleEvent] = for { updated1 <- update(turtle1) updated2 <- update(turtle2) } yield (updated1, updated2) // Caveat: consistency vs scalability - atomic persistence of events is only possible within a single shard/partition of the underlying store

// 146

Handling concurrency

// 147

Concurrency

// So now we can write declarative programs which reify all the changes we want // to make to some state. val myProgram = ( TurtleCommands.walkRight(1) andThen TurtleCommands.walkRight(1) andThen TurtleCommands.walk(2) )

// 148

Concurrency

// So now we can write declarative programs which reify all the changes we want // to make to some state. val myProgram = ( TurtleCommands.walkRight(1) andThen TurtleCommands.walkRight(1) andThen TurtleCommands.walk(2) ) // It’s easy to introduce optimistic locking on top of it // and achieve something similar to STM

// 149

//

Summing up

// 150

What we’ve seen today

-

Modeling and using events and handlers

-

The limitation of an imperative approach

-

How a functional approach can help us overcome these limitations

-

Event sourcing can become an implementation detail

// 151

Merci. Daniel KRZYWICKI [email protected] @eleaar

Étienne VALLETTE d’OSIA [email protected] @dohzya

// 152

Appendum

// 153

ReaderWriterState monad

case class SourcedUpdate[STATE, EVENT, A]( run: ReaderWriterStateT[Either[String, ?], Unit, Vector[EVENT], STATE, A] ) { def events(initialState: STATE): Either[String, Vector[EVENT]] = run.runL((), initialState) def state(initialState: STATE): Either[String, Option[STATE]] = run.runS((), initialState) def map[B](fn: A => B): Sourced[STATE, EVENT, B] = SourcedState(run.map(fn)) def flatMap[B](fn: A => Sourced[STATE, EVENT, B]): Sourced[STATE, EVENT, B] = SourcedState(run.flatMap(fn(_).run)) } // 154

ReaderWriterState monad

def walkRight(dist: Int) = for { _ <- source(Turtle.walk(dist)) _ <- source(Turtle.turn(ToRight)) } yield () val result = ( sourceNew[Turtle](Turtle.create("123", Position.zero, North)) andThen ( for { _ <- sourceNew[Turtle](Turtle.create("123", Position.zero, North)) _ <- walkRight(1) _ <- walkRight(1) } yield () ) ).run // 155

composable event sourcing with monads - GitHub

def create(id: String, pos: Position, dir: Direction): Either[String, Turtle] = if (tooFarAwayFromOrigin(pos)) Either.left("Too far away") else Either.right(Turtle(id, pos, dir)). // curried command are like already-configured command def turn(rot: Rotation)(turtle: Turtle): Either[String, Turtle] = Right(turtle.copy(dir = turtle.dir.rotate(rot))).

754KB Sizes 56 Downloads 164 Views

Recommend Documents

Event for file change #1 - GitHub
Jun 6, 2017 - This is all Native swift, and there is no reliance on any apple ..... Ill try to compile on another computer as it may be the xcode version i'm ...

Event-Driven Concurrency in JavaScript - GitHub
24 l. Figure 2.6: Message Passing in Go. When, for example, Turnstile thread sends a value over counter ...... Is JavaScript faster than C? http://onlinevillage.blogspot. ... //people.mozilla.com/~dmandelin/KnowYourEngines_Velocity2011.pdf.

Sourcing & Trading.pdf
To develop a process model with clear scoping of roles and responsibilities. To define software components for automation and efficacy increases. Page 1 of 2 ...

Sourcing & Trading.pdf
following the European market integration. Lasting triple digit revenue growth ... To define software components for automation and efficacy increases. Page 1 of 2 ... testing of the ETRM suite was. completed ... reporting. The product design.

Composable, Parameterizable Templates for High ...
Abstract—High-level synthesis tools aim to make FPGA programming easier by raising the ... Our results demonstrate that a small number of optimized templates.

CP2K with LIBXSMM - GitHub
make ARCH=Linux-x86-64-intel VERSION=psmp AVX=2. To target for instance “Knights ... //manual.cp2k.org/trunk/CP2K_INPUT/GLOBAL/DBCSR.html).

Java with Generators - GitHub
processes the control flow graph and transforms it into a state machine. This is required because we can then create states function SCOPEMANGLE(node).

Strategic Sourcing Update_SUBOA_2_2_17X.pdf
There was a problem previewing this document. Retrying... Download. Connect more apps... Try one of the apps below to open or edit this item. Strategic ...

OpenBMS connection with CAN - GitHub
Arduino with BMS- and CAN-bus shield as BMS a master. - LTC6802-2 or LTC6803-2 based boards as cell-level boards. - CAN controlled Eltek Valere as a ...

Better performance with WebWorkers - GitHub
Chrome52 on this Laptop. » ~14kbyte. String => 133ms ... 3-4 Seks processing time on samsung galaxy S5 with crosswalk to finish the transition with ... Page 17 ...

with ZeroMQ and gevent - GitHub
Normally, the networking of distributed systems is ... Service Oriented .... while True: msg = socket.recv() print "Received", msg socket.send(msg). 1. 2. 3. 4. 5. 6. 7.

Getting Started with CodeXL - GitHub
10. Source Code View . ..... APU, a recent version of Radeon Software, and the OpenCL APP SDK. This document describes ...... lel_Processing_OpenCL_Programming_Guide-rev-2.7.pdf. For GPU ... trademarks of their respective companies.

Getting Started with Go - GitHub
Jul 23, 2015 - The majority of my experience is in PHP. I ventured into Ruby, ... Compiled, Statically Typed, Concurrent, Imperative language. Originally ...

Getting Acquainted with R - GitHub
In this case help.search(log) returns all the functions with the string 'log' in them. ... R environment your 'working directory' (i.e. the directory on your computer's file ... Later in the course we'll discuss some ways of implementing sanity check

Training ConvNets with Torch - GitHub
Jan 17, 2014 - ... features + SVM. – Neural Nets (and discuss discovering graph structure automařcally). – ConvNets. • Notebook Setup ... Page 9 ...

Examples with importance weights - GitHub
Page 3 ... Learning with importance weights y. wT t x wT t+1x s(h)||x||2 ... ∣p=(wt−s(h)x)Tx s (h) = η. ∂l(p,y). ∂p. ∣. ∣. ∣. ∣p=(wt−s(h)x)Tx. Finally s(0) = 0 ...

Digital Design with Chisel - GitHub
Dec 27, 2017 - This lecture notes (to become a book) are an introduction into hardware design with the focus on using the hardware construction language Chisel. The approach of this book is to present small to medium sized typical hardware components

Deep Learning with H2O.pdf - GitHub
best-in-class algorithms such as Random Forest, Gradient Boosting and Deep Learning at scale. .... elegant web interface or fully scriptable R API from H2O CRAN package. · grid search for .... takes to cut the learning rate in half (e.g., 10−6 mea

An Automatically Composable OWL Reasoner for ...
matching OWL entailment rules in a rule engine ... search space and improves the reasoning efficiency. ..... without dedicated optimization for OWL reasoning.

An Automatically Composable OWL Reasoner for ...
order logic theorem prover (or engines of its subset). F-logic-based reasoners (e.g. FOWL [7]) map OWL ontology to f-logic and perform OWL reasoning using.

COROR: A COmposable Rule-entailment Owl ...
Jul 24, 2011 - The Semantic Sensor Network (SSN) is a recently emerged research strand using Semantic Web technologies, in particular OWL and its.

Ontology-based Semantics for Composable Autonomic ...
Ontology-based Semantics for Composable Autonomic Elements. John Keeney, Kevin Carey, David Lewis, Declan O'Sullivan, Vincent Wade. Trinity College Dublin. Knowledge and Data Engineering Group. Computer Science Department, College Green, Dublin 2, Ir