SCALA3 & ASYNC:


[BEHIND FUTURES]
RUSLAN SHEVCHENKO <RUSLAN@SHEVCHENKO.KIEV.UA>


@RSSH1
https://github.com/rssh/dotty-cps-async
[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
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.
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
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.
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 ?
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 ….
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
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
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
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
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.
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:
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:
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]
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


}
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.
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
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


}
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
What we need from monad:
Control flow: trait CpsMonad[F[_]]:


def pure[A](x:A):F[A]


def map[A,B](fa:F[A])(f: A=>B): F[B]


def flatMap[A,B](fa: F[A])(f: A=>F[B]): F[B]


Try/catch: trait CpsTryMonad[F[_]] extends CpsMonad[F]:


def error[A](e: Throwable): F[A]


def flatMapTry[A,B](fa: [A])(f: Try[A]=>B)
async[M] {


await[Future](…)


}
trait CpsAsyncMonad[F[_]] extends CpsTryMonad[F]:


def adoptCallbackStyle[A](source: (Try[A]=>Unit) => Unit): F[A]


async[Future] {


await[M](…)


}
trait CpsSchedulingMonad[F[_]] extends CpsAsyncMonad[F]:


def spawn[A](op: F[A]): F[A]
Automatic colouring: trait CpsMemoizingMonad[F[_]] extends CpsAsyncMonad[F]:


transient inline def memoize[A](v: F[A])
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]]
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


}
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


}
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])
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.
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]) = ...


...
High-order functions [Local Substitutions]: Categorical interpreation.
Arrows:
Functors:
Left Kan Extension …
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).
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
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.
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.
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.
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
How to help
https://github.com/rssh/dotty-cps-async

Ecosystem:

cps-async-connect

scala-gopher

Start using in examples and pet-project:

Discover techniques and bugs.

Di
ff
erent types of monads (probabilistic programming, streams, etc … )

Provide feedback.

Svitla talks 2021_03_25

  • 1.
    SCALA3 & ASYNC: [BEHINDFUTURES] RUSLAN SHEVCHENKO <RUSLAN@SHEVCHENKO.KIEV.UA> @RSSH1 https://github.com/rssh/dotty-cps-async
  • 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. : MonadicEffect 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 defdoBoringStaff(request: Request): Response = val query = buildQuery(request) val result = db.select(query) resut.toResponse App Server Web DB Thread blocked
  • 5.
    Reminder: 02: reactivity defdoBoringStaff(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 defdoBoringStaff(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 defdoBoringStaff(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 defdoBoringStaff(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: Effectmonads: 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: forcomprehension: 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 defdoBoringStaff(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 dothis [?] 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 CPStransform 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
  • 21.
    What we needfrom monad: Control flow: trait CpsMonad[F[_]]: def pure[A](x:A):F[A] def map[A,B](fa:F[A])(f: A=>B): F[B] def flatMap[A,B](fa: F[A])(f: A=>F[B]): F[B] Try/catch: trait CpsTryMonad[F[_]] extends CpsMonad[F]: def error[A](e: Throwable): F[A] def flatMapTry[A,B](fa: [A])(f: Try[A]=>B) async[M] { await[Future](…) } trait CpsAsyncMonad[F[_]] extends CpsTryMonad[F]: def adoptCallbackStyle[A](source: (Try[A]=>Unit) => Unit): F[A] async[Future] { await[M](…) } trait CpsSchedulingMonad[F[_]] extends CpsAsyncMonad[F]: def spawn[A](op: F[A]): F[A] Automatic colouring: trait CpsMemoizingMonad[F[_]] extends CpsAsyncMonad[F]: transient inline def memoize[A](v: F[A])
  • 22.
    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]]
  • 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-stapproach: 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 [LocalSubstitutions] 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 [LocalSubstitutions] 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 [LocalSubstitutions]: Categorical interpreation. Arrows: Functors: Left Kan Extension …
  • 29.
    High-order functions: privatedata 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 areequal 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: IOproblems 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: IOproblems 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: IOproblems 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
  • 35.
    How to help https://github.com/rssh/dotty-cps-async Ecosystem: cps-async-connect scala-gopher Startusing in examples and pet-project: Discover techniques and bugs. Di ff erent types of monads (probabilistic programming, streams, etc … ) Provide feedback.