Interact.jl, la nouvelle manière de réaliser des sites Web interactifs pour accéder à des codes de calcul

Julia est un langage de programmation principalement prévu pour des applications scientifiques, où la quantité de calculs à effectuer est très importante. D’habitude, ce genre de code s’écrit dans un langage assez statique, comme C, C++ ou Fortran, pour profiter de leur performance ; cependant, Julia se focalise sur une approche plus dynamique (comme Python ou MATLAB, plus prévus pour les interfaces que le calcul intensif) tout en gardant la performance à l’exécution. Julia arrive à rassembler ces deux côtés, la performance et la dynamicité.

Cet aspect dynamique a déjà été exploité pour créer des interfaces graphiques par le Web qui exploitent des codes de calcul. Par exemple, Escher.jl proposait de créer des interfaces composables, développées d’une manière similaire à Shiny en R. Ce paquet n’est plus maintenu depuis 2016 et n’a de remplaçant digne de ce nom que depuis peu, avec Interact.jl nouvelle génération (ce projet s’appelait InteractBase.jl jusqu’il y a peu avant d’être renommé et de remplacer l’ancien Interact.jl).

Ce nouveau paquet propose de créer des interfaces graphiques sur le Web pour des applications scientifiques. Il se construit par-dessus WebIO.jl, une couche d’abstraction pour l’interaction à travers les protocoles du Web : on peut ainsi développer un seul code pour l’utiliser dans un navigateur Web traditionnel (à travers le serveur Mux.jl), dans une application de bureau créée avec Electron (grâce à Blink.jl) ou encore dans Juno, l’environnement de développement de référence actuel pour Julia, ou IJulia.

Interact.jl utilise HTML5 et ses composants pour ses widgets, ce qui permet de couvrir facilement un grand nombre de fonctionnalités (sélection de couleur, de date, d’heure, de texte, etc.). Des frameworks CSS sont utilisés pour le style, qui est alors entièrement abstrait du code de l’interface : pour le moment, on peut utiliser Bulma et UIkit, mais aussi utiliser d’autres frameworks (UIkit a été ajouté en moins de deux cents lignes de code) ou encore intégrer son propre code CSS pour modifier un framework existant. Vue.js/.jl est utilisé pour la logique JavaScript et la synchronisation avec le script Julia.

Voir aussi : le tutoriel officiel, la liste des widgets disponibles, la documentation.

Sources : ANN: InteractBase, a redesign of Interact to create and style web apps in Julia, Sputnik project, second update.

Advertisements

Sortie de Julia 0.7 Alpha

Le langage de programmation Julia est prévu pour apporter les bienfaits en termes de productivité des langages dynamiques au monde extrêmement conservateur du calcul de haute performance. La première préversion Alpha du langage en version 0.7 est maintenant disponible et elle sera assez proche de la version 1.0. Elle contient toutes les fonctionnalités désapprouvées du langage, avec des avertissements (ces constructions donneront directement des erreurs avec Julia 1.0, prévue pour début août) : cette version 0.7 n’a que pour objectif de faciliter la transition de l’écosystème vers la 1.0. Les développeurs de Julia se sont inspirés des problèmes que Python a eu en passant de la version 2 à 3 : il sera possible, pour une très grande majorité des paquets, d’avoir un même code fonctionnel sur la version 0.6 (l’actuelle), la 0.7 ou sur la 0.7 et la 1.0.

La liste des changements par rapport à la version 0.6 est impressionnante : elle représente quatre-vingt cinq kilooctets de texte !

Syntaxe

Une bonne partie des changements de cette version vient de la syntaxe du langage. Ainsi, la syntaxe de définition des fonctions est plus flexible. Les arguments peuvent être déstructurés automatiquement par le langage : range((min, max)) = max - min définit automatiquement une fonction à un seul argument, range, cet argument étant un tuple. L’avantage est que, dans le code de la fonction, on peut utiliser chaque élément de ce tuple sans devoir le déstructurer soi-même : une fonction aussi simple peut être écrite en une ligne, en gagnant en lisibilité. Cette syntaxe se mélange sans aucun problème avec d’autres arguments :

foo((a, b), c) = (a + b) * c

