Outils de résolution des dépendances Sémantique

Outil de résolution des dépendances


Un résolveur de dépendances (ci-après dénommé résolveur, environ Transl.) Ou un gestionnaire de packages est un programme qui définit un ensemble cohérent de modules en tenant compte des restrictions définies par l'utilisateur.


Les contraintes sont généralement spécifiées par les noms de module et les numéros de version. Dans l'écosystème JVM des modules Maven, le nom de l'organisation (identifiant de groupe) sera également indiqué. De plus, les restrictions peuvent inclure des plages de versions, des modules exclus, des remplacements de version, etc.


Les trois principales catégories de packages sont représentées par les packages OS (Homebrew, packages Debian, etc.),
modules pour des langages de programmation spécifiques (CPAN, RubyGem, Maven, etc.) et des extensions spécifiques à l'application (plugins Eclipse, plugins IntelliJ, extensions VS Code).


Sémantique du résolveur


En première approximation, nous pouvons représenter les dépendances des modules sous forme de DAG (graphe acyclique dirigé, graphe acyclique dirigé).


Cette représentation est appelée graphe de dépendance. Considérez les dépendances de deux modules:


  • a:1.0 dépend de c:1.0
  • b:1.0 dépend de c:1.0 et d:1.0

 +-----+ +-----+ |a:1.0| |b:1.0| +--+--+ +--+--+ | | +<-------+ | | vv +--+--+ +--+--+ |c:1.0| |d:1.0| +-----+ +-----+ 

Si le module dépend de a:1.0 et b:1.0 , alors une liste complète des dépendances sera présentée a:1.0 , b:1.0 , c:1.0 et d:1.0 . Et ce n'est qu'un tour d'arbre.


La situation deviendra plus compliquée si les dépendances transitives sont spécifiées par une gamme de versions:


  • a:1.0 dépend de c:1.0
  • b:1.0 dépend de c:[1.0,2) et d:1.0

 +-----+ +-----+ |a:1.0| |b:1.0| +--+--+ +--+--+ | | | +-----------+ | | | vvv +--+--+ +--+------+ +--+--+ |c:1.0| |c:[1.0,2)| |d:1.0| +-----+ +---------+ +-----+ 

Ou, si différentes versions sont spécifiées pour les dépendances transitives:


  • a:1.0 dépend de c:1.0
  • b:1.0 dépend de c:1.2 et d:1.2

Ou, si des exceptions sont levées pour la dépendance:


  • dépendance à a:1.0 , qui dépend de c:1.0 , à l'exclusion de c:*
  • b:1.0 dépend de c:1.2 et d:1.2

Différents résolveurs interprètent différemment les restrictions définies par les utilisateurs. J'appelle de telles règles la sémantique des résolveurs.


