2. [Q]. Need 101 ?
Can be functional concurrent programming be liberated
from the monadic style ?
// ScalaUA-2020, ScalaR-2020. (Year ago)
- what’s after a year.
- what’s interesting behind async/await
3. Remind-01. : Monadic Effect
E
ff
ect: [Something, which change the behaviour of program in non-functional way]
Exceptions
Context switching.
IO
Monadic E
ff
ect
Behaviour injected in monad.
M(f), when we throw exception, we call method in M which suspect f and return exception
You can look at e
ff
ect monads as an interpreter layer.
4. Reminder: 02: reactivity
def doBoringStaff(request: Request): Response =
val query = buildQuery(request)
val result = db.select(query)
resut.toResponse
App Server
Web DB
Thread blocked
5. Reminder: 02: reactivity
def doBoringStaff(request: Request): Response =
val query = buildQuery(request)
val result = db.select(query)
resut.toResponse
App Server
Web DB
Thread blocked
Web
DB
Oops, all thread blocked
Request will wait.
6. Reminder: 02: reactivity
def doBoringStaff(request: Request): Response =
val query = buildQuery(request)
val result = db.select(query)
resut.toResponse
Web
DB
Oops, all thread blocked
Request will wait.
WTF
- operation system should care about this ?
- context switching between kernel/user mode is slow
- operation system research is died slow
- language runtime should care about this ?
7. Reminder: 02: reactivity
def doBoringStaff(request: Request): Response =
val query = buildQuery(request)
val result = db.select(query)
resut.toResponse
WTF
- language runtime should care about this ?
- JVM interpreter is stack based, each switch to other thread
means changing/ copying stack.
- Project Loom: https://openjdk.java.net/projects/loom/
- Second attempt to do this on JVM
- Virtual threads [Java Entity] with same API as native Thread
- Work in progress ….
8. Reminder: 02: reactivity
def doBoringStaff(request: Request): Response =
val query = buildQuery(request)
val result = db.select(query)
resut.toResponse
def doBoringStaff(request: Request): F[Response] =
query = buildQuery(request)
for{
result <- db.select(query)
} yield resut.toResponse
Effect Monad
9. Reminder: 03: Effect monads: for comprehension
def doBoringStaff(request: Request): F[Response] =
query = buildQuery(request)
for{
result <- db.select(query)
} yield resut.toResponse
Effect Monad
All monads are equal but some are more equal than others
Future
Cats-Effect IO
ZIO
Abstract F[_]
Monix Task Roll you own …
Haskell IO
ScalaZ IO
10. def doBoringStaff(request: Request): F[Response] =
for{
optUser <- usersStorage.findUser(request.userId)
user <- F.fromOption(optUser)
_ <- monitoredAccess(user).ifM(logAccess(user), IO.void)
query = buildQuery(user, request)
result1 <- db1.select(query)
result2 <- db2.select(query)
result = merge(result1,result2)
} yield resut.toResponse
Effect monads: for comprehension: real world
11. def doBoringStaff(request: Request): F[Response] =
for{
optUser <- usersStorage.findUser(request.userId)
user <- F.fromOption(optUser)
_ <- monitoredAccess(user).ifM(logAccess(user), IO.void)
query = buildQuery(user, request)
result1 <- db1.select(query)
result2 <- db2.select(query)
result = merge(result1,result2)
} yield resut.toResponse
Effect monads: for comprehension: real world
Sequential execution
12. Effect monads: for comprehension: real world
def doBoringStaff(request: Request): F[Response] =
for{
optUser <- usersStorage.findUser(request.userId)
user <- F.fromOption(optUser)
_ <- monitoredAccess(user).ifM(logAccess(user), IO.void)
query = buildQuery(user, request)
(result1, result2) <- db1.select(query)|+|db2.select(query)
result = merge(result1,result2)
} yield result.toResponse
DSL instead control flow.
What to do if we need ‘real for’ ?
Extra layer of complexity.
13. Effect monads: async/await: real world
def doBoringStaff(request: Request):F[Response] = async[F] {
user = await(userStorage.findUser(request.userId).getOrElse(
throw new IllegalArgumentException("user not found")))
if (await(monitoredAccess(user)))
await(logAccess(user))
val query = buildQuery(user, request)
val result1 = db1.select(query)
val result2 = db2.select(query)
merge( await(result1), await(result2) ).toResponse
}
Better, but:
Manual colouring:
Too many awaits:
14. Effect monads: async/await: colouring
def doBoringStaff(request: Request): F[Response] = async[F] {
user = await(userStorage.findUser(request.userId)).getOrElse(
throw new IllegalArgumentException("user not found"))
if (await(monitoredAccess(user)))
await(logAccess(user))
val query = buildQuery(user, request)
val result1 = db1.select(query)
val result2 = db2.select(query)
merge( await(result1), await(result2) ).toResponse
}
Better, but:
Manual colouring:
Too many awaits:
15. Effect monads: async/await: automatic colouring
import cps.imlicitAwait
def doBoringStaff(request: Request): F[Response] = async[F] {
user = userStorage.findUser(request.userId).getOrElse(
throw new IllegalArgumentException("user not found"))
if (monitoredAccess(user))
await(logAccess(user))
val query = buildQuery(user, equest)
val result1 = db1.select(query)
val result2 = db2.select(query)
merge( result1, result2 ).toResponse
}
[currently work only for Future, support for IO is in progress]
16. Reminder: 02: reactivity
def doBoringStaff(request: Request): Response =
val query = buildQuery(request)
val result = db.select(query)
resut.toResponse
def doBoringStaff(request: Request): F[Response] = async[F] {
val query = buildQuery(request)
val result = db.select(query)
resut.toResponse
}
17. How to do this [?]
https://github.com/rssh/dotty-cps-async
Optimised monadic CPS [Continuation Passing Style] transform.
Challenges:
Size of the language.
Monads interoperability.
High-order functions.
Automatic colouring.
18. Optimised monadic CPS transform
def doBoringStaff(request: Request): F[Response] = async[F] {
val query = buildQuery(request)
val result = db.select(query)
resut.toResponse
}
def doBoringStaff(request: Request): F[Response] = {
val m = summon[CpsMonad[F]]
val query = buildQuery(request)
m.map(db.select(query)){ x =>
val result = x
resut.toResponse
}
}
// implicitly[CpsMonad[F]]. In scala2
19. Effect monads: async/await: automatic colouring
import cps.imlicitAwait
def doBoringStaff(request: Request): F[Response] = async[F] {
user = userStorage.findUser(request.userId).getOrElse(
throw new IllegalArgumentException("user not found"))
if (monitoredAccess(user))
await(logAccess(user))
val query = buildQuery(user, equest)
val result1 = db1.select(query)
val result2 = db2.select(query)
merge( result1, result2 ).toResponse
}
20. Effect monads: async/await: macro output
def doBoringStaff(request: Request): IO[Response] = {
m = summon[CpsMonad[F]]
m.flatMap(userStorage.findUser(request.userId)){ x =>
m.flatMap(summon[AsyncShift[Option[User]]].getOrElse(m,u)(
() => m.error(new IllegalArgumentException("user not found")))
){ x =>
val user = x
m.flatMap(monitoredAccess(user)){ x =>
m.flatMap(logAccess(user)){ x =>
val query = buildQuery(user, equest)
val result1 = db1.select(query)
val result2 = db2.select(query)
m.flatMap(result1){ a1 =>
m.flatMap(result2){ a2 =>
merge( a1, a2 )
} } } } } } }
// In principle, near the same code as written by hand with for-comprehension
23. Monads interoperability
def doPOST(uri: String, value: ujson.Value): Future[ujson.Value] = async[Future] {
val request = HttpRequest.newBuilder()
.uri(new URI(uri))
.header("Content-Type","application/json")
.POST(HttpRequest.BodyPublishers.ofString(write(value)))
.build()
val response = await(client.sendAsync(request,HttpResponse.BodyHandlers.ofString()))
parseResponse(response)
}
java.util.concurrent.CompletableFuture[HttpResponse[String]]
trait CpsMonadConversion[F[_],G[_]]:
def apply[T](mf:CpsMonad[F],mg:CpsMonad[G],ft:F[T]):G[T]
given toFutureConversion[F[_]](using ExecutionContext, CpsSchedulingMonad[F]): CpsMonadConversion[F,Future] =
new CpsMonadConversion[F, Future] {
override def apply[T](mf: CpsMonad[F], mg: CpsMonad[Future], ft:F[T]): Future[T] =
val p = Promise[T]()
val u = summon[CpsSchedulingMonad[F]].mapTry(ft){
case Success(v) => p.success(v)
case Failure(ex) => p.failure(ex)
}
summon[CpsSchedulingMonad[F]].spawn(u)
p.future
}
24. Monads interoperability
def doPOST(uri: String, value: ujson.Value): Future[ujson.Value] = async[Future] {
val request = HttpRequest.newBuilder()
.uri(new URI(uri))
.header("Content-Type","application/json")
.POST(HttpRequest.BodyPublishers.ofString(write(value)))
.build()
val response = await(client.sendAsync(request,HttpResponse.BodyHandlers.ofString()))
parseResponse(response)
}
java.util.concurrent.CompletableFuture[HttpResponse[String]]
given toFutureConversion[F[_]](using ExecutionContext, CpsSchedulingMonad[F]): CpsMonadConversion[F,Future] =
new CpsMonadConversion[F, Future] {
override def apply[T](mf: CpsMonad[F], mg: CpsMonad[Future], ft:F[T]): Future[T] =
val p = Promise[T]()
val u = summon[CpsSchedulingMonad[F]].mapTry(ft){
case Success(v) => p.success(v)
case Failure(ex) => p.failure(ex)
}
summon[CpsSchedulingMonad[F]].spawn(u)
p.future
}
25. High-order functions [1]
1-st approach:
By-Name parameters as functions from 0 arguments:
Local substitutions
Private data
Iterable[A] . map[B](f : A ⇒ B) : Iterable[B]
AsyncShift[Iterable[A]] . map[F[_], B](m : CpsMonad[F], c : Iterable[A])
(f : A = > F[B]) : F[Iterable[B]]
Option[T] . getOrElse(x := > T)
AsyncShift[Option[T]] . getOrElse[F[_])(m : CpsMonad[F], c : Option[T])(x : () = > F[T])
26. High-order functions [Local Substitutions]
async[F]{
for{ log <- logs if (await(isMalicious(log.url))
} yield (await(report(log)))
}
async[F] {
logs.withFilter( log => await(isMalicious(log.url)) )
.map(log => await(report(log)))
}
=
We want navigate via list of logs once.
With previous approach we will get filtered list of urls, and then iterate on it.
27. High-order functions [Local Substitutions]
Solution: create local WithFilter, which will run both filter and map in map.
class DelayedWithFilter[F[_], A, C[_], CA <: C[A]](c: CA,
m: CpsMonad[F],
p:A=>F[Boolean],
) extends CallChainAsyncShiftSubst[F, WithFilter[A,C], F[WithFilter[A,C]] ]:
// return eager copy
def _origin: F[WithFilter[A,C]] = ...
def map[B](f: A => B): F[C[B]] = ...
def map_async[B](f: A => F[B]): F[C[B]] = ...
def flatMap[B](f: A => IterableOnce[B]): F[C[B]] = ...
def flatMap_async[B](f: A => F[IterableOnce[B]]): F[C[B]] = ...
def withFilter(q: A=>Boolean) =
DelayedWithFilter(c, m, x => m.map(p(x))(r => r && q(x)) )
def withFilter_async(q: A=> F[Boolean]) = ...
...
28. High-order functions [Local Substitutions]: Categorical interpreation.
Arrows:
Functors:
Left Kan Extension …
29. High-order functions: private data
class MyIntController:
def modify(f: Int => Int): Int = …
def modify_async[F[_]](m: CpsMonad[M])(f: Int => F[Int]): F[Int] = …
- add a <x>_async (or <x>Async) method to you class….
class Select[F[_]]:
def fold[S](s0:S)(step: S => S | SelectFold.Done[S]): S = …
def foldAsync[S](s0:S)(step: S => F[S | SelectFold.Done[S]]): F[S] = …
- F[_] can be in class. (Useful for DSL-s).
30. Effect monads: async/await: automatic colouring
import cps.imlicitAwait
def doBoringStaff(request: Request): F[Response] = async[F] {
user = userStorage.findUser(request.userId).getOrElse(
throw new IllegalArgumentException("user not found"))
if (monitoredAccess(user))
await(logAccess(user))
val query = buildQuery(user, equest)
val result1 = db1.select(query)
val result2 = db2.select(query)
merge( result1, result2 ).toResponse
}
- when F = Future[_]. - all ok.
- F = IO[_] — ambiguity
31. All monads are equal but some are more equal than others
Future:
represent already started computation.
cached by default.
Cats-effect IO, ZIO, scalaz IO:
represent computation, which are ‘not started’
not cached
referential transparency
val future = Future{ something }
val a = await(future)
val b = await(future)
- something will be evaluated once.
- the a and b will be the same object.
val io = IO.delay{ something }
val a = await(io)
val b = await(io)
- something will be evaluated twice.
- the a and b will not be the same object.
32. Automatic colouring: IO problems
def ctr(name:String) = async[IO] {
val counter = counters(name).getOrThrow()
val value = counter(name).increment()
if (value % LOG_MOD == 0) then
log(s"counter $name is $value")
if (value == THRESHOLD) then
log(s"counter $name exceeded treshold")
}
def ctr(name:String) = async[IO] {
val counter = counters(name).getOrThrow()
val value = counter(name).increment()
if (await(value) % LOG_MOD == 0) then
log(s"counter $name is ${await(value)}”)
if (await(value) == THRESHOLD) then
log(s"counter $name exceeded treshold")
}
def ctr(name:String) = async[IO] {
val counter = counters(name).getOrThrow()
val value = await(counter(name).increment())
if (value % LOG_MOD == 0) then
log(s"counter $name is $value")
if (value == THRESHOLD) then
log(s"counter $name exceeded treshold")
}
V1
V2
- this variant will be chosen by implicitAwait
- value will be increment 3 times.
33. Automatic colouring: IO problems
def ctr(name:String) = async[IO] {
val counter = counters(name).getOrThrow()
val value = Memoize[counter(name).increment()]
if (await(value) % LOG_MOD == 0) then
log(s"counter $name is ${await(value)}”)
if (await(value) == THRESHOLD) then
log(s"counter $name exceeded treshold")
}
- Memoize:
- Future[T]: nothing
- IO: IO[IO[T]]
- Monix Task: Task[T]
Solution: memoize values. If you store something in val, then you want to reuse it.
34. Automatic colouring: IO problems
def ctr(name:String) = async[IO] {
val counter = counters(name).getOrThrow()
val valueMem = Memoize[counter(name).increment()]
val value = await(valueMem)
if (await(value) % LOG_MOD == 0) then
log(s"counter $name is ${await(value)}”)
if (await(value) == THRESHOLD) then
log(s"counter $name exceeded treshold")
}
- Memoize:
- We break referential transparency;
- But it was already broken when we remove awaits…
Solution: memoize values. If you store something in val, then you want to reuse it.
// currently in development, will be in the next version