To Effect or
Not to Effect
We ❤ FP
Easy to read, understand and trust code
• referential transparency
• local reasoning
def combine(a: Int, b: Int): Int = a + b
val five = combine(2, 3)
val five_v2 = 2 + 3
val five_v3 = 5
All the above are replaceable with one another
We ❤ FP
Side effects are "bad"...
• throwing errors breaks control flow
• changing data makes code hard to track
• synchronizing state makes code bug-prone
... but they are necessary and inevitable
• we must "make things happen" in the world
• we must interact with the software
val printSomething: Unit = println("Effects!")
val printSomething_v2: Unit = () // not the same
var anInt = 0
val changingVar: Unit = (anInt += 1)
val changingVar_v2: Unit = () // not the same
val service: Service = ...
service.sendToKafka("some data")
service.log("this just happened")
val value = service.getOrFail[String]("myKey")
Effects
Manage possibly side-effecting computations as values
• the type should describe the kind of computation
• the type description should contain the value type of the computation
• in case of side effects, the description and the execution of the effect are separate
Effect example: Option
Is Option an "effect"?
• the type should describe the kind of computation
ü yes, the type describes a possibly absent value
• the type description should contain the value type of the computation
ü yes, the type description contains the value type: the A that might be there
• in case of side effects, the description and the execution of the effect are separate
ü no side effects required, so... yes
def maybeGetString() =
if Random.nextBoolean() then "Scala" else null
val anOption = Option(maybeGetString())
Effect example: Future
Is Future an "effect"?
• the type should describe the kind of computation
ü yes, the type describes a computation that's run on a separate thread
• the type description should contain the value type of the computation
ü yes, it's the A that will be returned later
• in case of side effects, the description and the execution of the effect are separate
❌ no, the execution starts immediately
val aFuture: Future[Int] = Future {
val someComputation = 40 + 2
Thread.sleep(1000)
someComputation * 10
}
Exercise: A Custom Effect
case class MyEffect[A](unsafeRun: () => A) {
def map[B](f: A => B): MyEffect[B] =
MyEffect(() => f(unsafeRun()))
def flatMap[B](f: A => MyEffect[B]): MyEffect[B] =
MyEffect(() => f(unsafeRun()).unsafeRun())
}
Put a computation in a box, suspend it
Is this an "effect"?
• the type should describe the kind of computation
ü yes, it's an arbitrary computation that returns a value
• the type description should contain the value type of the computation
ü yes, it's the A that will be returned
• in case of side effects, the description and the execution of the effect are separate
ü yes, nothing gets executed until unsafeRun is called
Super-Effects
case class MyEffect[A](unsafeRun: () => A) {
def map[B](f: A => B): MyEffect[B] =
MyEffect(() => f(unsafeRun()))
def flatMap[B](f: A => MyEffect[B]): MyEffect[B] =
MyEffect(() => f(unsafeRun()).unsafeRun())
}
Powerful abstraction
• describes any computation
• obeys monadic laws
• is referentially transparent
• can be chained, passed around, returned as result
• can be enhanced with transformers, error handlers, etc
• can be scheduled to on a thread
Effect Systems
à Cats Effect
ZIO
Kyo
Others
Cats Effect IO
val ourFirstIO: IO[Int] = IO.pure(42)
val aDelayedIO: IO[Int] = IO.delay {
println("I'm producing an integer")
54
}
val improvedMeaningOfLife = ourFirstIO.map(_ * 2)
val printedMeaningOfLife = ourFirstIO.flatMap(mol => IO.delay(println(mol)))
def smallProgram(): IO[Unit] = for {
line1 <- IO(StdIn.readLine())
line2 <- IO(StdIn.readLine())
_ <- IO.delay(println(line1 + line2))
} yield ()
Direct enhancement of "MyEffect"
Super-effect: can model any computation
A program as value
Cats Effect IO: Superpowers
val aFailedCompute: IO[Int] = IO.delay(throw new RuntimeException("A FAILURE"))
val dealWithIt = aFailure.handleErrorWith {
case _: RuntimeException => IO.delay(println("I'm still here"))
// ...
}
Error handling
val bigComputation: IO[Int] = ...
val veryBigComputation: IO[String] = ...
val bigCombination: IO[String] = (bigComputation, veryBigComputation).parMapN {
(num, string) => s"my goal in life is $num and $string")
}
Parallelizing efffects
Cats Effect IO: Concurrency
Light threads aka Fibers
def runOnSomeOtherThread[A](io: IO[A]) = for {
fib <- io.start
result <- fib.join
} yield result
A whole new world
• semantic blocking: effects can wait without blocking a JVM thread
• cancellation: effect chains can be interrupted without heavy Thread.interrupt calls
• timeouts
• retries
• error handling
• tupling
• racing
• ... all with the same assumption: IOs are FP-composable data structures
Cats Effect IO: Concurrency
Example: racing
def testRace() = {
val meaningOfLife = runWithSleep(42, 1.second)
val favLang = runWithSleep("Scala", 2.seconds)
val first: IO[Either[Int, String]] = IO.race(meaningOfLife, favLang)
/*
- both IOs run on separate fibers
- the first one to finish will complete the result
- the loser will be canceled
*/
first.flatMap {
case Left(mol) => IO(s"Meaning of life won: $mol")
case Right(lang) => IO(s"Fav language won: $lang")
}
}
the only important call here
Cats Effect IO: Concurrency
Example: cancellation
val specialPaymentSystem = (
IO("Payment running, don't cancel me...").debug >>
IO.sleep(1.second) >>
IO("Payment completed.").debug
).onCancel(IO("MEGA CANCEL OF DOOM!").debug.void)
val cancellationOfDoom = for {
fib <- specialPaymentSystem.start
_ <- IO.sleep(500.millis) >> fib.cancel
_ <- fib.join
} yield ()
val atomicPayment = specialPaymentSystem.uncancelable
Turn effects into transactions
Cats Effect IO: Concurrency
Example: pinpoint cancellation
val authFlow: IO[Unit] = IO.uncancelable { poll =>
for {
pw <- poll(inputPassword).onCancel(IO("Auth timeout, try later.").void) // this is cancelable
verified <- verifyPassword(pw) // this is NOT cancelable
_ <- if (verified) IO("Authentication successful.") // this is NOT cancelable
else IO("Authentication failed.")
} yield ()
}
val authProgram = for {
authFib <- authFlow.start
_ <- IO.sleep(3.seconds) >> IO("Auth timeout, attempting cancel...") >> authFib.cancel
_ <- authFib.join
} yield ()
Cats Effect: Concurrency
Ref = effectful atomic reference
val atomicMol: IO[Ref[IO, Int]] = IO.ref(42)
// modifying is an effect
val increasedMol: IO[Unit] = atomicMol.flatMap { ref =>
ref.set(43) // thread-safe
}
Deferred = effectful "promise"
val aDeferred: IO[Deferred[IO, Int]] = Deferred[IO, Int]
// get blocks the calling fiber (semantically)
// until some other fiber completes the Deferred with a value
val reader: IO[Int] = aDeferred.flatMap { signal =>
signal.get // blocks the fiber
}
val writer = aDeferred.flatMap { signal =>
signal.complete(42)
}
Ref + Deferred = ❤
Producer-consumer patterns
Notification services
Concurrency primitives
• mutex
• semaphore
• cyclic barrier
• countdown latch
Controlled semantic blocking
Cats Effect: Resource
Using a resource has 3 steps, and each is an effect
1. acquire
2. consume
3. release
val connectionResource = Resource.make(
IO(new Connection("rockthejvm.com")) // acquire
)(
conn => conn.close().void // release
)
// ... later in your code
val resourceFetchUrl = for {
fib <- connectionResource.use(conn => conn.open() >> IO.never).start
_ <- IO.sleep(1.second) >> fib.cancel
} yield ()
Resource is a composable abstraction focused on acquire/release
Cats Effect: Resource
Has FP transformers
1. map/flatMap
2. error handlers
3. finalizers
val connRes = for {
file <- Resource.make(IO(openConfFile("myconfig.conf")))(file => IO(file.close()))
url <- Resource.make(IO(file.getConnString()))(_ => IO.unit)
conn <- Resource.make(IO(new Connection(url)))(conn => IO(conn.close()))
} yield conn
val ioWithFinalizer = connRes.use(...).guaranteeCase {
case Succeeded(fa) => fa.flatMap(result => IO(s"releasing resource: $result").debug).void
case Errored(e) => IO("using the reosource failed because $e").debug.void
case Canceled() => IO("resource got canceled, releasing what's left").debug.void
}
Polymorphic Effects
Instead of IO[A], Option[A] or MyEffect[A], think general: F[A]
Maintain capabilities of effects inside type classes
1. map/flatMap: Monad
2. error handlers: MonadError
3. cancellation and finalizers: MonadCancel
4. fibers/light threads: Spawn
5. general ref/deferred: Concurrent
6. lifting async computations to an effect: Async
You can call the respective methods if you have the correct type class
instance in scope
Polymorphic Effects
Instead of IO[A], Option[A] or MyEffect[A], think general: F[A]
val authFlow: IO[Unit] = IO.uncancelable { poll =>
for {
pw <- poll(inputPassword).onCancel(IO("Auth timeout, try later.").void) // cancelable
verified <- verifyPassword(pw) // this is NOT cancelable
_ <- if (verified) IO("Authentication successful.") // this is NOT cancelable
else IO("Authentication failed.")
} yield ()
}
def authFlow[F[_], E](using mc: MonadCancel[F,E]): F[Unit] = mc.uncancelable { poll =>
for {
pw <- poll(inputPassword).onCancel(mc.pure("Auth timeout, try later.").void) // cancelable
verified <- verifyPassword(pw) // this is NOT cancelable
_ <- if (verified) mc.pure("Authentication successful.") // this is NOT cancelable
else mc.pure("Authentication failed.")
} yield ()
}
domain
domain
domain
Architecture
Onionex
• onion with some layers segmented
• hex without the ceremony
domain
repository
service
UI
Testing
Infra
Apps
domain
domain
domain
user domain
user repo
user service
"I want to log in"
core
domain
domain
domain
Architecture
Onionex
• onion with some layers segmented
• hex without the ceremony
domain
repository
service
UI
Testing
Infra
Apps
domain
domain
domain
user domain
user repo
user service
"Here's your API key"
core
domain
domain
domain
Architecture
Onionex
• onion with some layers segmented
• hex without the ceremony
domain
repository
service
UI
Testing
Infra
Apps
domain
domain
domain
user domain
user repo
user service
TEST ALL THINGS
core
domain
domain
domain
Architecture
Onionex
• onion with some layers segmented
• hex without the ceremony
domain
repository
service
UI
Testing
Infra
Apps
domain
domain
user domain
user repo
TEST THE REPO
core
Architecture
Onionex
• onion with layers segmented
• hex without the ceremony
core logic
UI
action
Hex traits
• "business logic" separated from FE/BE details
• UI à core à actions flow via inversion of control
A Backend Module
Onionex
• onion with some layers segmented
• hex without the ceremony
Polymorphic effects
• Layers described in terms of capabilities
• Programs (e.g. service calls) described in general terms
• The enabler of capabilities instantiated last
trait Auth[F[_]] {
def login(email: String, password: String): F[Option[User]]
def signUp(newUserInfo: NewUserInfo): F[Option[User]]
...
}
object LiveAuth {
def apply[F[_]: Async: Logger](
users: Users[F],
tokens: Tokens[F],
emails: Emails[F]
): F[LiveAuth[F]] = ... // power up engines
}
Backend
Onionex + Effects
• onion with some layers segmented
• hex without the ceremony
object Core {
def apply[F[_]: Async: Logger](
xa: Transactor[F],
tokenConfig: TokenConfig,
emailServiceConfig: EmailServiceConfig,
stripeConfig: StripeConfig
): Resource[F, Core[F]] = {
val coreF = for {
jobs <- LiveJobs[F](xa)
users <- LiveUsers[F](xa)
tokens <- LiveTokens[F](users)(xa, tokenConfig)
emails <- LiveEmails[F](emailServiceConfig)
auth <- LiveAuth[F](users, tokens, emails)
stripe <- LiveStripe[F](stripeConfig)
} yield new Core(jobs, users, auth, stripe)
Resource.eval(coreF)
}
}
Pros
• layers "click" like legos
• each layer has its own capabilities
(e.g. MonadThrow, Concurrent, etc)
• easy to configure, configs propagate
• easy to test individual layers
• easy to replace implementations
• easy to navigate
• easy to read, understand & maintain
object Core {
def apply[F[_]: Async: Logger](
xa: Transactor[F],
tokenConfig: TokenConfig,
emailServiceConfig: EmailServiceConfig,
stripeConfig: StripeConfig
): Resource[F, Core[F]] = {
val coreF = for {
jobs <- LiveJobs[F](xa)
users <- LiveUsers[F](xa)
tokens <- LiveTokens[F](users)(xa, tokenConfig)
emails <- LiveEmails[F](emailServiceConfig)
auth <- LiveAuth[F](users, tokens, emails)
stripe <- LiveStripe[F](stripeConfig)
} yield new Core(jobs, users, auth, stripe)
Resource.eval(coreF)
}
}
Backend
Onionex + Effects
• onion with some layers segmented
• hex without the ceremony
Cons
• requires learning curve & discipline
• errors are invisible unless surfaced
• almost always ends up with Sync/Async
• becomes tempting to misunderstand
(implicit vs explicit DI)
object Core {
def apply[F[_] :Async :Logger :Jobs :Users :Tokens :Emails :Auth :Stripe]: F[Core[F]] =
...
}
Effect Systems
Cats Effect
à ZIO
Kyo
Others
Richer Super-Effects
Super-effects can suspend arbitrary computations
Not a full program, does not include dependencies and errors
case class MyEffect[A](unsafeRun: () => A) {
def map[B](f: A => B): MyEffect[B] = ...
def flatMap[B](f: A => MyEffect[B]): MyEffect[B] = ...
}
case class MyRichEffect[R,E,A](unsafeRun: R => Either[E,A]) {
def map[B](f: A => B): MyRichEffect[R,E,B] = ...
def flatMap[B](f: A => MyRichEffect[R,E,B]): MyRichEffect[R,E,B] = ...
}
Need a change of signature
ZIO
// success
val meaningOfLife: ZIO[Any, Nothing, Int] = ZIO.succeed(42)
// failure
val aFailure: ZIO[Any, String, Nothing] = ZIO.fail("Something went wrong")
val smallProgram = for {
_ <- ZIO.succeed(println("what's your name"))
name <- ZIO.succeed(StdIn.readLine())
_ <- ZIO.succeed(println(s"Welcome to ZIO, $name"))
} yield ()
A richer super-effect
Same core ideas
• FP-style transformers
• fibers/light threads and concurrency primitives
• semantic blocking
• cancellation and error handling
• timeouts, retries
Effectful Dependency Injection
case class User(name: String, email: String)
class UserSubscription(emailService: EmailService, userDatabase: UserDatabase) {
def subscribeUser(user: User): Task[Unit] = ...
// Task[A] == ZIO[Any, Throwable, A]
}
class EmailService {
def email(user: User): Task[Unit] = ...
}
class UserDatabase(connectionPool: ConnectionPool) {
def insert(user: User): Task[Unit] = ...
}
class ConnectionPool(nConnections: Int) {
def get: Task[Connection] = ...}
case class Connection() {
def runQuery(query: String): Task[Unit] = ...
}
Assuming a standard layered app
Effectful Dependency Injection
val subscriptionService = ZIO.attempt(
UserSubscription.create(
EmailService.create(),
UserDatabase.create(
ConnectionPool.create(10)
)
)
)
Assuming a standard layered app, you normally need to build the object...
def subscribe(user: User): ZIO[Any, Throwable, Unit] = for {
sub <- subscriptionService
_ <- sub.subscribeUser(user)
} yield ()
... before you can use it
Effectful Dependency Injection
Instead, assume that the required object exists, and use it...
def subscribe_v2(user: User): ZIO[UserSubscription, Throwable, Unit] = for {
sub <- ZIO.service[UserSubscription] // ZIO[UserSubscription, Nothing, UserSubscription]
_ <- sub.subscribeUser(user)
} yield ()
val program = for {
_ <- subscribe(User("Daniel", "daniel@rockthejvm.com"))
_ <- subscribe(User("Bon Jovi", "jon@rockthejvm.com"))
} yield ()
... then provide it later
but how?
Effectful Dependency Injection
Create layers, i.e. effects that will be run when required
object UserSubscription {
val live: ZLayer[EmailService with UserDatabase, Nothing, UserSubscription] =
ZLayer.fromFunction(new UserSubscription(_, _))
}
object EmailService {
val live: ZLayer[Any, Nothing, EmailService] =
ZLayer.succeed(new EmailService)
}
object UserDatabase {
val live: ZLayer[ConnectionPool, Nothing, UserDatabase] =
ZLayer.fromFunction(new UserDatabase(_))
}
object ConnectionPool {
def live(nConnections: Int): ZLayer[Any, Nothing, ConnectionPool] =
ZLayer.succeed(new ConnectionPool(nConnections))
}
Effectful Dependency Injection
Use the layers at the end of your program, they will be passed automatically
val runnableProgram_v2 = program.provide(
UserSubscription.live,
EmailService.live,
UserDatabase.live,
ConnectionPool.live(10)
)
Benefits
• no need to manage dependencies manually
• each dependency is passed once to all dependents
Effectful Dependency Injection
Use the layers at the end of your program, they will be passed automatically
val runnableProgram_v2 = program.provide(
UserSubscription.live,
EmailService.live,
UserDatabase.live,
ConnectionPool.live(10)
)
Benefits
• the dependency graph is auto-detected
• each dependency is passed just once in the right place
• each layer can be easily swapped (e.g. for testing)
Example
ZIOnionex
• similar architecture as with Cats Effect
• dependencies auto-injected
def program = for {
_ <- ZIO.log("Rock the JVM! Bootstrapping...")
_ <- runMigrations
_ <- startServer
} yield ()
override def run =
program.provide(
// services
CompanyServiceLive.layer,
ReviewServiceLive.configuredLayer,
UserServiceLive.layer,
JWTServiceLive.configuredLayer,
EmailServiceLive.configuredLayer,
InviteServiceLive.configuredLayer,
PaymentServiceLive.configuredLayer,
OpenAIServiceLive.configuredLayer,
FlywayServiceLive.configuredLayer,
// repos
CompanyRepositoryLive.layer,
ReviewRepositoryLive.layer,
UserRepositoryLive.layer,
RecoveryTokensRepositoryLive.configuredLayer,
InviteRepositoryLive.layer,
// other requirements
configuredServer,
Repository.dataLayer
)
Pros
• layers "click" automagically
• each layer has clear responsibility
• easy to configure selectively
• easy to test individual layers
• easy to navigate
• easy to read, understand & maintain
Cons
• error management requires discipline
• any non-trivial impl can push type
inference to its limits
• any manual intervention in layers makes
code clunky
Effect Systems
Cats Effect
ZIO
à Kyo
Others
Kyo: Effects Come Last
The "pending" type describes requirements in the type signature
sealed infix type <[+A, -S]
// Int < Any is sugar for <[Int, Any]
val meaningOfLife: Int < Any = 42
"pending Any" means you can use raw values
The requirement must be executed to produce a value
def readConfFile(path: String): String = ...
val confString: String < IO =
IO(readConfFile("application.conf"))
The "pending" is a description of how an effect will be performed
Kyo: Effects Come Last
The "pending" type describes requirements in the type signature
sealed infix type <[+A, -S]
// Int < Any is sugar for <[Int, Any]
val meaningOfLife: Int < Any = 42
"pending Any" means you can use raw values
The requirement must be executed to produce a value
def readConfFile(path: String): String = ...
val confString: String < Sync =
Sync.defer(readConfFile("application.conf"))
Kyo: Effects Come Last
The requirement must be executed to produce a value
Possible requirements
• Sync: may perform side effects
• Async: may use fibers/light threads
• Abort: may terminate unexpectedly with an error
• Env, Layer: dependency injection
• Loop, Memo, Chunk, Stream, Var, Emit, Clock, Random, ...
• any combination of the above
def readConfFile(path: String): String = ...
val confString: String < Sync =
Sync.defer(readConfFile("application.conf"))
Kyo: Effects Come Last
The requirements may be combined
val declarationOfWar: String < (Abort[Exception] & Sync) =
Sync.defer("this is").map { b =>
Abort.get(Right("SPARTA")).map { c =>
b + " " + c
}
}
Is "pending" an "effect"?
• the type should describe the kind of computation
ü yes: the type describes an arbitrary computation
• the type description should contain the value type of the computation
ü yes: the type description contains the value type
• in case of side effects, the description and the execution of the effect are separate
ü yes: the second type argument must be executed first
Kyo: Pros & Cons
Pros
• familiar ideas: programs as values, composed
• increased clarity: types declare requirements to obtain values
• allows direct-style Scala with internal DSL, instead of map/flatMap
• supports multi-shot continuations
• can interoperate with Cats Effect and ZIO
Cons
• each requirement has its own rules for producing values, need to conform to all
• type inference is quickly pushed to the limit
But Why?
Why to Effect
Programs as Values
• ease of reading, understanding and refactoring your code
• clarity by the type system: capabilities, errors, values
• ease of testing and isolating code
• functional programming with programs!
But what about...?
• performance is unaffected
• fiber/light thread schedulers are top-notch, even surpassing Project Loom
Best for
• experienced functional programmers, especially in teams
• important software™
Main benefits
• increased safety and trust in the software
• high dev velocity
Why NOT to Effect
Programs as Values
• takes a while to learn
• requires comfort with FP
• needs experience to use successfully
Worst for
• inexperienced/mixed teams under pressure
• prototyping
• combination of low stakes + need for speed + another stack you know well
Dangers of using effects poorly
• unreadable code
• lost dev cycles
• broken safety, often worse than vanilla Scala
Scala rocks
rockthejvm.com
Plan
Text & bullet
• we love FP, ref transparency
• effect definitions/questions
• option, future
• my io
• Cats Effect – IO
• Cats Effect – concurrency, cancellation, tagless final
• ZIO – another IO, equivalence with R => Either[E,A]
• ZIO – concurrency, cancellation, layers
• Kyo – TODO
• Architecture comparison (things from RoP slides)
• When to use effect systems
• Drawbacks of effect systems
• Effects vs Direct-style Scala
• When not to use effect systems (monad transformers?)
TODO
• code examples
• pictures
• include some ScalaJS
keyword method[G](param: ActorSystem) => member + "some text” + aValOrVar
// comment
Regular slide
big comment
(x, y) => x + y
Text & bullet
• big bullet
• small bullet
small comment
Error message