Vous devrez peut-être connaître certaines de ces sémantiques, par exemple:


  • la sémantique de votre propre module (déterminée par l'outil de construction que vous utilisez);
  • la sémantique des bibliothèques que vous utilisez (déterminée par l'outil de construction que l'auteur a utilisé);
  • la sémantique des modules que votre module utilisera comme dépendance (définie par l'outil de construction de l'utilisateur final).

Outils de résolution des dépendances dans l'écosystème JVM


Puisque je supporte sbt , je dois travailler principalement dans l'écosystème JVM.


Semantics Maven: victoires les plus proches


Dans les graphiques où il y a un conflit de dépendances (dans le graphique des dépendances a il existe de nombreuses versions différentes du composant d , par exemple d:1.0 et d:2.0 ), Maven utilise la stratégie de gain le plus proche pour résoudre le conflit.


La résolution des conflits de dépendance est un processus qui détermine quelle version d'un artefact sera sélectionnée si plusieurs versions différentes du même artefact sont trouvées parmi les dépendances. Maven sélectionne la définition la plus proche. C'est-à-dire utilise la version la plus proche de votre projet dans l'arborescence des dépendances.
Vous pouvez toujours garantir l'utilisation de la bonne version en la déclarant explicitement dans le POM du projet. Notez que si deux versions de la dépendance ont la même profondeur dans l'arborescence, la première sera sélectionnée. La définition la plus proche signifie que la version la plus proche du projet dans l'arborescence des dépendances sera utilisée. Par exemple, si les dépendances pour A , B et C définies comme A -> B -> C -> D 2.0 et A -> E -> D 1.0 , alors, lors de la construction de A , D 1.0 sera utilisé, car le trajet de A à D via E plus court (que via B et C , env. transl.).

Cela signifie que de nombreux modules Java publiés à l'aide de Maven ont été compilés à l'aide de la sémantique la nearest-wins . Pour illustrer ce qui précède, créez un simple pom.xml :


 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>foo</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <dependencyManagement> <dependencies> <dependency> <groupId>com.typesafe.play</groupId> <artifactId>play-ws-standalone_2.12</artifactId> <version>1.0.1</version> </dependency> </dependencies> </dependencyManagement> </project> 

mvn dependency:build-classpath renvoie le mvn dependency:build-classpath résolu.
Il est à noter que l'arborescence résultante utilise com.typesafe:config:1.2.0 même si Akka 2.5.3 dépend transitoirement de com.typesafe:config:1.3.1 .


mvn dependency:tree donne cette confirmation visuelle:


 [INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ foo --- [INFO] com.example:foo:jar:1.0.0 [INFO] \- com.typesafe.play:play-ws-standalone_2.12:jar:1.0.1:compile [INFO] +- org.scala-lang:scala-library:jar:2.12.2:compile [INFO] +- javax.inject:javax.inject:jar:1:compile [INFO] +- com.typesafe:ssl-config-core_2.12:jar:0.2.2:compile [INFO] | +- com.typesafe:config:jar:1.2.0:compile [INFO] | \- org.scala-lang.modules:scala-parser-combinators_2.12:jar:1.0.4:compile [INFO] \- com.typesafe.akka:akka-stream_2.12:jar:2.5.3:compile [INFO] +- com.typesafe.akka:akka-actor_2.12:jar:2.5.3:compile [INFO] | \- org.scala-lang.modules:scala-java8-compat_2.12:jar:0.8.0:compile [INFO] \- org.reactivestreams:reactive-streams:jar:1.0.0:compile 

De nombreuses bibliothèques offrent une compatibilité descendante, mais la compatibilité directe n'est pas garantie à quelques exceptions près, ce qui est alarmant.


Sémantique d'Apache Ivy: dernières victoires


Par défaut, Apache Ivy utilise la stratégie des dernières victoires pour résoudre les conflits de dépendance.


Si ce conteneur n'est pas présent, le gestionnaire de conflits par défaut est utilisé pour tous les modules. Le gestionnaire de conflits par défaut actuel est "dernière révision".
Remarque: Les conflicts conteneurs sont l'un des fichiers Ivy.

Jusqu'à la version SBT 1.3.x résolveur de dépendance interne est Apache Ivy. Le pom.xml utilisé précédemment est décrit un peu plus brièvement dans SBT:


 ThisBuild / scalaVersion := "2.12.8" ThisBuild / organization := "com.example" ThisBuild / version := "1.0.0-SNAPSHOT" lazy val root = (project in file(".")) .settings( name := "foo", libraryDependencies += "com.typesafe.play" %% "play-ws-standalone" % "1.0.1", ) 

Dans le sbt shell entrez show externalDependencyClasspath pour obtenir le chemin de classe résolu. Il doit indiquer la version de com.typesafe:config:1.3.1 . De plus, l'avertissement suivant sera toujours affiché:


 [warn] There may be incompatibilities among your library dependencies; run 'evicted' to see detailed eviction warnings. 

L'appel de la evicted dans le sbt shell vous permet d'obtenir un rapport de résolution de conflit:


 sbt:foo> evicted [info] Updating ... [info] Done updating. [info] Here are other dependency conflicts that were resolved: [info] * com.typesafe:config:1.3.1 is selected over 1.2.0 [info] +- com.typesafe.akka:akka-actor_2.12:2.5.3 (depends on 1.3.1) [info] +- com.typesafe:ssl-config-core_2.12:0.2.2 (depends on 1.2.0) [info] * com.typesafe:ssl-config-core_2.12:0.2.2 is selected over 0.2.1 [info] +- com.typesafe.play:play-ws-standalone_2.12:1.0.1 (depends on 0.2.2) [info] +- com.typesafe.akka:akka-stream_2.12:2.5.3 (depends on 0.2.1) 

Dans la sémantique des latest-wins , spécifier config:1.2.0 dans la pratique signifie «me fournir la version 1.2.0 ou supérieure».
Ce comportement est légèrement plus préférable que dans la stratégie de nearest-wins les nearest-wins , car les versions des bibliothèques transitives ne sont pas rétrogradées. Cependant, l'appel evicted doit vérifier si les remplacements ont été effectués correctement.


Cours de sémantique: dernières victoires


Avant d'aborder la description de la sémantique, je répondrai à une question importante - comment se prononce Coursier. Selon la note d' Alex Arshambo , il s'agit d'un poussin prononcé.


Fait intéressant, la documentation de Coursier contient une page sur la gestion des versions , qui parle de la sémantique de la résolution des dépendances.


Considérez l'intersection d'intervalles donnés:
  • S'il est vide (les intervalles ne se croisent pas), alors il y a un conflit.
  • Si aucun intervalle n'est spécifié, il est supposé que l'intersection est représentée par (,) (l'intervalle correspondant à toutes les versions).
    Ensuite, envisagez des versions spécifiques:
    • Nous rejetons les versions spécifiques sous les limites de l'intervalle.
    • S'il existe des versions spécifiques au-dessus des limites de l'intervalle, il y a un conflit.
    • Si des versions spécifiques se trouvent dans les limites de l'intervalle, le résultat doit être le plus récent d'entre elles.
    • S'il n'y a pas de versions spécifiques dans ou au-dessus des limites de l'intervalle, le résultat doit être l'intervalle.

Parce que on dit , donc - c'est la sémantique des latest-wins .
Vous pouvez le vérifier en utilisant sbt 1.3.0-RC3 , qui utilise Coursier.


 ThisBuild / scalaVersion := "2.12.8" ThisBuild / organization := "com.example" ThisBuild / version := "1.0.0-SNAPSHOT" lazy val root = (project in file(".")) .settings( name := "foo", libraryDependencies += "com.typesafe.play" %% "play-ws-standalone" % "1.0.1", ) 

L'appel de show externalDependencyClasspath partir de la console sbt 1.3.0-RC3 renverra com.typesafe:config:1.3.1 , comme prévu. Le rapport sur la résolution des conflits rapporte la même chose:


 sbt:foo> evicted [info] Here are other dependency conflicts that were resolved: [info] * com.typesafe:config:1.3.1 is selected over 1.2.0 [info] +- com.typesafe.akka:akka-actor_2.12:2.5.3 (depends on 1.3.1) [info] +- com.typesafe:ssl-config-core_2.12:0.2.2 (depends on 1.2.0) [info] * com.typesafe:ssl-config-core_2.12:0.2.2 is selected over 0.2.1 [info] +- com.typesafe.play:play-ws-standalone_2.12:1.0.1 (depends on 0.2.2) [info] +- com.typesafe.akka:akka-stream_2.12:2.5.3 (depends on 0.2.1) 

Remarque: Apache Ivy émule la sémantique des nearest-wins ?


Lors de la résolution des dépendances de module à partir du référentiel Maven, Ivy convertit le fichier POM et place l'attribut force="true" dans ivy.xml dans le cache.


Par exemple, cat ~/.ivy2/cache/com.typesafe.akka/akka-actor_2.12/ivy-2.5.3.xml :


  <dependencies> <dependency org="org.scala-lang" name="scala-library" rev="2.12.2" force="true" conf="compile->compile(*),master(compile);runtime->runtime(*)"/> <dependency org="com.typesafe" name="config" rev="1.3.1" force="true" conf="compile->compile(*),master(compile);runtime->runtime(*)"/> <dependency org="org.scala-lang.modules" name="scala-java8-compat_2.12" rev="0.8.0" force="true" conf="compile->compile(*),master(compile);runtime->runtime(*)"/> </dependencies> 

La documentation Ivy dit:


Ces deux latest gestionnaires de conflits prennent en compte l'attribut de dépendance de force .
Ainsi, les dépendances directes peuvent déclarer un attribut force (voir dépendance), indiquant que de la dépendance directe et des révisions indirectes, la préférence devrait être donnée aux révisions de dépendance directes.

Pour moi, cette formulation signifie que force="true" conçu afin de redéfinir la logique des latest-wins les latest-wins et d'émuler la sémantique des nearest-wins les nearest-wins . Mais, heureusement, cela n'était pas destiné à se produire, et nous avons maintenant les latest-wins : comme nous pouvons le voir, sbt 1.2.8 reprend com.typesafe:config:1.3.1 .


Cependant, on peut observer l'effet de force="true" lors de l'utilisation d'un gestionnaire de conflit strict, qui semble rompu.


 ThisBuild / conflictManager := ConflictManager.strict 

Le problème est qu'un gestionnaire de conflits strict ne semble pas empêcher la substitution de version. show externalDependencyClasspath retourne joyeusement com.typesafe:config:1.3.1 .
Un problème connexe est que l'ajout d'une version de com.typesafe:config:1.3.1 , qu'un gestionnaire de conflits strict a mis dans le graphique, conduit à une erreur.


 ThisBuild / scalaVersion := "2.12.8" ThisBuild / organization := "com.example" ThisBuild / version := "1.0.0-SNAPSHOT" ThisBuild / conflictManager := ConflictManager.strict lazy val root = (project in file(".")) .settings( name := "foo", libraryDependencies ++= List( "com.typesafe.play" %% "play-ws-standalone" % "1.0.1", "com.typesafe" % "config" % "1.3.1", ) ) 

Cela ressemble à ceci:


 sbt:foo> show externalDependencyClasspath [info] Updating ... [error] com.typesafe#config;1.2.0 (needed by [com.typesafe#ssl-config-core_2.12;0.2.2]) conflicts with com.typesafe#config;1.3.1 (needed by [com.example#foo_2.12;1.0.0-SNAPSHOT]) [error] org.apache.ivy.plugins.conflict.StrictConflictException: com.typesafe#config;1.2.0 (needed by [com.typesafe#ssl-config-core_2.12;0.2.2]) conflicts with com.typesafe#config;1.3.1 (needed by [com.example#foo_2.12;1.0.0-SNAPSHOT]) 

À propos du versioning


Nous avons mentionné la sémantique des latest-wins , suggérant que les versions dans une représentation sous forme de chaîne peuvent apparaître dans un certain ordre.
Par conséquent, la gestion des versions fait partie de la sémantique.


Procédure de versioning dans Apache Ivy


Ce commentaire Javadoc dit que lors de la création du comparateur de versions, Ivy s'est concentré sur la fonction de comparaison des versions de PHP :


Cette fonction remplace d'abord _, - et + par un point . dans les représentations de chaînes de versions et ajoute également . avant et après tout ce qui n'est pas un nombre. Ainsi, par exemple, «4.3.2RC1» devient «4.3.2.RC.1». Elle compare ensuite les pièces reçues de gauche à droite.

Pour les pièces contenant des éléments spéciaux ( dev , alpha ou a , beta ou b , RC ou rc , # , pl ou p ) *, les éléments sont comparés dans l'ordre suivant:

toute chaîne qui n'est pas un élément spécial <dev <alpha = a <beta = b <RC = rc <# <pl = p.

Ainsi, non seulement différents niveaux (par exemple, «4.1» et «4.1.2») peuvent être comparés, mais des versions spécifiques à PHP contenant des informations sur l'état du développement.
* env. perev.

Nous pouvons vérifier comment les versions sont ordonnées en écrivant une petite fonction.


 scala> :paste // Entering paste mode (ctrl-D to finish) val strategy = new org.apache.ivy.plugins.latest.LatestRevisionStrategy case class MockArtifactInfo(version: String) extends org.apache.ivy.plugins.latest.ArtifactInfo { def getRevision: String = version def getLastModified: Long = -1 } def sortVersionsIvy(versions: String*): List[String] = { import scala.collection.JavaConverters._ strategy.sort(versions.toArray map MockArtifactInfo) .asScala.toList map { case MockArtifactInfo(v) => v } } // Exiting paste mode, now interpreting. scala> sortVersionsIvy("1.0", "2.0", "1.0-alpha", "1.0+alpha", "1.0-X1", "1.0a", "2.0.2") res7: List[String] = List(1.0-X1, 1.0a, 1.0-alpha, 1.0+alpha, 1.0, 2.0, 2.0.2) 

Procédure de versioning dans Coursier


La page GitHub sur la sémantique de résolution des dépendances a une section sur le versioning.


Coursier utilise l'ordre de versioning personnalisé de Maven. Avant de comparer, les représentations de chaînes des versions sont décomposées en éléments séparés ...
Pour obtenir de tels éléments, les versions sont séparées par les caractères., - et _ (et les séparateurs eux-mêmes sont supprimés) et par des remplacements de lettre à chiffre ou de numéro à lettre.

Pour écrire un test, créez un sous-projet avec les libraryDependencies += "io.get-coursier" %% "coursier-core" % "2.0.0-RC2-6" et exécutez la console :


 sbt:foo> helper/console [info] Starting scala interpreter... Welcome to Scala 2.12.8 (OpenJDK 64-Bit Server VM, Java 1.8.0_212). Type in expressions for evaluation. Or try :help. scala> import coursier.core.Version import coursier.core.Version scala> def sortVersionsCoursier(versions: String*): List[String] = | versions.toList.map(Version.apply).sorted.map(_.repr) sortVersionsCoursier: (versions: String*)List[String] scala> sortVersionsCoursier("1.0", "2.0", "1.0-alpha", "1.0+alpha", "1.0-X1", "1.0a", "2.0.2") res0: List[String] = List(1.0-alpha, 1.0, 1.0-X1, 1.0+alpha, 1.0a, 2.0, 2.0.2) 

Il s'avère que Coursier commande les numéros de version dans un ordre complètement différent de celui de Ivy.
Si vous avez utilisé des balises alphabétiques permissives, cet ordre peut entraîner une certaine confusion.


À propos des plages de versions


Habituellement, j'évite d'utiliser des plages de versions, bien qu'elles soient largement utilisées dans les webjars et les modules npm republiés sur Maven Central. Quelque chose comme "is-number": "^4.0.0" peut être écrit dans le module "is-number": "^4.0.0" qui correspondra à [4.0.0,5) .


Gestion des plages de versions dans Apache Ivy


Dans cet assemblage, angular-boostrap:0.14.2 dépend de angular:[1.3.0,) .


 ThisBuild / scalaVersion := "2.12.8" ThisBuild / organization := "com.example" ThisBuild / version := "1.0.0-SNAPSHOT" lazy val root = (project in file(".")) .settings( name := "foo", libraryDependencies ++= List( "org.webjars.bower" % "angular" % "1.4.7", "org.webjars.bower" % "angular-bootstrap" % "0.14.2", ) ) 

L'appel de show externalDependencyClasspath dans sbt 1.2.8 retournera angular-bootstrap:0.14.2 et angular:1.7.8 . Et où est passé le 1.7.8 ? Lorsqu'Ivy rencontre une gamme de versions, il va essentiellement sur Internet et trouve ce qu'il peut trouver, parfois même en utilisant le screenscraping.


Ce traitement des plages de versions rend les assemblys non répétitifs (l'exécution du même assemblage tous les quelques mois vous donne des résultats différents).


Gestion des gammes de versions à Coursier


Section Résolution des dépendances de Coursier sur la page Github
lit:


Des versions spécifiques à intervalles sont préférées
Si votre module a une dépendance sur [1.0,2.0) et 1.4, l'approbation de la version sera effectuée en faveur de 1.4.
S'il y a une dépendance sur 1.4, alors cette version sera préférée dans la gamme [1.0,2.0).

Cela semble prometteur.


 sbt:foo> show externalDependencyClasspath [warn] There may be incompatibilities among your library dependencies; run 'evicted' to see detailed eviction warnings. [info] * Attributed(/Users/eed3si9n/.sbt/boot/scala-2.12.8/lib/scala-library.jar) [info] * Attributed(/Users/eed3si9n/.coursier/cache/v1/https/repo1.maven.org/maven2/org/webjars/bower/angular/1.4.7/angular-1.4.7.jar) [info] * Attributed(/Users/eed3si9n/.coursier/cache/v1/https/repo1.maven.org/maven2/org/webjars/bower/angular-bootstrap/0.14.2/angular-bootstrap-0.14.2.jar) 

show externalDependencyClasspath sur le même assembly avec angular-bootstrap:0.14.2 renvoie angular-bootstrap:0.14.2 et angular:1.4.7 comme prévu. Il s'agit d'une amélioration par rapport à Ivy.


Prenons le cas le plus compliqué lorsque plusieurs plages de versions disjointes sont utilisées. Par exemple:


 ThisBuild / scalaVersion := "2.12.8" ThisBuild / organization := "com.example" ThisBuild / version := "1.0.0-SNAPSHOT" lazy val root = (project in file(".")) .settings( name := "foo", libraryDependencies ++= List( "org.webjars.npm" % "randomatic" % "1.1.7", "org.webjars.npm" % "is-odd" % "2.0.0", ) ) 

L'appel de show externalDependencyClasspath dans sbt 1.3.0-RC3 renvoie l'erreur suivante:


 sbt:foo> show externalDependencyClasspath [info] Updating https://repo1.maven.org/maven2/org/webjars/npm/kind-of/maven-metadata.xml No new update since 2018-03-10 06:32:27 https://repo1.maven.org/maven2/org/webjars/npm/is-number/maven-metadata.xml No new update since 2018-03-09 15:25:26 https://repo1.maven.org/maven2/org/webjars/npm/is-buffer/maven-metadata.xml No new update since 2018-08-17 14:21:46 [info] Resolved dependencies [error] lmcoursier.internal.shaded.coursier.error.ResolutionError$ConflictingDependencies: Conflicting dependencies: [error] org.webjars.npm:is-number:[3.0.0,4):default(compile) [error] org.webjars.npm:is-number:[4.0.0,5):default(compile) [error] at lmcoursier.internal.shaded.coursier.Resolve$.validate(Resolve.scala:394) [error] at lmcoursier.internal.shaded.coursier.Resolve.validate0$1(Resolve.scala:140) [error] at lmcoursier.internal.shaded.coursier.Resolve.$anonfun$ioWithConflicts0$4(Resolve.scala:184) [error] at lmcoursier.internal.shaded.coursier.util.Task$.$anonfun$flatMap$2(Task.scala:14) [error] at scala.concurrent.Future.$anonfun$flatMap$1(Future.scala:307) [error] at scala.concurrent.impl.Promise.$anonfun$transformWith$1(Promise.scala:41) [error] at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:64) [error] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [error] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [error] at java.lang.Thread.run(Thread.java:748) [error] (update) lmcoursier.internal.shaded.coursier.error.ResolutionError$ConflictingDependencies: Conflicting dependencies: [error] org.webjars.npm:is-number:[3.0.0,4):default(compile) [error] org.webjars.npm:is-number:[4.0.0,5):default(compile) 

Techniquement, c'est vrai, car ces plages ne se chevauchent pas. Alors que sbt 1.2.8 résout en is-number:4.0.0 .


En raison du fait que les plages de versions sont suffisamment courantes pour être ennuyeuses, j'envoie une requête Pull à Coursier pour implémenter des règles sémantiques plus latest-wins qui vous permettent de sélectionner des versions ultérieures dans les limites inférieures des plages.
Voir coursier / coursier # 1284 .


Conclusion


La sémantique du résolveur définit un chemin de classe spécifique basé sur des contraintes définies par l'utilisateur.


Habituellement, les différences de détails se manifestent de différentes manières pour résoudre les conflits de version.


  • Maven utilise la stratégie de nearest-wins les nearest-wins , qui peut rétrograder les dépendances transitives.
  • Ivy utilise la stratégie des latest-wins .
  • Coursier utilise principalement la stratégie des latest-wins , tout en essayant de spécifier les versions plus strictement.
  • Le gestionnaire de plage de versions Ivy va sur Internet, ce qui rend la même version non répétable.
  • Coursier et Ivy organisent des représentations de chaînes de versions de manières très différentes.

Même ces subtilités de l'écosystème Scala ne seront pas discutées lors de ScalaConf le 26 novembre à Moscou. Artem Seleznev introduira la pratique de travailler avec la base de données en programmation fonctionnelle sans JDBC. Wojtek Pitula parlera de l'intégration et expliquera comment il a créé l'application dans laquelle il a placé toutes les bibliothèques de travail. Et 16 autres rapports pleins de hardcore technique seront présentés à la conférence.

Source: https://habr.com/ru/post/fr474106/


All Articles