Toujours dans les définitions de fonction, le conteneur d’arguments nommés (généralement, kw) est maintenant implémenté comme un tuple nommé (plutôt qu’une liste de paires). Ce conteneur apparaît lorsqu’une fonction ne définit pas tous ses arguments nommés : f(; kw...), par exemple. Les fonctions habituelles des dictionnaires fonctionnent alors naturellement : au lieu de devoir itérer dans toutes les entrées manuellement, on peut simplement utiliser haskey(kw, "arg"). Ceci n’est rendu possible que par l’implémentation des tuples nommés avec une excellente performance (qui a empêché cette manière de procéder jusqu’à présent). Petit supplément : une fonction peut maintenant marquer un argument nommé comme obligatoire, simplement en ne définissant pas sa valeur par défaut (ce qui peut être très utile pour la lisibilité du code).

L’objet missing sert désormais à représenter des valeurs manquantes (comme NULL en SQL ou NA en R, voire comme NaN pour le calcul en virgule flottante). Elle se propage naturellement dans les opérateurs et les fonctions mathématiques selon une logique à trois valeur (1 + missing == missing, par exemple). Cet objet a pour type Missing. Il peut simplifier très largement des parties de code qui utilisaient Nullable{T} comme type pour gérer des données qui pourraient manquer : le type de ces données devrait maintenant être Union{T, Missing}, ce qui est aussi clair ; dans le code, il n’est plus nécessaire d’utiliser des artefacts comme get(v) pour récupérer la valeur dans le cas où une valeur existe.

Aussi, la gestion des opérateurs a été dopée. Outre le nouvel opérateur de comparaison , quelques caractères de combinaison d’Unicode (primes, exposants, indices) peuvent se combiner aux opérateurs prédéfinis. Par exemple, on peut ainsi définir le nouvel opérateur de somme des carrés : +₂(a,b) = a^2 + b^2. Ces possibilités seront utiles à certains paquets effectuant des mathématiques de haut vol, en rapprochant la syntaxe de la notation mathématique usuelle. Fidèle à son habitude, Julia ne rate donc pas une occasion de gérer une plus grosse partie d’Unicode. Toujours pour les opérateurs, mais en mode mineur, l’opérateur paire => (utilisé pour définir des dictionnaires, par exemple) peut être diffusé sur des listes (.=>).

Pour l’appel des macros, il devient possible d’utiliser des crochets en plus des parenthèses : @macroname[args] correspond à @macroname([args]). Cette possibilité est prévue pour l’implémentation facile de nouveaux types de tableaux, par exemple des tableaux statiques (StaticArrays.jl) : il est plus naturel d’écrire @SArray[1 2; 3 4] que @SArray([1 2; 3 4]).

Les énumérations peuvent être définies dans un bloc begin-end, plutôt que de devoir être définies sur une seule ligne :

@enum Fruit begin
	apple=1 
	orange=2
	kiwi=4
end

Auparavant, il fallait impérativement écrire @enum Fruit apple=1 orange=2 kiwi=4, peu importe le nombre d’items.

La macro @isdefined permettra de déterminer si une variable locale est définie. Les tuples nommés, comme t=(a=1, b2=), font leur apparition : leurs champs peuvent être accédés par un numéro (t[1]) ou par un nom (t.a). De même, dans les fonctions générées (elles prennent en argument les types des arguments et génèrent un code hautement spécialisé), il devient possible de définir une fonction qui est à la fois générée et non générée (pour une implémentation non spécialisée) en utilisant la macro @generated pour délimiter les parties générées des autres.

Certaines syntaxes sont désapprouvées, en vue de leur assigner une nouvelle signification plus tard. Par exemple, begin ne peut plus apparaître dans une indexation : dans le futur, a[begin] pourrait sélectionner le premier item d’un tableau (tout comme a[end] sélectionne le dernier élément). Il n’est plus possible d’effectuer d’assignation au niveau d’une indexation : x[i=1] sélectionnait l’élément 1 du tableau et définissait une variable i (certains utilisaient cette possibilité pour se rapprocher de leurs habitudes en C, comme a[i += 1] pour sélectionner l’élément courant et incrémenter i) ; bientôt, cette syntaxe pourrait être récupérée pour passer des arguments nommés lors de l’indexation, ce qui serait utile pour la lisibilité de certains types de tableaux (notamment bidimensionnels : on pourrait écrire a[x=1, y=2], comme xarray en Python).