To Effect or Not to Effect - a Scala Perspective [Func Prog Conf 2025]

  • 1.
  • 2.
    We ❤ FP Easyto read, understand and trust code • referential transparency • local reasoning def combine(a: Int, b: Int): Int = a + b val five = combine(2, 3) val five_v2 = 2 + 3 val five_v3 = 5 All the above are replaceable with one another
  • 3.
    We ❤ FP Sideeffects are "bad"... • throwing errors breaks control flow • changing data makes code hard to track • synchronizing state makes code bug-prone ... but they are necessary and inevitable • we must "make things happen" in the world • we must interact with the software val printSomething: Unit = println("Effects!") val printSomething_v2: Unit = () // not the same var anInt = 0 val changingVar: Unit = (anInt += 1) val changingVar_v2: Unit = () // not the same val service: Service = ... service.sendToKafka("some data") service.log("this just happened") val value = service.getOrFail[String]("myKey")
  • 4.
    Effects Manage possibly side-effectingcomputations as values • the type should describe the kind of computation • the type description should contain the value type of the computation • in case of side effects, the description and the execution of the effect are separate
  • 5.
    Effect example: Option IsOption an "effect"? • the type should describe the kind of computation ü yes, the type describes a possibly absent value • the type description should contain the value type of the computation ü yes, the type description contains the value type: the A that might be there • in case of side effects, the description and the execution of the effect are separate ü no side effects required, so... yes def maybeGetString() = if Random.nextBoolean() then "Scala" else null val anOption = Option(maybeGetString())
  • 6.
    Effect example: Future IsFuture an "effect"? • the type should describe the kind of computation ü yes, the type describes a computation that's run on a separate thread • the type description should contain the value type of the computation ü yes, it's the A that will be returned later • in case of side effects, the description and the execution of the effect are separate ❌ no, the execution starts immediately val aFuture: Future[Int] = Future { val someComputation = 40 + 2 Thread.sleep(1000) someComputation * 10 }
  • 7.
    Exercise: A CustomEffect case class MyEffect[A](unsafeRun: () => A) { def map[B](f: A => B): MyEffect[B] = MyEffect(() => f(unsafeRun())) def flatMap[B](f: A => MyEffect[B]): MyEffect[B] = MyEffect(() => f(unsafeRun()).unsafeRun()) } Put a computation in a box, suspend it Is this an "effect"? • the type should describe the kind of computation ü yes, it's an arbitrary computation that returns a value • the type description should contain the value type of the computation ü yes, it's the A that will be returned • in case of side effects, the description and the execution of the effect are separate ü yes, nothing gets executed until unsafeRun is called
  • 8.
    Super-Effects case class MyEffect[A](unsafeRun:() => A) { def map[B](f: A => B): MyEffect[B] = MyEffect(() => f(unsafeRun())) def flatMap[B](f: A => MyEffect[B]): MyEffect[B] = MyEffect(() => f(unsafeRun()).unsafeRun()) } Powerful abstraction • describes any computation • obeys monadic laws • is referentially transparent • can be chained, passed around, returned as result • can be enhanced with transformers, error handlers, etc • can be scheduled to on a thread
  • 9.
    Effect Systems à CatsEffect ZIO Kyo Others
  • 10.
    Cats Effect IO valourFirstIO: IO[Int] = IO.pure(42) val aDelayedIO: IO[Int] = IO.delay { println("I'm producing an integer") 54 } val improvedMeaningOfLife = ourFirstIO.map(_ * 2) val printedMeaningOfLife = ourFirstIO.flatMap(mol => IO.delay(println(mol))) def smallProgram(): IO[Unit] = for { line1 <- IO(StdIn.readLine()) line2 <- IO(StdIn.readLine()) _ <- IO.delay(println(line1 + line2)) } yield () Direct enhancement of "MyEffect" Super-effect: can model any computation A program as value
  • 11.
    Cats Effect IO:Superpowers val aFailedCompute: IO[Int] = IO.delay(throw new RuntimeException("A FAILURE")) val dealWithIt = aFailure.handleErrorWith { case _: RuntimeException => IO.delay(println("I'm still here")) // ... } Error handling val bigComputation: IO[Int] = ... val veryBigComputation: IO[String] = ... val bigCombination: IO[String] = (bigComputation, veryBigComputation).parMapN { (num, string) => s"my goal in life is $num and $string") } Parallelizing efffects
  • 12.
    Cats Effect IO:Concurrency Light threads aka Fibers def runOnSomeOtherThread[A](io: IO[A]) = for { fib <- io.start result <- fib.join } yield result A whole new world • semantic blocking: effects can wait without blocking a JVM thread • cancellation: effect chains can be interrupted without heavy Thread.interrupt calls • timeouts • retries • error handling • tupling • racing • ... all with the same assumption: IOs are FP-composable data structures
  • 13.
    Cats Effect IO:Concurrency Example: racing def testRace() = { val meaningOfLife = runWithSleep(42, 1.second) val favLang = runWithSleep("Scala", 2.seconds) val first: IO[Either[Int, String]] = IO.race(meaningOfLife, favLang) /* - both IOs run on separate fibers - the first one to finish will complete the result - the loser will be canceled */ first.flatMap { case Left(mol) => IO(s"Meaning of life won: $mol") case Right(lang) => IO(s"Fav language won: $lang") } } the only important call here
  • 14.
    Cats Effect IO:Concurrency Example: cancellation val specialPaymentSystem = ( IO("Payment running, don't cancel me...").debug >> IO.sleep(1.second) >> IO("Payment completed.").debug ).onCancel(IO("MEGA CANCEL OF DOOM!").debug.void) val cancellationOfDoom = for { fib <- specialPaymentSystem.start _ <- IO.sleep(500.millis) >> fib.cancel _ <- fib.join } yield () val atomicPayment = specialPaymentSystem.uncancelable Turn effects into transactions
  • 15.
    Cats Effect IO:Concurrency Example: pinpoint cancellation val authFlow: IO[Unit] = IO.uncancelable { poll => for { pw <- poll(inputPassword).onCancel(IO("Auth timeout, try later.").void) // this is cancelable verified <- verifyPassword(pw) // this is NOT cancelable _ <- if (verified) IO("Authentication successful.") // this is NOT cancelable else IO("Authentication failed.") } yield () } val authProgram = for { authFib <- authFlow.start _ <- IO.sleep(3.seconds) >> IO("Auth timeout, attempting cancel...") >> authFib.cancel _ <- authFib.join } yield ()
  • 16.
    Cats Effect: Concurrency Ref= effectful atomic reference val atomicMol: IO[Ref[IO, Int]] = IO.ref(42) // modifying is an effect val increasedMol: IO[Unit] = atomicMol.flatMap { ref => ref.set(43) // thread-safe } Deferred = effectful "promise" val aDeferred: IO[Deferred[IO, Int]] = Deferred[IO, Int] // get blocks the calling fiber (semantically) // until some other fiber completes the Deferred with a value val reader: IO[Int] = aDeferred.flatMap { signal => signal.get // blocks the fiber } val writer = aDeferred.flatMap { signal => signal.complete(42) }
  • 17.
    Ref + Deferred= ❤ Producer-consumer patterns Notification services Concurrency primitives • mutex • semaphore • cyclic barrier • countdown latch Controlled semantic blocking
  • 18.
    Cats Effect: Resource Usinga resource has 3 steps, and each is an effect 1. acquire 2. consume 3. release val connectionResource = Resource.make( IO(new Connection("rockthejvm.com")) // acquire )( conn => conn.close().void // release ) // ... later in your code val resourceFetchUrl = for { fib <- connectionResource.use(conn => conn.open() >> IO.never).start _ <- IO.sleep(1.second) >> fib.cancel } yield () Resource is a composable abstraction focused on acquire/release
  • 19.
    Cats Effect: Resource HasFP transformers 1. map/flatMap 2. error handlers 3. finalizers val connRes = for { file <- Resource.make(IO(openConfFile("myconfig.conf")))(file => IO(file.close())) url <- Resource.make(IO(file.getConnString()))(_ => IO.unit) conn <- Resource.make(IO(new Connection(url)))(conn => IO(conn.close())) } yield conn val ioWithFinalizer = connRes.use(...).guaranteeCase { case Succeeded(fa) => fa.flatMap(result => IO(s"releasing resource: $result").debug).void case Errored(e) => IO("using the reosource failed because $e").debug.void case Canceled() => IO("resource got canceled, releasing what's left").debug.void }
  • 20.
    Polymorphic Effects Instead ofIO[A], Option[A] or MyEffect[A], think general: F[A] Maintain capabilities of effects inside type classes 1. map/flatMap: Monad 2. error handlers: MonadError 3. cancellation and finalizers: MonadCancel 4. fibers/light threads: Spawn 5. general ref/deferred: Concurrent 6. lifting async computations to an effect: Async You can call the respective methods if you have the correct type class instance in scope
  • 21.
    Polymorphic Effects Instead ofIO[A], Option[A] or MyEffect[A], think general: F[A] val authFlow: IO[Unit] = IO.uncancelable { poll => for { pw <- poll(inputPassword).onCancel(IO("Auth timeout, try later.").void) // cancelable verified <- verifyPassword(pw) // this is NOT cancelable _ <- if (verified) IO("Authentication successful.") // this is NOT cancelable else IO("Authentication failed.") } yield () } def authFlow[F[_], E](using mc: MonadCancel[F,E]): F[Unit] = mc.uncancelable { poll => for { pw <- poll(inputPassword).onCancel(mc.pure("Auth timeout, try later.").void) // cancelable verified <- verifyPassword(pw) // this is NOT cancelable _ <- if (verified) mc.pure("Authentication successful.") // this is NOT cancelable else mc.pure("Authentication failed.") } yield () }
  • 22.
    domain domain domain Architecture Onionex • onion withsome layers segmented • hex without the ceremony domain repository service UI Testing Infra Apps domain domain domain user domain user repo user service "I want to log in" core
  • 23.
    domain domain domain Architecture Onionex • onion withsome layers segmented • hex without the ceremony domain repository service UI Testing Infra Apps domain domain domain user domain user repo user service "Here's your API key" core
  • 24.
    domain domain domain Architecture Onionex • onion withsome layers segmented • hex without the ceremony domain repository service UI Testing Infra Apps domain domain domain user domain user repo user service TEST ALL THINGS core
  • 25.
    domain domain domain Architecture Onionex • onion withsome layers segmented • hex without the ceremony domain repository service UI Testing Infra Apps domain domain user domain user repo TEST THE REPO core
  • 26.
    Architecture Onionex • onion withlayers segmented • hex without the ceremony core logic UI action Hex traits • "business logic" separated from FE/BE details • UI à core à actions flow via inversion of control
  • 27.
    A Backend Module Onionex •onion with some layers segmented • hex without the ceremony Polymorphic effects • Layers described in terms of capabilities • Programs (e.g. service calls) described in general terms • The enabler of capabilities instantiated last trait Auth[F[_]] { def login(email: String, password: String): F[Option[User]] def signUp(newUserInfo: NewUserInfo): F[Option[User]] ... } object LiveAuth { def apply[F[_]: Async: Logger]( users: Users[F], tokens: Tokens[F], emails: Emails[F] ): F[LiveAuth[F]] = ... // power up engines }
  • 28.
    Backend Onionex + Effects •onion with some layers segmented • hex without the ceremony object Core { def apply[F[_]: Async: Logger]( xa: Transactor[F], tokenConfig: TokenConfig, emailServiceConfig: EmailServiceConfig, stripeConfig: StripeConfig ): Resource[F, Core[F]] = { val coreF = for { jobs <- LiveJobs[F](xa) users <- LiveUsers[F](xa) tokens <- LiveTokens[F](users)(xa, tokenConfig) emails <- LiveEmails[F](emailServiceConfig) auth <- LiveAuth[F](users, tokens, emails) stripe <- LiveStripe[F](stripeConfig) } yield new Core(jobs, users, auth, stripe) Resource.eval(coreF) } } Pros • layers "click" like legos • each layer has its own capabilities (e.g. MonadThrow, Concurrent, etc) • easy to configure, configs propagate • easy to test individual layers • easy to replace implementations • easy to navigate • easy to read, understand & maintain
  • 29.
    object Core { defapply[F[_]: Async: Logger]( xa: Transactor[F], tokenConfig: TokenConfig, emailServiceConfig: EmailServiceConfig, stripeConfig: StripeConfig ): Resource[F, Core[F]] = { val coreF = for { jobs <- LiveJobs[F](xa) users <- LiveUsers[F](xa) tokens <- LiveTokens[F](users)(xa, tokenConfig) emails <- LiveEmails[F](emailServiceConfig) auth <- LiveAuth[F](users, tokens, emails) stripe <- LiveStripe[F](stripeConfig) } yield new Core(jobs, users, auth, stripe) Resource.eval(coreF) } } Backend Onionex + Effects • onion with some layers segmented • hex without the ceremony Cons • requires learning curve & discipline • errors are invisible unless surfaced • almost always ends up with Sync/Async • becomes tempting to misunderstand (implicit vs explicit DI) object Core { def apply[F[_] :Async :Logger :Jobs :Users :Tokens :Emails :Auth :Stripe]: F[Core[F]] = ... }
  • 30.
  • 31.
    Richer Super-Effects Super-effects cansuspend arbitrary computations Not a full program, does not include dependencies and errors case class MyEffect[A](unsafeRun: () => A) { def map[B](f: A => B): MyEffect[B] = ... def flatMap[B](f: A => MyEffect[B]): MyEffect[B] = ... } case class MyRichEffect[R,E,A](unsafeRun: R => Either[E,A]) { def map[B](f: A => B): MyRichEffect[R,E,B] = ... def flatMap[B](f: A => MyRichEffect[R,E,B]): MyRichEffect[R,E,B] = ... } Need a change of signature
  • 32.
    ZIO // success val meaningOfLife:ZIO[Any, Nothing, Int] = ZIO.succeed(42) // failure val aFailure: ZIO[Any, String, Nothing] = ZIO.fail("Something went wrong") val smallProgram = for { _ <- ZIO.succeed(println("what's your name")) name <- ZIO.succeed(StdIn.readLine()) _ <- ZIO.succeed(println(s"Welcome to ZIO, $name")) } yield () A richer super-effect Same core ideas • FP-style transformers • fibers/light threads and concurrency primitives • semantic blocking • cancellation and error handling • timeouts, retries
  • 33.
    Effectful Dependency Injection caseclass User(name: String, email: String) class UserSubscription(emailService: EmailService, userDatabase: UserDatabase) { def subscribeUser(user: User): Task[Unit] = ... // Task[A] == ZIO[Any, Throwable, A] } class EmailService { def email(user: User): Task[Unit] = ... } class UserDatabase(connectionPool: ConnectionPool) { def insert(user: User): Task[Unit] = ... } class ConnectionPool(nConnections: Int) { def get: Task[Connection] = ...} case class Connection() { def runQuery(query: String): Task[Unit] = ... } Assuming a standard layered app
  • 34.
    Effectful Dependency Injection valsubscriptionService = ZIO.attempt( UserSubscription.create( EmailService.create(), UserDatabase.create( ConnectionPool.create(10) ) ) ) Assuming a standard layered app, you normally need to build the object... def subscribe(user: User): ZIO[Any, Throwable, Unit] = for { sub <- subscriptionService _ <- sub.subscribeUser(user) } yield () ... before you can use it
  • 35.
    Effectful Dependency Injection Instead,assume that the required object exists, and use it... def subscribe_v2(user: User): ZIO[UserSubscription, Throwable, Unit] = for { sub <- ZIO.service[UserSubscription] // ZIO[UserSubscription, Nothing, UserSubscription] _ <- sub.subscribeUser(user) } yield () val program = for { _ <- subscribe(User("Daniel", "daniel@rockthejvm.com")) _ <- subscribe(User("Bon Jovi", "jon@rockthejvm.com")) } yield () ... then provide it later but how?
  • 36.
    Effectful Dependency Injection Createlayers, i.e. effects that will be run when required object UserSubscription { val live: ZLayer[EmailService with UserDatabase, Nothing, UserSubscription] = ZLayer.fromFunction(new UserSubscription(_, _)) } object EmailService { val live: ZLayer[Any, Nothing, EmailService] = ZLayer.succeed(new EmailService) } object UserDatabase { val live: ZLayer[ConnectionPool, Nothing, UserDatabase] = ZLayer.fromFunction(new UserDatabase(_)) } object ConnectionPool { def live(nConnections: Int): ZLayer[Any, Nothing, ConnectionPool] = ZLayer.succeed(new ConnectionPool(nConnections)) }
  • 37.
    Effectful Dependency Injection Usethe layers at the end of your program, they will be passed automatically val runnableProgram_v2 = program.provide( UserSubscription.live, EmailService.live, UserDatabase.live, ConnectionPool.live(10) ) Benefits • no need to manage dependencies manually • each dependency is passed once to all dependents
  • 38.
    Effectful Dependency Injection Usethe layers at the end of your program, they will be passed automatically val runnableProgram_v2 = program.provide( UserSubscription.live, EmailService.live, UserDatabase.live, ConnectionPool.live(10) ) Benefits • the dependency graph is auto-detected • each dependency is passed just once in the right place • each layer can be easily swapped (e.g. for testing)
  • 39.
    Example ZIOnionex • similar architectureas with Cats Effect • dependencies auto-injected def program = for { _ <- ZIO.log("Rock the JVM! Bootstrapping...") _ <- runMigrations _ <- startServer } yield () override def run = program.provide( // services CompanyServiceLive.layer, ReviewServiceLive.configuredLayer, UserServiceLive.layer, JWTServiceLive.configuredLayer, EmailServiceLive.configuredLayer, InviteServiceLive.configuredLayer, PaymentServiceLive.configuredLayer, OpenAIServiceLive.configuredLayer, FlywayServiceLive.configuredLayer, // repos CompanyRepositoryLive.layer, ReviewRepositoryLive.layer, UserRepositoryLive.layer, RecoveryTokensRepositoryLive.configuredLayer, InviteRepositoryLive.layer, // other requirements configuredServer, Repository.dataLayer ) Pros • layers "click" automagically • each layer has clear responsibility • easy to configure selectively • easy to test individual layers • easy to navigate • easy to read, understand & maintain Cons • error management requires discipline • any non-trivial impl can push type inference to its limits • any manual intervention in layers makes code clunky
  • 40.
  • 41.
    Kyo: Effects ComeLast The "pending" type describes requirements in the type signature sealed infix type <[+A, -S] // Int < Any is sugar for <[Int, Any] val meaningOfLife: Int < Any = 42 "pending Any" means you can use raw values The requirement must be executed to produce a value def readConfFile(path: String): String = ... val confString: String < IO = IO(readConfFile("application.conf")) The "pending" is a description of how an effect will be performed
  • 42.
    Kyo: Effects ComeLast The "pending" type describes requirements in the type signature sealed infix type <[+A, -S] // Int < Any is sugar for <[Int, Any] val meaningOfLife: Int < Any = 42 "pending Any" means you can use raw values The requirement must be executed to produce a value def readConfFile(path: String): String = ... val confString: String < Sync = Sync.defer(readConfFile("application.conf"))
  • 43.
    Kyo: Effects ComeLast The requirement must be executed to produce a value Possible requirements • Sync: may perform side effects • Async: may use fibers/light threads • Abort: may terminate unexpectedly with an error • Env, Layer: dependency injection • Loop, Memo, Chunk, Stream, Var, Emit, Clock, Random, ... • any combination of the above def readConfFile(path: String): String = ... val confString: String < Sync = Sync.defer(readConfFile("application.conf"))
  • 44.
    Kyo: Effects ComeLast The requirements may be combined val declarationOfWar: String < (Abort[Exception] & Sync) = Sync.defer("this is").map { b => Abort.get(Right("SPARTA")).map { c => b + " " + c } } Is "pending" an "effect"? • the type should describe the kind of computation ü yes: the type describes an arbitrary computation • the type description should contain the value type of the computation ü yes: the type description contains the value type • in case of side effects, the description and the execution of the effect are separate ü yes: the second type argument must be executed first
  • 45.
    Kyo: Pros &Cons Pros • familiar ideas: programs as values, composed • increased clarity: types declare requirements to obtain values • allows direct-style Scala with internal DSL, instead of map/flatMap • supports multi-shot continuations • can interoperate with Cats Effect and ZIO Cons • each requirement has its own rules for producing values, need to conform to all • type inference is quickly pushed to the limit
  • 46.
  • 47.
    Why to Effect Programsas Values • ease of reading, understanding and refactoring your code • clarity by the type system: capabilities, errors, values • ease of testing and isolating code • functional programming with programs! But what about...? • performance is unaffected • fiber/light thread schedulers are top-notch, even surpassing Project Loom Best for • experienced functional programmers, especially in teams • important software™ Main benefits • increased safety and trust in the software • high dev velocity
  • 48.
    Why NOT toEffect Programs as Values • takes a while to learn • requires comfort with FP • needs experience to use successfully Worst for • inexperienced/mixed teams under pressure • prototyping • combination of low stakes + need for speed + another stack you know well Dangers of using effects poorly • unreadable code • lost dev cycles • broken safety, often worse than vanilla Scala
  • 49.
  • 50.
    Plan Text & bullet •we love FP, ref transparency • effect definitions/questions • option, future • my io • Cats Effect – IO • Cats Effect – concurrency, cancellation, tagless final • ZIO – another IO, equivalence with R => Either[E,A] • ZIO – concurrency, cancellation, layers • Kyo – TODO • Architecture comparison (things from RoP slides) • When to use effect systems • Drawbacks of effect systems • Effects vs Direct-style Scala • When not to use effect systems (monad transformers?) TODO • code examples • pictures • include some ScalaJS
  • 51.
    keyword method[G](param: ActorSystem)=> member + "some text” + aValOrVar // comment Regular slide big comment (x, y) => x + y Text & bullet • big bullet • small bullet small comment Error message