Piotr Gawryś | twitter.com/p_gawrys
Replacing an expression by its bound value doesn't alter the behavior of your program. Why?
See this excellent description if you wish to know more!
import cats.effect.IO
val ioa = IO { println("hi KSUG!") }
// nothing happens yet, it's just a description
// of evaluation that will produce `Unit` or
// end with error
val program: IO[Unit] =
for {
_ <- ioa
_ <- ioa
} yield ()
// prints "hi KSUG!" twice
program.unsafeRunSync()
import cats.effect.IO
import cats.syntax.apply._
val program: IO[Unit] =
ioa.flatMap(_ => ioa)
val program: IO[Unit] =
ioa *> ioa
import cats.effect.IO
import scala.concurrent.duration._
// do `fa` followed by `fb`,
// discard the first result
def *>[A, B](fa: F[A])(fb: F[B]): F[B] =
map2(fa, fb)((_, b) => b)
// waits 100 millis (without blocking!)
// then prints "woke up"
IO.sleep(100.millis) *> IO(println("woke up"))
import cats.effect.IO
val ioa = IO(println("A"))
val iob = IO.raiseError(new Exception("B"))
val ioc = IO(println("C"))
val program: IO[Int] =
for {
_ <- ioa
_ <- iob // short circuits IO with error
_ <- ioc // ioc will never happen!
} yield 42
// A
// Exception in thread "main" java.lang.Exception: B
program.unsafeRunSync()
// A
// Process finished with exit code 0
val safeProgram: IO[Either[Throwable, Int]] =
program.attempt
safeProgram.unsafeRunSync()
// will print A, B and C
val program: IO[Int] =
for {
_ <- ioa
// in case of error executes IO in argument
_ <- iob.handleErrorWith(_ => IO(println("B")))
_ <- ioc
} yield 42
import cats.syntax.applicativeError._
import cats.effect.IO
val ioa = IO(println("A"))
val iob = IO.raiseError(new Exception("B"))
val ioc = IO(println("C"))
def handleError: PartialFunction[Throwable, IO[Unit]] = {
case _ => IO(println("errB"))
}
// prints "A", "errB" and ends with error
val program =
for {
_ <- ioa
// executes PartialFunction and rethrows error
_ <- iob.onError(handleError)
_ <- ioc
} yield 42
Good place to look for methods are MonadError and ApplicativeError
import cats.effect.IO
import cats.instances.list._
import cats.syntax.all._
val ioa = IO(println("A"))
val iob = IO(println("B"))
val listOfIO: List[IO[Unit]] = List(ioa, iob)
val ioOfList: IO[List[Unit]] = listOfIO.parSequence
// using Parallel typeclass instance
val parIO: IO[Unit] =
(ioa, iob).parMapN((_, _) => ())
val ioInParallel: IO[Unit] =
for {
// run in "background"
fibA <- ioa.start
fibB <- iob.start
// wait for ioa result
_ <- fibA.join
_ <- fibB.join
} yield ()
// run both in parallel
// and cancel the loser
val raceIO: IO[Either[Unit, Unit]] =
IO.race(ioa, iob)
See documentation and scala docs for more information!
Purely functional wrapper for AtomicRef. Allows for safe concurrent access and mutation but preserves referential transparency.
abstract class Ref[F[_], A] {
def get: F[A]
def set(a: A): F[Unit]
def modify[B](f: A => (A, B)): F[B]
// ... and more
}
changes Ref to A but can return something else to the caller
import cats.effect.IO
import cats.effect.concurrent.Ref
// Creating `Ref` returns `IO[Ref[...]]`
// To use it we need to flatMap over it.
// Keep in mind that IO is just description
// of operation - running it several times
// will repeat everything, so running it twice
// will produce two different Refs
val stringsRef: IO[Ref[IO, List[String]]] =
Ref.of[IO, List[String]](List())
def take(ref: Ref[IO, List[String]]): IO[Option[String]] =
ref.modify(list => (list.drop(1), list.headOption))
def put(ref: Ref[IO, List[String]], s: String): IO[Unit] =
ref.modify(list => (s :: list, ()))
val program: IO[Unit] =
for {
_ <- stringsRef.flatMap(put(_, "KSUG"))
element <- stringsRef.flatMap(take)
} yield println(element)
program.unsafeRunSync() // prints "None"
val program: IO[Unit] =
for {
ref <- stringsRef
_ <- put(ref, "KSUG")
element <- take(ref)
} yield println(element)
program.unsafeRunSync() // prints "Some(KSUG)"
Confusing? On the contrary!
It forces us to share state in only one possible way - passing it as a parameter.
The regions of sharing are exactly the same of your call graph - no more wondering which class share this state.
Want to go more in-depth regarding sharing state in FP? Check out this blog post
Request passwords from the server and decrypt it using provided Decrypter class.
It can process up to 4 calls in parallel, decrypts in three stages (prepare, decode, decrypt), randomly fails with Exception and silently corrupts other running instances unless you restart it.
The task is to have as high throughput and correctness as you can achieve
Decrypter has three methods that we need to invoke in order:
class Decrypter {
...
def prepare(password: String): PasswordPrepared
def decode(state: PasswordPrepared): PasswordDecoded
def decrypt(state: PasswordDecoded): String
}
PasswordClient which handles HTTP requests is already implemented.
If you with to try it locally:
git clone https://github.com/VirtusLab/akka-workshop-client.git
cd akka-workshop-client
git checkout cats-effect-master-step1
git clone https://github.com/VirtusLab/akka-workshop.git
cd akka-workshop/server
sbt run
In Main.scala you'll see run method which starts HTTP Client and proceeds to invoke methods responsible for decrypting passwords.
override def run(args: List[String]): IO[ExitCode] =
Http1Client[IO]()
.bracket { httpClient =>
val client = PasswordClient.create(httpClient)
for {
token <- client.requestToken("Piotrek")
_ <- decryptForever(client, token)(timer)
} yield ExitCode.Success
}(_.shutdown)
change it to your name
LEADERBOARD
http://async-in-2018.herokuapp.com/?mode=remote
For the first step we have to use existing PasswordClient to request, decrypt and validate few passwords! For now we don't care about Decrypter failures.
abstract class PasswordClient[F[_]](httpClient: Client[F]) {
def requestToken(userName: String): F[Token]
def requestPassword(token: Token): F[EncryptedPassword]
def validatePassword(token: Token, encryptedPassword: String,
decryptedPassword: String): F[Status]
}
Methods should be self-explanatory - feel free to modify their implementation if you want to add logging - I recommend .flatTap(a => IO(println(a)) defined in cats!
If you don't recognize F[_] don't worry - it just means that we could write implementation for something other than IO
Go up on the leaderboard!
http://async-in-2018.herokuapp.com/?mode=parallel
If you follow provided skeleton the structure is just IO running forever so now try to do the same for N IO running in parallel.
One of the earlier slides should give you a hint!
If you see full leaderboard you're probably notice awful correctness after parallelization.
This is because one of the parallel IO can corrupt the decrypter and it won't restart until all of them fail.
Let's think about the mechanism that could stop other tasks as soon as one of them fails.
Unlike Akka, Cats-Effect doesn't have any supervision mechanism included.
The point of Cats-Effect and IO model in general is to have simple, composable building blocks to easily build more complicated structures.
If you share state between parallel IOs you can use it to signal failure. I'd suggest refreshing Ref
Most straightforward way is to introduce checkpoints in the decryption code to determine whether we should stop our recursive calls or not.
On failure we should set signal and terminate.
With previous step done it's possible to have 100% correctness without sacrificing parallelism.
It doesn't mean we can't improve our throughput.
So far we restart as soon as possible if error occurs in any task but we don't have to discard our progress.
If we save our last step we could improve performance quite a bit!
To do that I'd suggest sharing state once again. This time we could use something like Ref[IO, List[Password]].
All that has to be done is to introduce new parameter and save the password before terminating.
def getPassword(client: PasswordClient[IO], token: Token): IO[Password] = {
client.requestPassword(token)
}
This looks like a good place to take saved passwords instead of requesting them each time.
Now that everything is working fast and correct we could take a closer look at how the code is organized.
fullDecryption functions is probably cluttered with two Refs and a lot of Ref specific logic like modify.
We could get inspiration from existing PasswordClient and define algebra that exposes clean interface, e.g.:
trait PasswordQueue[F[_]] {
def take: F[Option[Password]]
def put(password: Password): F[Unit]
}
And the implementation:
object PasswordQueue {
def create[F[_] : Sync]: F[PasswordQueue[F]] =
Ref.of[F, List[Password]](List.empty).map { state =>
new PasswordQueue[F] {
def take: F[Option[Password]] = ???
def put(password: Password): F[Unit] = ???
}
}
}
This trick completely separates Ref behind interface closer to domain logic.