Bibliothèque standard

Le gestionnaire de paquets précédent, Pkg2, était un reliquat de Julia 0.2. Il avait un certain nombre de limitations, notamment pour indiquer des dépendances conditionnelles entre modules (par exemple, pour des fonctions dont leur seul objectif est de faciliter l’utilisation conjointe de deux modules). La description des paquets se faisait par une myriade de petits fichiers (quelques octets), ce qui faisait rapidement une performance horrible selon le système de fichiers ; en particulier, une version est décrite par un identifiant de commit Git, ce qui impose de cloner le dépôt Git d’un paquet pour l’installer (y compris tout l’historique, même s’il est très gros), puis d’utiliser Git pour vérifier la version installée (ce qui est d’une lenteur abyssale). Ces caractéristiques étaient très intéressante dans le cas d’un écosystème en plein boom et où la proportion d’utilisateurs qui ne contribuent pas à de nouveaux paquets est assez faible, mais plus à un langage qui se veut utilisable par le plus grand nombre. Pkg3 évite ces écueils en repartant de zéro pour la description et l’installation de paquets. Le dépôt de paquets actuel est bien sûr importé directement dans celui de Pkg3 (une liste modérée des “meilleurs” paquets sera aussi disponible). Pkg3 gère aussi la notion d’environnement, c’est-à-dire un ensemble de paquets et de versions que l’on peut échanger rapidement (comme les environnements virtuels de Python).

Le protocole d’itération a complètement changé avec cette version, notamment en exploitant le travail effectué pour intégrer missing dans le langage avec une bonne performance. Auparavant, il fallait implémenter trois fonctions différentes : start pour déterminer l’état initial de l’itérateur, next pour récupérer l’élément courant et le prochain état, done pour indiquer si l’état est final. Maintenant, il n’y a plus que la fonction iterate : avec un seul argument (l’itérateur), elle doit retourner l’état initial et le premier élément ; avec deux arguments (l’itérateur et l’état), elle doit retourner le prochain élément et le prochain état… ou nothing si l’itérateur n’a plus d’éléments dans la collection.

La bibliothèque mathématique de Julia est en cours de réécriture : elle était écrite entièrement en C (openlibm), en repartant du code de FreeBSD et d’OpenBSD, mais avait ses limitations. Notamment, si une implémentation d’une fonction spéciale (logarithme, exponentielle, sinus, etc.) est fournie, elle ne vaut que pour un seul type ; si elle était écrite en Julia, la même implémentation pourrait être utilisée avec d’autres types (nombre à virgule flottante sur trente-deux ou soixante-quatre bits, voire à précision infinie, par exemple). La charge de maintenance est identique (le projet Julia doit toujours maintenir sa propre bibliothèque mathématique, celles des compilateurs n’étant pas toujours au niveau), mais les avantages sont présents — notamment celui de diminuer la barrière d’entrée pour les nouveaux contributeurs. Ce travail est toujours en cours.

Parallélisme en mémoire partagée avec Julia

Julia est un langage de programmation prévu pour le calcul scientifique tout d’abord, même s’il se généralise de plus en plus, notamment pour les interfaces graphiques (InteractBase.jl, par exemple). Il cherche à obtenir le meilleur des deux mondes : celui des langages pour le calcul intensif (C, C++, Fortran…), où la performance est le critère le plus important, ainsi que celui des langages dynamiques (Python, LISP…), bien plus pratiques pour le développement.

Dès 2015, les premières expériences pour apporter le parallélisme à mémoire partagée ont donné naissance à la construction @threads : il devient alors très simple de lancer toutes les itérations d’une boucle donnée en parallèle. Cette manière de procéder correspond à ce qui est le plus souvent fait dans le calcul intensif (elle correspond à OpenMP, par exemple). Son implémentation force à repenser certaines parties de l’implémentation de Julia, afin de s’assurer que toutes les parties du code fonctionnent de manière sécurisée en parallèle : le ramasse-miettes, la génération du code, etc.

