Ecosystèmes Scala:
Lightbend VS Typelevel
Présentation
Loïc Descotte
- Développeur Scala
- Architecte chez Kaizen Solutions
Lightbend
Entreprise qui offre du support autour de Scala et de librairies Scala (et avec des API Java)
Propose une transition simple depuis Java via des librairies mixant OO et FP
Orienté architectures d’entreprises : micro services, cloud, etc.
Typelevel
Communauté open source promouvant la FP pure et proposant un grand nombre de librairies basées sur ce principe
Lightbend
Akka
Play Framework
Slick
SBT
Lagom
Typelevel
Cats (fondation)
Shapeless
Http4s
Doobie
Circe
FS2
Monix
...
Fonctions pures : définition
Les fonctions pures sont :
- Totales : elles renvoient toujours un résultat
- Déterministes : elles renvoient toujours le même résultat pour les mêmes paramètres d’entrée
- Sans effet de bord : elles ne modifient rien qui leur est extérieur
Fonctions pures : avantages
- Prédictibles
- Transparence référentielle (RT), donc refactoring plus aisé
- Testables
RT (Wikipedia) :La transparence référentielle est une propriété des expressions d'un langage de programmation qui fait qu'une expression peut être remplacée par sa valeur sans changer le comportement du programme
Effets
Définition
Fonctions à effet de bord (wikipedia) :
Les fonctions qui modifient une variable ou ses arguments, qui lèvent une exception, qui écrivent des données vers un écran ou un fichier, qui lisent les données d'un clavier ou d'un fichier, les fonctions appelant d'autres fonctions à effet de bord
Plus simplement : l'absence de transparence référentielle.
Future
- Traitements asynchrones
- Non bloquant
- Non lazy (strict)
- Threads natifs
IO
- Encapsulation des effets
- Lazy
- Synchrones ou asynchrones
- Fibers (threads légers)
Future : exemples
def futureComputation(x: Int): Future[Int] = ???
//parallel computation
val f1 = futureComputation(1)
val f2 = futureComputation(2)
for {
v1 <- f1
v2 <- f2
} yield (v1 + v2)
//sequential computation
def f1 = futureComputation(1)
def f2 = futureComputation(2)
for {
v1 <- f1
v2 <- f2
} yield (v1 + v2)
Ok mais attention au refactoring!
IO : exemples
def futureComputation(x: Int): IO[Int] = ??? //async IO
val f1 = futureComputation(1)
val f2 = futureComputation(2)
for {
v1 <- f1
v2 <- f2
} yield (v1 + v2)
def f1 = futureComputation(1)
def f2 = futureComputation(2)
for {
v1 <- f1
v2 <- f2
} yield (v1 + v2)
for {
v1 <- futureComputation(1)
v2 <- futureComputation(2)
} yield (v1 + v2)
Ces 3 for comprehension sont équivalents.
Les traitements sont séquentiels.
IO est lazy et permet d'assurer la transparence référentielle (RT).
Exemple avec Cats Effect
IO : exemples
def futureComputation(x: Int): IO[Int] = ??? //async IO
val f1 = futureComputation(1)
val f2 = futureComputation(2)
// traitement parralèle
val program = (f1 , f2).parMapN {
(v1, v2) => v1 + v2
}
program.unsafeRunSync()
//ou
object Main extends IOApp {
//...
def run(args: List[String]): IO[ExitCode] = program.as(ExitCode.Success)
}
Avant le unsafeRunSync rien n'est exécuté, tout est lazy
On sait où les effets vont se produire et où les erreurs peuvent apparaître
Type classes
Polymorphisme ad hoc
Massivement utilisées dans Cats
case class Color(r: Int, g: Int, b: Int)
implicit val colorNumeric: Numeric[Color] = ??? //implement numeric methods for Color
List(Color(10,10,10), Color(200, 255, 10), Color(20, 55, 1)).sum
// Color(62,66,32)
// List scaladoc :
def sum[B >: A](implicit num: Numeric[B]): B
List(1,2,3).sum //6
Type classes et IO
import cats.effect.IO
import cats.implicits._
//traverse
val ioList: List[IO[Int]] = List(1, 2, 3).map(x => IO.pure(x+1))
val ioList2: IO[List[Int]] = List(1, 2, 3).map(x => IO.pure(x+1)).sequence
val ioList3: IO[List[Int]] = List(1, 2, 3).traverse(x => IO.pure(x+1))
//cartesian
// execute both, keep only dbCall result in IO
val dbResult: IO[DBResult] = IO(println("go!")) *> IO(dbCall(x))
// execute both, keep only println result in IO (IO[Unit])
val logMessageIO: IO[Unit] = IO(println("go!")) <* IO(dbCall(x))
Cats Effect bénéficie des type classes de Cats.
Streaming : exemples
case class Message(text: String, author: String)
object Message { implicit val messageReader = Json.reads[Message] }
object MixedStream extends App {
implicit val system = ActorSystem("MixedStream")
implicit val materializer = ActorMaterializer()
val wsClient = StandaloneAhcWSClient()
def queryToSource(keyword: String): Source[Message, NotUsed] = {
val request = wsClient
.url(s"http://localhost:3000?keyword=$keyword")
Source.fromFuture(request.stream()).flatMapConcat(_.bodyAsSource)
.via(Framing.delimiter(ByteString("\n"),
maximumFrameLength = 100, allowTruncation = true))
.map(byteString => Json.parse(byteString.utf8String).as[Message])
}
// can mix n streams
val keywordSources: Source[String, NotUsed] = Source(List("Akka", "FS2"))
val done: Future[Done] = keywordSources.flatMapMerge(10, queryToSource)
.runWith(Sink.foreach(println))
Await.result(done, Duration.Inf)
}
Akka Stream / Play
Streaming : exemples
case class Message(text: String, author: String)
object Message { implicit val messageReader = Json.reads[Message] }
object MixedStream extends App {
implicit val system = ActorSystem("MixedStream")
implicit val materializer = ActorMaterializer()
val wsClient = StandaloneAhcWSClient()
def queryToSource(keyword: String): Source[Message, NotUsed] = {
val request = wsClient
.url(s"http://localhost:3000?keyword=$keyword")
Source.fromFuture(request.stream()).flatMapConcat(_.bodyAsSource)
.via(Framing.delimiter(ByteString("\n"),
maximumFrameLength = 100, allowTruncation = true))
.map(byteString => Json.parse(byteString.utf8String).as[Message])
}
// can mix n streams
val keywordSources: Source[String, NotUsed] = Source(List("Akka", "FS2"))
val done: Future[Done] = keywordSources.flatMapMerge(10, queryToSource)
.runWith(Sink.foreach(println))
Await.result(done, Duration.Inf)
}
Akka Stream / Play
Streaming : exemples
case class Message(text: String, author: String)
object Message { implicit val messageReader = Json.reads[Message] }
object MixedStream extends App {
implicit val system = ActorSystem("MixedStream")
implicit val materializer = ActorMaterializer()
val wsClient = StandaloneAhcWSClient()
def queryToSource(keyword: String): Source[Message, NotUsed] = {
val request = wsClient
.url(s"http://localhost:3000?keyword=$keyword")
Source.fromFuture(request.stream()).flatMapConcat(_.bodyAsSource)
.via(Framing.delimiter(ByteString("\n"),
maximumFrameLength = 100, allowTruncation = true))
.map(byteString => Json.parse(byteString.utf8String).as[Message])
}
// can mix n streams
val keywordSources: Source[String, NotUsed] = Source(List("Akka", "FS2"))
val done: Future[Done] = keywordSources.flatMapMerge(10, queryToSource)
.runWith(Sink.foreach(println))
Await.result(done, Duration.Inf)
}
Akka Stream / Play
Streaming : exemples
case class Message(text: String, author: String)
object Message { implicit val messageReader = Json.reads[Message] }
object MixedStream extends App {
implicit val system = ActorSystem("MixedStream")
implicit val materializer = ActorMaterializer()
val wsClient = StandaloneAhcWSClient()
def queryToSource(keyword: String): Source[Message, NotUsed] = {
val request = wsClient
.url(s"http://localhost:3000?keyword=$keyword")
Source.fromFuture(request.stream()).flatMapConcat(_.bodyAsSource)
.via(Framing.delimiter(ByteString("\n"),
maximumFrameLength = 100, allowTruncation = true))
.map(byteString => Json.parse(byteString.utf8String).as[Message])
}
// can mix n streams
val keywordSources: Source[String, NotUsed] = Source(List("Akka", "FS2"))
val done: Future[Done] = keywordSources.flatMapMerge(10, queryToSource)
.runWith(Sink.foreach(println))
Await.result(done, Duration.Inf)
}
Akka Stream / Play
Streaming : exemples
case class Message(text: String, author: String)
object Message { implicit val messageReader = Json.reads[Message] }
object MixedStream extends App {
implicit val system = ActorSystem("MixedStream")
implicit val materializer = ActorMaterializer()
val wsClient = StandaloneAhcWSClient()
def queryToSource(keyword: String): Source[Message, NotUsed] = {
val request = wsClient
.url(s"http://localhost:3000?keyword=$keyword")
Source.fromFuture(request.stream()).flatMapConcat(_.bodyAsSource)
.via(Framing.delimiter(ByteString("\n"),
maximumFrameLength = 100, allowTruncation = true))
.map(byteString => Json.parse(byteString.utf8String).as[Message])
}
// can mix n streams
val keywordSources: Source[String, NotUsed] = Source(List("Akka", "FS2"))
val done: Future[Done] = keywordSources.flatMapMerge(10, queryToSource)
.runWith(Sink.foreach(println))
Await.result(done, Duration.Inf)
}
Akka Stream / Play
Streaming : exemples
object MixedStream extends App {
implicit val sttpBackend = AsyncHttpClientFs2Backend[cats.effect.IO]()
def queryToStream(keyword: String): IO[Stream[IO,Message]] = {
val responseIO: IO[Response[Stream[IO,ByteBuffer]]] =
sttp.post(uri"http://localhost:3000?keyword=$keyword").response(asStream[Stream[IO, ByteBuffer]])
.readTimeout(Duration.Inf).send()
responseIO.map { response =>
response.body match {
case Right(stream) => stream.flatMap { bytes =>
val s = new String(bytes.array(), "UTF-8")
Stream(s).through(stringStreamParser[IO]).through(decoder[IO, Message])
}
case Left(_) => Stream(Message( "http error", "app"))
}
}
}
val streamsIO: IO[List[Stream[IO,Message]]] = List("Akka", "FS2").traverse(queryToStream)
val mergedIO: IO[Stream[IO, Message]] = streamsIO.map(streams => streams.reduceLeft(_.merge(_)))
val printStreamIO: IO[Stream[IO, Unit]] = mergedIO.map(merged => merged.map(println))
val printIO: IO[Unit] = printStreamIO.flatMap(_.compile.drain)
printIO.unsafeRunSync()
}
FS2 / Circe
Streaming : exemples
object MixedStream extends App {
implicit val sttpBackend = AsyncHttpClientFs2Backend[cats.effect.IO]()
def queryToStream(keyword: String): IO[Stream[IO,Message]] = {
val responseIO: IO[Response[Stream[IO,ByteBuffer]]] =
sttp.post(uri"http://localhost:3000?keyword=$keyword").response(asStream[Stream[IO, ByteBuffer]])
.readTimeout(Duration.Inf).send()
responseIO.map { response =>
response.body match {
case Right(stream) => stream.flatMap { bytes =>
val s = new String(bytes.array(), "UTF-8")
Stream(s).through(stringStreamParser[IO]).through(decoder[IO, Message])
}
case Left(_) => Stream(Message( "http error", "app"))
}
}
}
val streamsIO: IO[List[Stream[IO,Message]]] = List("Akka", "FS2").traverse(queryToStream)
val mergedIO: IO[Stream[IO, Message]] = streamsIO.map(streams => streams.reduceLeft(_.merge(_)))
val printStreamIO: IO[Stream[IO, Unit]] = mergedIO.map(merged => merged.map(println))
val printIO: IO[Unit] = printStreamIO.flatMap(_.compile.drain)
printIO.unsafeRunSync()
}
FS2 / Circe
Streaming : exemples
object MixedStream extends App {
implicit val sttpBackend = AsyncHttpClientFs2Backend[cats.effect.IO]()
def queryToStream(keyword: String): IO[Stream[IO,Message]] = {
val responseIO: IO[Response[Stream[IO,ByteBuffer]]] =
sttp.post(uri"http://localhost:3000?keyword=$keyword").response(asStream[Stream[IO, ByteBuffer]])
.readTimeout(Duration.Inf).send()
responseIO.map { response =>
response.body match {
case Right(stream) => stream.flatMap { bytes =>
val s = new String(bytes.array(), "UTF-8")
Stream(s).through(stringStreamParser[IO]).through(decoder[IO, Message])
}
case Left(_) => Stream(Message( "http error", "app"))
}
}
}
val streamsIO: IO[List[Stream[IO,Message]]] = List("Akka", "FS2").traverse(queryToStream)
val mergedIO: IO[Stream[IO, Message]] = streamsIO.map(streams => streams.reduceLeft(_.merge(_)))
val printStreamIO: IO[Stream[IO, Unit]] = mergedIO.map(merged => merged.map(println))
val printIO: IO[Unit] = printStreamIO.flatMap(_.compile.drain)
printIO.unsafeRunSync()
}
FS2 / Circe
Streaming : exemples
object MixedStream extends App {
implicit val sttpBackend = AsyncHttpClientFs2Backend[cats.effect.IO]()
def queryToStream(keyword: String): IO[Stream[IO,Message]] = {
val responseIO: IO[Response[Stream[IO,ByteBuffer]]] =
sttp.post(uri"http://localhost:3000?keyword=$keyword").response(asStream[Stream[IO, ByteBuffer]])
.readTimeout(Duration.Inf).send()
responseIO.map { response =>
response.body match {
case Right(stream) => stream.flatMap { bytes =>
val s = new String(bytes.array(), "UTF-8")
Stream(s).through(stringStreamParser[IO]).through(decoder[IO, Message])
}
case Left(_) => Stream(Message( "http error", "app"))
}
}
}
val streamsIO: IO[List[Stream[IO,Message]]] = List("Akka", "FS2").traverse(queryToStream)
val mergedIO: IO[Stream[IO, Message]] = streamsIO.map(streams => streams.reduceLeft(_.merge(_)))
val printStreamIO: IO[Stream[IO, Unit]] = mergedIO.map(merged => merged.map(println))
val printIO: IO[Unit] = printStreamIO.flatMap(_.compile.drain)
printIO.unsafeRunSync()
}
FS2 / Circe
Streaming : exemples
object MixedStream extends App {
implicit val sttpBackend = AsyncHttpClientFs2Backend[cats.effect.IO]()
def queryToStream(keyword: String): IO[Stream[IO,Message]] = {
val responseIO: IO[Response[Stream[IO,ByteBuffer]]] =
sttp.post(uri"http://localhost:3000?keyword=$keyword").response(asStream[Stream[IO, ByteBuffer]])
.readTimeout(Duration.Inf).send()
responseIO.map { response =>
response.body match {
case Right(stream) => stream.flatMap { bytes =>
val s = new String(bytes.array(), "UTF-8")
Stream(s).through(stringStreamParser[IO]).through(decoder[IO, Message])
}
case Left(_) => Stream(Message( "http error", "app"))
}
}
}
val streamsIO: IO[List[Stream[IO,Message]]] = List("Akka", "FS2").traverse(queryToStream)
val mergedIO: IO[Stream[IO, Message]] = streamsIO.map(streams => streams.reduceLeft(_.merge(_)))
val printStreamIO: IO[Stream[IO, Unit]] = mergedIO.map(merged => merged.map(println))
val printIO: IO[Unit] = printStreamIO.flatMap(_.compile.drain)
printIO.unsafeRunSync()
}
FS2 / Circe
Slick
- Permet de manipuler les requêtes SQL comme des collections Scala
- Oblige à écrire soit même le mapping ou d'utiliser des annotations
- Interface basée sur Future
Doobie
- Tire parti de shapeless pour dériver les modèles (mapping) à la compilation
- Utilise les IO (ou équivalent)
Slick : exemple
case class Country(code: String, name: String, pop: Int, gnp: Option[Double])
class Countries(tag: Tag) extends Table[Country](tag, "country") {
def code = column[String]("code", O.PrimaryKey)
def name = column[String]("name")
def pop= column[Int]("population")
def gnp= column[Double]("gnp")
def * = (code , name, pop, gnp.?) <> (Country.tupled, Country.unapply)
}
val countries = TableQuery[Countries]
def populationIn(range: Range): Future[Seq[Country]] = {
val query = for {
c <- countries if (c.pop > range.min && c.pop < range.max)
} yield c
db.run(query.result)
}
Doobie: exemple
case class Country(code: String, name: String, pop: Int, gnp: Option[Double])
def populationIn(range: Range) =
sql"""
| select code, name, population, gnp
| from country
| where population > ${range.min}
| and population < ${range.max}
| """.query[Country]
val xa = Transactor.fromDriverManager[IO](
"org.postgresql.Driver", "jdbc:postgresql:world", "postgres", ""
)
sql"select name from country"
.query[String] // Query0[String]
.to[List] // ConnectionIO[List[String]]
.transact(xa) // IO[List[String]]
.unsafeRunSync // List[String]
.take(5) // List[String]
.foreach(println) // Unit
Lightbend
- Plus facile d'accès sans background FP
- Librairies plus matures (pour l'instant)
- Plus de features/plugins disponibles (modules Play, connecteurs Akka Streams, clustering ...)
- Plus d'exemples sur Internet
Typelevel
- Ouvre la porte à une nouvelle façon de penser
- La FP facilite l'évolutivité du code
- Refactoring plus sûr (RT)
- Bon support de la communauté
Conclusion :Points forts
Scala Lightbend <-> Typelevel
By loicd
Scala Lightbend <-> Typelevel
- 2,024