Architecture et programmation réactives avec Akka et Scala – Partie 2
Dans le premier blog post de la série “Architecture et programmation réactives avec Akka et Scala”, on avait premièrement rappelé le contexte global duquel a émané le paradigme de la réactivité, par la suite on a fait un rapide tour d’horizon des bibliothèques réactives ciblant la JVM
, et enfin introduit l’architecture commune de base.
Dans cette deuxième partie, on va entamer l’aspect pratique proprement dit. On introduira en premier la structure du projet exemple en Scala
, puis on fera la connaissance du système d’acteur et voir ce qu’il représente pour Akka Streams
, enfin on verra d’intéressantes notions d’architectures dans Akka Streams
à savoir la modularité et la composition.
Réactivité en pratique avec Akka Streams et Scala
Il faut d’une part savoir que Scala
est un langage qui fait partie de la famille JVM
. C’est-à-dire que son compilateur cible la JVM
en générant du “bytecode” Java
à exécuter par cette dernière. Cela lui confère la capacité d’intégrer nativement toutes les bibliothèques écrites en Java
: On parle alors d’interopérabilité de Scala
avec Java
.
D’autre part Akka
est l’une des bibliothèques phare du monde Scala
, bien qu’elle existe tout aussi pour Java
. Et comme évoqué dans le premier blog post, se basant sur le modèle du système d’acteur d’exécution concurrentielle (voir ci-bas “Le système d’acteur dans Akka”), Akka
évite de reproduire les problématiques liées au modèle de multi-threading bloquant de Java
.
Offrant une gamme de plusieurs sous-modules comme Akka Actors
, Akka HTTP
ou encore le module sophistiqué Akka Cluster
, on utilisera uniquement lors des manipulations qui suivront Akka Streams
; ce module étant une implémentation des paradigmes de la réactivités tels qu’énoncés dans “The reactive manifesto”
Dans les exemples qui suivront, on utilisera un mode de présentation “macro” où sera introduite une vue globale du projet avec son socle principal et sa structure Sbt
(outil de gestion des projets Scala
, équivalent de Maven
ou Gradle
pour les projets Java
).
Par la suite, au fur et à mesure de l’avancement, des explications sous la forme de notes informatives accompagneront, quand nécessaire, les points à éclaircir.
Place à présent au vif du sujet ! Voici le plan qu’on suivra :
- Structure et définition du projet
- Le système d’acteur dans
Akka
- Projet exemple
- L’instance d’exécution
- Architecture basique de
Akka Streams
- Modularité et composition
Structure et définition du projet
Pour des raisons d’homogénéité, Sbt
(Simple Build Tool) adopte une structure presque identique à celle d’un projet standard Java bien qu’il soit possible de faire autrement.
Dans notre exemple, on aura alors comme structure du projet :
On va maintenant modifier le contenu du fichier <span style="text-decoration: underline">build.sbt</span>
pour déclarer les propriétés du projet ainsi que ses dépendances externes. Rajoutons les lignes suivantes :
organization:="com.soat.techclub" name:="akka-streams" version:="1.0" scalaVersion:="2.12.8" libraryDependencies++=Seq( "com.typesafe.akka" %% "akka-stream" % "2.5.22", "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2", "ch.qos.logback" % "logback-classic" % "1.2.3") scalacOptions++=Seq("-feature","-language:postfixOps")
- Lignes 1,2,3définition de l’identité ou des coordonnées uniques du projet. Organisation à laquelle il appartient, son nom et sa version (même système d’identification que
Maven
)
- Ligne 4
la version deScala
à utiliser dans ce projet
- Lignes 6,7,8,9
déclaration des dépendances (bibliothèques) externes qui prend la forme d’une séquence ou liste :akka-stream
,scala-logging
etlogback-classic
pour le logging
- Ligne 11
quelques options pour le compilateurscalac
- Notes générales sur
Sbt
- La définition d’un projet
Sbt
se base sur un ensemble de clé/valeur où chaque clé est d’un typeScala
donné. - Par exemple, la clé
scalaVersion
est de typeSettingKey[String]
(une clé-propriétéSettingKey
encapsulant une valeurString
) à laquelle on affecte une valeur via l’opérateur:=
. - Un peu plus particulière, la clé
libraryDependencies
est de typeSettingKey[Seq[ModuleID]]
. Pour simplifier, disons que c’est une clé-propriété encapsulant une séquenceSeq
(ou liste pour certains) : on retiendra alors son type commeSettingKey[Seq[_]]
.
On remarquera cette fois que l’opérateur d’affectation devient++=
. Cela est dû au fait qu’on concatène une nouvelleSeq
à la clélibraryDependencies
dont le type estSeq
. - La déclaration d’une classe générique
Gen
avec le paramètre typeT
se fait (dansScala
en général) :class Gen[T] {...}
- Les dépendances sont déclarées sous la forme
groupId <strong>%%</strong> artifactId <strong>%</strong> version
étant donné qu’elles sont retrouvées par défaut depuis lerepository
central deMaven
.
À noter une légère différence entre les opérateurs%%
et%
. Le premier concatène tout simplement la version deScala
du projet au nom de l’artifactId
, ce qui nous donne concrètement pour notre exemple :
–"com.typesafe.akka" <strong>%%</strong> "akka-stream" <strong>%</strong> " 2.5.22"
est équivalent à
–"com.typesafe.akka" <strong>%</strong> "akka-stream<strong>_2.12</strong>" <strong>%</strong> "2.5.22"
- La définition d’un projet
Le système d’acteur dans Akka
Akka
a fait le choix de ne pas s’appuyer sur le modèle d’exécution concurrente bloquant natif à Java
, puisque ce dernier fait que les thread
s en cours d’exécution se suspendent mutuellement.
À petite échelle, le blocage pourrait ne pas entraver de façon significative l’exécution d’une application, mais à partir d’une certaine dimension la synchronisation risque de devenir trop complexe.
De la sorte, une partie non négligeable des efforts de conception et développement d’un projet seraient focalisés sur les problématiques d’interblocage (deadlock
), famine (starvation
) et autres. Surtout avec les outils natifs à Java
comme les blocs synchronized {…}
, les lock
ou les semaphore
qui ne garantissent pas un comportement déterministe.
Entre en jeu alors un modèle avec une approche différente, non bloquante, dite exécution asynchrone : il s’agit du modèle d’acteur.
Historiquement apparu au courant des années 1970s, le principe de base stipule qu’un groupe d’entité nommée acteur interagit ensemble dans le but d’assurer une exécution concurrente non bloquante, donc plus fluide et plus performante.
L’article suivant sur le blog de SOAT traite le sujet des acteurs avec plus de détails : Les systèmes réactifs et le pattern actor model
Le modèle d’acteur a un impact direct sur la conception à l’architecture qui devient désormais nettement plus facile à élaborer. En effet, la disparition de la synchronisation et de tous ses aspects négatifs a pour effet de dé-complexifier la réflexion autour de la construction des applications.
Pour résumer, si on avait à présenter le modèle acteur en quelques points, on dirait :
- un acteur est caractérisé par un état propre confiné et un comportement bien défini (analogue à la définition de l’objet dans le paradigme orienté objet)
- un acteur interagit avec d’autres acteurs à travers l’envoi de messages sans l’attente d’une réponse immédiate (d’où l’asynchronisme et le non-blocage)
- l’ensemble des acteurs coexistent dans un système d’acteur et sont structurés selon une hiérarchie père-fils.
Pour le cas d’Akka
, le système d’acteur est son moteur d’exécution et ce pour tous ses modules, y compris donc pour Akka Streams
.
Pour plus de détails sur le fonctionnement d’ Akka
avec le modèle d’acteur, se référer à cet article de blog de SOAT : Akka.Net – les fondamentaux
Projet exemple
Une fois la structure et la définition du projet établies, on peut passer désormais à l’étape de codage proprement dite.
Instance d’exécution
Découvrons alors le contenu du fichier source MainExecutor
localisé sous le package com.soat.techclub
.
MainExecutor.scala
package com.soat.techclub import com.typesafe.scalalogging.Logger object MainExecutor extends App { val logger = Logger(MainExecutor.getClass) logger.info("Ceci est un message de log ;)") }
- Ligne 5On déclare l’objet
MainExecutor
éponyme du fichier source avec le mot cléobject
pour exprimer le fait qu’il s’agisse d’une instance unique. Un peu l’équivalent d’un champ statique dans une classeJava
. Cela a du sens puisqu’on utilise une seule instance de cette entité pour lancer l’exécution.
L’objetMainExecutor
hérite d’untrait
, une sorte d’interface dansScala
, nomméApp
.
Cela permet d’exécuter les instructions se trouvant directement dans le corps délimité par les accolades{ … }
et d’éviter ainsi de déclarer la méthode principale exécutante comme dansJava
: la fameusepublic static void main(String[] args){ … }
.
- Ligne 7Les messages de log seront affichés grâce à l’objet
logger
qu’on a instancié à l’aide du constructeurLogger
de la dépendancescala-logging
. Dépendance rajoutée plus haut dans le fichierbuild.sbt
.
Un point à ne pas manquer aussi : on notera l’utilisation du mot cléval
uniquement, avec l’absence du type d’objet pour la déclaration dulogger
. Si on bénéficie d’une syntaxe légère, c’est que derrière les coulissesScala
, malgré les apparences, c’est un langage fortement typé qui se dote d’un compilateur intelligent sachant inférer le type d’une variable déclarée.
- Ligne 8
L’instruction qui affichera le message de log sur la sortie standard de la console.
Pour lancer l’instance exécutive MainExecutor
il suffit de se placer à la racine du répertoire du projet, là où se trouve le fichier build.sbt
, puis de lancer la commande sbt run
. On verra s’afficher sur la console alors :
[info] Done packaging. [info] Running com.soat.techclub.MainExecutor [run-main-0] INFO c.s.t.MainExecutor$ - Ceci est un message de log 😉 [success] Total time: 12 s, completed May 12, 2019 10:38:29 PM
La finalité de MainExectuor
est de servir de socle d’exécution pour notre projet-exemple. Ainsi, tout le code et toutes les instructions à lancer seront localisés dans le corps de cet objet.
Architecture basique de Akka Streams
Comme déjà évoqué dans la section Architecture commune du premier blog post, Akka Streams
implémente l’architecture de base dont la finalité est de définir le flux où transiteront les objets émis.
Voyons de plus près comment cela se traduit en code, rajoutons les instructions suivantes dans le corps de l’instance d’exécution MainExecutor.scala
:
implicit val actrSys = ActorSystem("Soat-ActorSystem") implicit val actMatrlzer = ActorMaterializer() val logger = Logger(MainExecutor.getClass) val origin = 1 to 25 val source: Source[Int,NotUsed] = Source(origin) val flow: Flow[Int,String,NotUsed] = Flow[Int].map(i => i.toString * 3) val sink: Sink[String,Future[Done]] = Sink.foreach[String]( str => logger.info(s"Triple i => $str")) val runnableGraph: RunnableGraph[NotUsed] = source.via(flow).to(sink) val runGraph: NotUsed = runnableGraph.run
- Lignes 1,2Ici on déclare et instancie le moteur d’exécution, un système d’acteur nommé
"Soat-ActorSystem"
référencé paractrSys
. Pareillement, on déclare et instancie dans un deuxième temps unActorMaterializer
référencé paractMatrlzer
.
Si on a définiActorSystem
comme le moteur d’exécution,ActorMaterializer
quant à lui possède un rôle de concrétisation ou de matérialisation. Il est responsable de l’instanciation et du lancement effectif d’un flux.
On notera lors de la déclaration des deux dernières valeurs (val
) l’utilisation d’un nouveau mot clé :implicit
.
- Lignes 6,7on déclare une suite d’entier
Int
de1
à25
nomméeorigin
(clin d’œil à l’élégance syntaxique) qu’on fournira comme argument au constructeurSource
. Ce constructeur retournera un objet, analogue auPublisher
(vu dans Architecture commune du premier blog post), qui sera la source d’émission du flux.
Le typeSource[Int,NotUsed]
déclaré explicitement (sans inférence de type) après source, indique que les objets émis seront de typeInt
.
Aussi lors de la matérialisation ou exécution effective, on aura un objet retour de typeNotUsed
. Autrement dit, on va tout court ignorer l’objet retourné au moment de la matérialisation.
- Ligne 9De la même manière, on déclare et instancie un objet
flow
analogue àOperator
(voir Architecture commune du premier blog post) dont le type explicite estFlow[Int,String,NotUsed]
.
Cela veut dire que cet étage du flux recevra des objets de typeInt
et émettra des objets de typeString
. On introduit ainsi une transformation exprimée grâce à la méthodemap
prenant une expressionlambda
comme argument.
Ici l’expression lambda transforme chaqueInt
en sa valeurString
répétée 3 fois (encore un clin d’œil à l’élégance de la syntaxe).
- Ligne 10Idem, déclaration et instanciation d’un objet
sink
de typeSink[String,Future[Done]]
(analogue àSubsriber
dans Architecture commune du premier blog post) indiquant que les objets reçus seront de typeString
et que lors de la matérialisation il y aura retour d’un objet de typeFuture[Done]
.
Cet objet retourné n’est pas disponible au moment de l’exécution, il le sera dans le futur. On pourra de la sorte à un moment ultérieur s’assurer que l’exécution du flux s’est terminée.
Ce que lesink
fera avec les objets reçus est décrit encore avec une expressionlambda
passée en argument à la méthodeforeach
: Maniant lelogger
, on affiche simplement un message avec la valeur duString
reçu en amont.
On notera le formatage de la chaîne de caractère préfixée avecs
et contenant la valeur de la variable à interpréter$str
.
- Ligne 12On connecte les étages source,
flow
etsink
pour former un flux linéaire à travers la méthodevia
pour raccorder les typesFlow
et la méthodeto
pour raccorder les typesSink
.
L’objet de retourrunnableGraph
est de typeRunnableGraph[NotUsed]
, c’est-à-dire graphe (linéaire dans notre cas) à exécuter retournant un objetNotUsed
au moment de la matérialisation.
L’objetNotUsed
provient par défaut de l’étage le plus à gauche : c’est-à-dire depuissource
pour cet exemple. Il est bien évidement possible de changer ce comportement afin de retourner celui d’un autre étage :flow
ousink
.
- Ligne 13On exécute le graphe composé précédemment et on obtient l’objet retourné suite à la matérialisation.
Lançons le code précédent avec la commande sbt run
et voyons le résultat de l’exécution :
[info] Done packaging. [info] Running com.soat.techclub.MainExecutor [akka.actor.default-dispatcher-2] INFO c.s.t.MainExecutor$ - Tripled => 111 [akka.actor.default-dispatcher-2] INFO c.s.t.MainExecutor$ - Tripled => 222 [akka.actor.default-dispatcher-2] INFO c.s.t.MainExecutor$ - Tripled => 333 [akka.actor.default-dispatcher-2] INFO c.s.t.MainExecutor$ - Tripled => 444 ...
On voit bien comment chaque entier est dupliqué 3 fois puis affiché dans le message de log.
Dans cet exemple, il existe certes des détails d’implémentation spécifiques à Akka Streams
comme la terminologie Source
, Flow
, Sink
et la méthode de construction du flux. Ou bien aussi des éléments syntaxiques et sémantiques propres au langage Scala
.
Mais en coulisses, c’est toujours la même architecture définie précédemment qui régit le fonctionnement du système réactif.
L’empreinte du modèle d’un flux ressemblant à une chaîne de montage industrielle reste fortement ressentie.
- Notes générales sur
Scala
- Les mots clés
val
etvar
sont utilisés respectivement pour la déclaration des constantes et des variables.
Par exemple, la syntaxe de déclaration d’une variable nomméexyz
de typeT
est<strong>var</strong> xyz : T = ...
- Placé devant une
val
/var
, une méthode ou une classe, le mot cléimplicit
rendra disponible cetteval
/var
, méthode ou classe discrètement ou encore implicitement dans le contexte ou scope actuel.
Concrètement, prenons le cas d’une méthode qui dans sa signature déclare un paramètre comme étantimplicit
. Il faudra lors de l’invocation soit lui passer explicitement un argument soit le déclarerimplicit
dans le scope en cours sans le lui passer.
Dans l’exemple précédent, le constructeurActorMaterializer()
a besoin d’un objetActorSystem
implicite dans le contexte pour créer son instance. C’est pour cela que la constanteval actrSys
a été marquée commeimplicit
.
- Les mots clés
Modularité et composition
La section précédente a introduit les 3 composants fondamentaux de Akka Streams
à savoir Source
, Flow
et Sink
qui lorsque connectés forment le flux ou graphe linéaire de base RunnableGraph
. Mais qu’arriverait-il si on connectait ces étages que partiellement ? Que pourrait-on bien avoir ?
Faisons l’expérience avec le snippet suivant :
val source = Source (1 to 10) val flow = Flow[Int].map( i => i.toString ) val stage = source.via(flow) logger.info(s"Class is >>> ${stage.getClass}")
On instancie un étage Source
qui émet une séquence de Int
de 1
à 10
, puis un étage Flow
qui transforme les Int
en String
. Par la suite on connecte les deux étages source
et flow
grâce à la méthode via et on récupère le résultat dans la constante stage.
Affichons la classe de stage
avec le formatage de la chaîne de caractère préfixée par s
.
- Notes générales sur
Scala
- Pour formater une chaîne de caractère dans
Scala
il faut la préfixer avecs
.
L’interprétation du contenu se fait avec le symbole$
pour une valeur simple et avec${ … }
pour une expression.
Cela s’appelle String interpolation.
- Pour formater une chaîne de caractère dans
Nous obtenons à la console :
[run-main-2] INFO c.m.l.MainExecutor$ - Class is >>> class akka.stream.scaladsl.Source
La classe obtenue est de type Source
.
On conclut donc qu’un objet Source[T,_]
raccordé à un objet Flow[T,G,_]
donne lieu à un objet de type Source[G,_]
(l’emploi du _
signifie qu’on ignore le type de la matérialisation).
Et quand on enchaîne avec le même raisonnement, si on reprend le dernier objet de type Source[G,_]
et qu’on le raccorde à un autre objet de type Flow[G,P,_]
cela donne lieu à un objet Source[P,_]
.
La conclusion finale est que de façon globale, si on raccorde Source[T0,_]
avec Flow[T0,T1,_]
puis Flow[T1,T2,_]
jusqu’à Flow[Tn-1,Tn,_]
on obtient un objet dont le type est Source[Tn,_]
.
Traduisons cela en code :
val source = Source (1 to 10) val flow0 = Flow[Int].map( i => i.toFloat ) val flow1 = Flow[Float].map(f => f.toString) val flow2 = Flow[String].map(str => str.length < 4 ) val stage: Source[Boolean,_] = source.via(flow0).via(flow1).via(flow2)
- Ligne 1
une source qui génère desInt
de1
à10
- Ligne 3
flow0
reçoit des objetsInt
et retourne desFloat
- Ligne 4
flow1
reçoit des objetsFloat
et retourne desString
- Ligne 5
flow2
reçoit des objetsString
et retourne desBoolean
- Ligne 7
On enchaîne les raccordements avec la méthodevia
pour former uneSource[Boolean,_]
.
À noter que si on change le type paramètre destage
à autre chose queBoolean
le compilateur marquera le type déclaré en erreur. Cela est dû au fait que le dernier étageflow2
émet unBoolean
donc la source résultante doit absolument retourner un objetSource[Boolean,_]
.
En appliquant symétriquement le même raisonnement aux objets de types Flow
et Sink
on arrivera aux conclusions suivantes :
- Le raccord de
Flow[T0,T1,_]
puisFlow[T1,T2,_]
jusqu’àFlow[Tn-1,Tn,_]
donnera lieu à un objet de typeFlow[T0,Tn,_]
. - Le raccord de
Flow[T0,T1,_]
puisFlow[T1,T2,_]
jusqu’àFlow[Tn-1,Tn,_]
avec en dernier unSink[Tn,_]
donnera lieu à un objet de typeSink[T0,_]
.
Construisons un exemple plus illustratif et plus concret qui aidera à mieux assimiler tous ces symboles.
Prenons le bout de code suivant :
def squareSource(intSeq:Seq[Int]) : Source[Int,_] = { Source[Int](intSeq) .via( Flow[Int].map(i => i * i) ) } def formatSink(msg:String) : Sink[Int,_] = { Flow[Int] .map(i => msg.format(i)) .via( Flow[String].map(str => str concat " !!")) .to( Sink.foreach[String](str => logger.info(str))) } squareSource(3 to 9).to(formatSink("Squared = %s")).run
- Ligne 1
On déclare une fonctionsquareSource
qui prend en paramètre uneSeq
de type Int (liste ou séquence d’entier) et qui retourne comme résultat uneSource
émettant desInt
.
- Lignes 3,4
À l’intérieur on crée un objetSource
à partir de la liste passée en paramètre qu’on raccorde avecvia
à unFlow
nouvellement créé. Cet objetFlow
va émettre la valeur du carré de l’entier reçu comme le décrit l’expressionlambda</codecode class="language-plaintext">i ⇒ i * i
.
Cette composition forme l’objet à retourner par la fonction.
- Ligne 7
On déclare une fonctionformatSink
acceptant un paramètre de typeString
et retournant unSink
ayant en réception un typeInt
. Le paramètre contiendra un message à formater avec les objetsInt
reçus.
- Lignes 9,10,11,12,13,14
on instancie et raccorde deux objets de typeFlow
. Le premier reçoit desInt
dont il utilisera la valeur pour formater le paramètremsg
et le deuxième concatènera à la chaîne formatée la chaîne!!
. Un dernier objetSink
vient se raccorder avecto
aux étages précédents pour afficher avec lelogger
le résultat obtenu.
La composition de ces 3 étages forme l’objetSink[Int]
à retourner par la méthode.
- Ligne 17
En une seule étape, on appelle les fonctions déclarées précédemment qui retourneront les étages construits puis on les raccorde en utilisantto
uniquement (on dispose que d’unSource
et unSink
) et enfin on exécute le graphe linéaire avecrun
.
En lançant notre petit programme toujours avec la commande sbt run
on obtient :
[info] Running com.soat.techclub.MainExecutor ... [akka.actor.default-dispatcher-4] INFO c.m.l.MainExecutor$ - Squared = 36 !! [akka.actor.default-dispatcher-4] INFO c.m.l.MainExecutor$ - Squared = 49 !! [akka.actor.default-dispatcher-4] INFO c.m.l.MainExecutor$ - Squared = 64 !! [akka.actor.default-dispatcher-4] INFO c.m.l.MainExecutor$ - Squared = 81 !!
Le résultat obtenu ne diffère pas de ce qu’on aurait eu s’il y avait déclaration et raccord des étages unité par unité (sans recours au regroupement par modules).
L’important avantage apporté par cette technique est qu’il est désormais possible de regrouper plusieurs traitements répartis sur un nombre illimité de sous étages puis d’exposer l’étage résultant sous la forme d’une entité Source
, Flow
ou Sink
.
Il suffira par la suite de raccorder le tout avec les bons appels méthodes (via
et to
) sans exposer le comportement intrinsèque de l’étage composé.
De plus, grâce au Materializer
un étage composé ne sera réellement concrétisé que lorsqu’on appelle effectivement la méthode run
.
Donc un étage, simple ou composé, représente uniquement une sorte de plan descriptif du mode de fonctionnement et non pas le fonctionnement en soi.
C’est-à-dire qu’à chaque fois qu’on effectue une matérialisation concrète, on obtient une exécution nouvelle non liée aux autres exécutions émanant du même étage.
En conséquence de tout cela, on pourra exporter les étages composés vers d’autres utilisateurs de façon complètement transparente les rendant de la sorte portables et partageables.
Récapitulons l’essentiel de la composition avec l’illustration suivante :
- Notes générales sur
Scala
- La syntaxe de déclaration d’une méthode ou fonction nommée
fooFunc
acceptant une liste de paramètre a de type A, b de type B, c de type C …etc et retournant un résultat de type R est comme suit :<strong>def</strong> fooFunc <strong>(</strong>a<strong>:</strong>A<strong>,</strong> b<strong>:</strong>B<strong>,</strong> c<strong>:</strong>C<strong>)</strongstrong>:</strong> R <strong>=</strongstrong>{</strong> ... <strong>}</strong>
Il est possible d’omettre l’expression du type de retour: R
étant donné que le compilateurScala
dispose aussi pour les méthodes et fonctions d’un mécanisme d’inférence de type. - Dans le corps de la méthode/fonction il est possible d’omettre le mot clé
return
car le compilateurScala
retourne par défaut le résultat de la dernière expression.
C’est pour cette raison que lors du dernier code snippet on n’a pas utilisé d’expression avecreturn
puisqu’on retourne directement l’objet construit. - Lorsque la méthode/fonction déclarée ne contient qu’une seule expression on peut omettre les accolades
{
et}
délimitant son corps.
Appliquées à la fonctionsquareSource
de l’exemple précédent, ces règles conduisent à cette réécriture de la fonction (sans type de retour, sans accolades) :<strong>def</strong> squareSource<strong>(</strong>intSeq : Seq[Int]) <strong>=</strong> Source[Int](intSeq).via(Flow[Int].map(i => i * i))
- La syntaxe de déclaration d’une méthode ou fonction nommée
Récapitulatif partie 2
Voilà ! 😃
On arrive à la fin de ce deuxième blog post de la série dans lequel on avait premièrement défini la structure de notre projet, puis présenté le modèle système d’acteur et enfin exploré l’architecture de base de Akka Streams
et sa propriété de modularité et composition.
Dans la troisième et dernière partie de notre série, on va se rapprocher du concret avec un cas d’utilisation plus tangible : une manipulation sur du contenu d’un fichier.
© SOAT
Toute reproduction interdite sans autorisation de la société SOAT