Cependant, cette construction est très limitée : elle ne s’adresse qu’aux cas où le même code est exécuté sur des parties de données différentes (parallélisme de données). Ce paradigme correspond très bien au fonctionnement des processeurs graphiques, mais pas à la totalité des applications, qui ont besoin d’un modèle de fils d’exécution plus précis. Par exemple, il n’était pas possible de lancer le code de calcul dans un fil d’exécution séparé de l’interface graphique — cette dernière devait être figée pendant l’exécution des calculs. Bien que limitée, elle s’est montrée très efficace en pratique : Celeste.jl est un moteur d’inférence à base d’images astrophysiques qui a pu être exécuté sur un superordinateur, en développant une puissance de calcul de plus d’un téraflops.

L’approche en cours de développement sera similaire à celle de Cilk (un dérivé de C développé au MIT), avec trois types principaux de construction : spawn pour lancer un fil d’exécution, sync pour en attendre les résultats et parfor pour paralléliser simplement une boucle. Ce paradigme est à la fois très puissant (toutes les fonctionnalités sont disponibles pour implémenter toute logique parallèle) et très simple (ce qui garantit une certaine productivité pour écrire le code et diminue le risque d’erreurs).

Néanmoins, cette manière d’écrire des programmes parallèles pose très vite des questions d’implémentation très pratiques : que faire en cas de parallélisme imbriqué (ou composé) ? Que se passe-t-il lorsqu’un code parallèle appelle une bibliothèque qui exploite elle-même le parallélisme ? En général, ces bibliothèques sont très bien écrites et profitent énormément des capacités parallèles (comme BLAS pour les opérations d’algèbre linéaire), mais la majorité des implémentations actuelles (OpenMP, Cilk…) ne permettent de paralléliser que la boucle extérieure, tandis que toutes les boucles intérieures sont exécutées en série (ce qui inclut les bibliothèques extérieures, de manière générale).

Julia envisage de fonctionner de manière différente, avec un ordonnancement en profondeur. La majorité des systèmes actuels utilisent le “vol” de tâches : quand un fil d’exécution a terminé toutes ses tâches, il prend le travail d’autres fils qui n’ont pas encore fini. Le problème est qu’il faut donc stocker une liste de toutes les tâches pour chacun des fils d’exécution, ce qui peut représenter une grande quantité de mémoire. Au contraire, l’ordonnancement en profondeur a d’excellentes garanties théoriques (pas besoin de stocker de grandes piles de tâches à exécuter pour chaque fil d’exécution) et, en pratique, a toutes les propriétés requises pour exploiter au mieux les caches des processeurs. Les fils d’exécution ne décident pas eux-mêmes des tâches à exécuter, puisqu’un contrôleur central s’en charge. Cependant, il n’en existe, pour le moment, aucune implémentation utilisable en production, seulement quelques codes de recherche.

Le code pour ce faire en Julia (partr) est en cours de développement depuis plus d’un an et utilise la notion de coroutine pour l’implémentation des tâches. Certaines fonctionnalités sont d’ores et déjà implémentées

Source : Shared memory parallelism in Julia with multi-threading.

Du neuf sur Julia 0.7

Julia, le langage de programmation orienté calcul scientifique moderne (aussi simple à utiliser que Python ou MATLAB, aussi performant que Fortran ou C++), n’est pas un grand habitué du respect des dates : l’arrêt des fonctionnalités pour la version 1.0 était prévu pour la mi-décembre… mais ce n’est toujours pas le cas à la mi-mars ! Comme depuis longtemps, cette version est annoncée pour “bientôt” : pendant ce temps, les développeurs peaufinent le langage et son environnement, pour qu’il soit aussi agréable que possible dès le premier jour de la 1.0. C’est aussi pour cela que la version 0.7 sortira plus tôt : elle contiendra toutes les nouveautés de la 1.0, avec quelques couches de compatiblité avec les précédentes versions.

Un très gros point bloquant pour le moment est la réécriture complète de l’optimiseur de Julia. En effet, pour atteindre un très haut niveau de performance, l’implémentation du langage utilise un moteur de compilation très performant (LLVM, aussi derrière Clang ou Flang), couplé à une série de passes d’optimisation spécifiques à Julia. Les dernières versions de Julia ont profondément modifié certains aspects du système de type, notamment pour gérer les valeurs manquantes dans les tableaux de données : au lieu d’un type spécifique, elles sont maintenant représentées par une union (soit rien, soit une valeur d’un type donné). De plus, le protocole des itérateurs change complètement, notamment pour être plus simple à implémenter : il se réduit à une fontion iterate de l’état courant, qui renvoie soit rien (s’il n’y a plus d’élément suivant), soit un nouvel état et la valeur associée — soit le même genre d’union que pour les valeurs manquantes. Cependant, la passe d’optimisation précédente était incapable de générer du code décemment rapide pour ces cas particuliers : pour garder une bonne performance, la réécriture était nécessaire.

Ce retard dû à l’optimiseur laisse du temps pour travailler sur d’autres points, notamment la gestion des tableaux. D’autres points restent toujours en suspens : moins importants (ils ne risquent pas de casser du code existant), ils pourront être intégrés par la suite, dans le cycle de vie de Julia 1.x. La vitesse de compilation en fait partie.

La gestion des paquets est aussi un chantier pour la première version stable à long terme de Julia. Elle est actuellement prise en charge par Pkg2, un utilitaire développé du temps de Julia 0.1 et qui commence à montrer ses limites : par exemple, pour les dépendances, il n’est possible que d’indiquer des dépendances dures. Impossible de représenter une dépendance à l’un d’une série de paquets (on souhaite simplement qu’au moins un des paquets de cette liste soit installé) ou des fonctionnalités conditionnelles (activer certaines fonctionnalités d’un paquet quand un autre est installé). La principale barrière au déploiement de la nouvelle version du gestionnaire de paquets, la bien nommée Pkg3, est qu’elle n’est compatible qu’avec les préversions actuelles de Julia 0.7… et que peu de paquets sont compatibles. Il manque aussi l’expressivité des versions requises pour l’installation d’un paquet (au moins telle version jusque telle autre en excluant une troisième, par exemple). Énormément d’architecture est déjà en place, comme la conversion automatique des descriptions de paquets au nouveau format : le grand jour, il sera possible de proposer automatiquement à tous les projets enregistrés dans le gestionnaire de paquets une description au nouveau format.

Sources : 1.0 progress/status, Pkg3 plan and status.

Les fonctionnalités de Julia 1.0 seront arrêtées le 15 décembre

Julia est un langage de programmation de haut niveau, dynamique, mais orienté applications de haute performance (surtout pour le calcul scientifique). Malgré son jeune âge (il est apparu pour la première fois au grand public en 2012), il a déjà réussi à fédérer une certaine communauté (on estime à 250 000 son nombre d’utilisateurs en 2017).

Il s’apprête à voir sa première version finale, numérotée 1.0, après de longues années de développement. Toutes les fonctionnalités ne sont pas encore implémentées, mais cela ne devrait tarder : plus aucune ne sera acceptée dès le 15 décembre. À cette date, la première préversion Alpha de la 0.7 sera disponible, six mois après la 0.6. Julia 0.7 et 1.0 devraient être disponibles dans deux mois, si tout va bien.

Bon nombre d’évolutions attendues font leur apparition, comme la macro @isdefined pour déterminer si une variable locale existe ou encore un conteneur de type dictionnaire pour les arguments passés par nom à une fonction (en réalité, des tuples nommés, une autre nouveauté). Les valeurs manquantes, très utilisées en science des données, font leur apparition au sein même du langage (auparavant, elles l’étaient dans les paquets qui en avaient besoin). La macro @nospecialize est utilisée pour indiquer que le compilateur ne peut pas spécialiser une fonction sur le type d’un argument.

Certaines constructions peu lisibles ne sont plus acceptées dans le langage : 1.+2 pouvait être compris comme la somme d’un nombre à virgule flottante (1.) et d’un réel (2) ou bien comme la somme élément par élément entre listes (d’un côté, 1, de l’autre, 2 : 1 .+ 2) ; la juxtaposition de nombres en notation binaire, octale et hexadécimale ne pouvait être qu’illisible (0xapi était compris comme 0xa * pi). La bibliothèque standard a été considérablement allégée, en suivant un mouvement lancé depuis belle lurette : les fonctionnalités moins souvent utilisées (ou qui mériteraient d’être mises à jour plus souvent que le compilateur) ont été déplacées dans des paquets.

La sortie de la version 1.0 se fera en deux temps : tout d’abord, la version 0.7, qui aura exactement les mêmes fonctionnalités, mais avec des avertissements pour ceux qui utiliseraient d’anciennes syntaxes maintenant déconseillées ou supprimées ; très rapidement après, la 1.0, expurgée de ces messages. Ainsi, il sera conseillé de migrer son code vers la version 0.7 pour profiter des messages d’avertissement plus nombreux ; ensuite, le passage à la 1.0 se fera instantanément. Pour les nouveaux, par contre, seule la version 1.0 sera mise en avant.

Toutes les fonctionnalités souhaitées pour Julia 1.0 ne sont pas encore implémentées, toutefois. Si les développeurs ont le temps, tout ce qui sera possible sera implémenté d’ici au 15 décembre, pour laisser le temps aux tests avant la version finale. Les autres fonctionnalités seront ajoutées lors du cycle de vie de Julia 1.x, quitte à laisser des syntaxes indésirables acceptées : elles ne seront supprimées que lors du passage à Julia 2.0.

Voir aussi : la liste des changements apportés jusqu’à présent à Julia 0.7.0.

Source : 1.0 Feature Freeze Dec 15th.

Zinc : Scala se met à la compilation rapide

Ces derniers temps, Scala a vu quelques améliorations notables dans ses outils de compilation. Notamment, le compilateur incrémental Zinc a atteint la version 1.0.

Zinc est utilisé par bon nombre de développeurs Scala sans vraiment qu’ils s’en rendent compte : il est intégré dans sbt (pour lequel il a d’abord été prévu), pants, CBT, IntelliJ ou encore Scala IDE. Son seul objectif est d’améliorer les temps de recompilation en limitant le nombre de fichiers traités : Zinc analyse les dépendances entre parties du code et n’en recompile qu’une partie, celle qui a été changée depuis la dernière compilation et tout ce qui en dépend. Il est prévu pour que le résultat de cette compilation soit identique à une compilation depuis zéro — quitte à être conservatif et à recompiler des choses qui n’en auraient pas eu besoin.

Une des nouveautés de cette version 1.0 est l’analyse de dépendance au niveau des classes : auparavant, cette analyse était effectuée par fichier. Cependant, vu qu’un fichier Scala peut contenir un grand nombre de classes, cette manière de procéder menait à de grandes inefficacités. Grâce à ce changement, les temps de recompilation ont pu être améliorés d’un facteur quarante dans certains cas. Ce code était en partie prêt depuis mars 2016, mais a dû être retravaillé avant d’arriver en production, notamment au niveau de la compatibilité avec Java.

En pratique, sur ScalaTest, l’évolution entre Zinc 0.13 et 1.0 est très importante : la recompilation de ce projet de 40 377 lignes prend sept fois moins de temps avec la nouvelle version, en ajoutant simplement une nouvelle méthode.

Avec Zinc 0.13, la compilation incrémentale prend vingt et une secondes :

Zinc 1.0 réduit ce temps à trois secondes :

D’autres améliorations de Zinc 1.0 concernent de rares cas de sous-recompilation (qui causaient des problèmes de correction), des améliorations dans le pont avec Java ou dans la gestion des types. Les API ont été améliorées et le code a été entièrement migré vers Scala 2.12 : Zinc est maintenant prêt pour Java 8.

Sources : Speed up compile times with Zinc 1.0.

Clazy Web UI, une interface Web pour visualiser les messages de Clang

Clazy est une extension au compilateur Clang qui effectue une analyse statique principalement orientée Qt, afin de faciliter le déploiement de bonnes pratiques, mais aussi de limiter les allocations de mémoire dues à une mauvaise utilisation de l’API.

KDAB annonce maintenant, après Clazy, une interface Web pour faciliter la visualisation des résultats, ainsi que des messages produits par le compilateur en général : Clazy Web UI. KDAB en héberge une instance pour Qt, ce qui permet de voir l’outil en situation réelle.

L’interface est prévue pour être simple à utiliser : après avoir sélectionné un module Qt, elle montre tous les types d’avertissement trouvés lors de la compilation et les endroits dans le code. Il est aussi possible de filtrer les résultats bruts, afin d’éliminer les faux positifs ou encore les mentions les moins intéressantes.

Outre l’amélioration de l’outillage C++, l’objectif est bien évidemment d’attirer plus de contributeurs à Qt, en leur montrant une série de tâches relativement simples à accomplir. Cela pourrait les aider à franchir la barre de gerrit.

Source : Clazy Results Visualizer for Qt, Web UI to view clazy and gcc warnings.