Guide de survie (EN CONSTRUCTION)
Présentation
Julia (julialang.org) est un langage récent
dont la première communication officielle date de février 2012
(début des travaux en 2009).
Julia est gratuit, multi-plateforme, basé
sur LLVM (comme le compilateur c++ Clang
, et on peut y insérer de l'assembleur),
développé essentiellement par le MIT et orienté scientifique.
Julia est inspiré de Matlab, Ruby, Python,... tout en étant compatible
avec la notion de copié-collé !
Julia intégre nativement les tableaux multidimensionnels et l'algèbre
linéaire et dispose d'une communautée très active autour le
l'optimisation et des mathématiques appliquées en général.
Julia est un langage dynamique typé, compilé à la volée, non orienté objet au sens classique mais qui supporte la notion de "multi-dispach" de méthodes, généralisant ainsi la notion d'objets et de méthodes virtuelles. Ses possibilités d'introspection et sa caractéristique homoiconique (comme TCL ;-) lui permette de proposer un système puissant de macro.
La première version officielle (v1.0) est sortie en été 2018 et garantit une certaine stabilité des fonctions permettant désormais un usage professionnelle. Cependant la jeunesse de ce langage entraine quelques inconvénients qui seront progressivement corrigés (pas de pré-compilation statique ce qui nécessite que l'utilisateur d'un programme Julia dispose de Julia sur sa machine, lenteur au démarrage (en cours de correction), ...). La version actuelle (nov 2019) est la v1.3.0.
Premier contact
Pour tester Julia à l'ensta : suivez les indications données en cours. Vous devrez probablement compléter votre environnement unix en tapant une des commandes suivantes dans votre terminal :
usediam julia # version avec quelques packages standard de Julia
usediam ro # si vous souhaitez utiliser Julia avec cplex ou d'autres outils de RO
Une de ces commandes étant tapée, vous pouvez vérifier l'installation de Julia par :
julia --version
=> julia version 1.2.0
Vous pouvez alors utiliser Julia pour taper des commandes en mode interactif :
julia
# Vous pouvez quitter par exit() ou plus simplement par <Control-d>
julia> f(x) = x<=0 ? 0 : x<=2 ? 1 : f(x-1)+f(x-2)
f (generic function with 1 method)
julia> a = [f(i) for i in 0:10]; # construit un vecteur sans l'afficher
julia> a' # affiche la transposée du vecteur a
1x11 Array{Int64,2}:
0 1 1 2 3 5 8 13 21 34 55
julia> sum(a)
143
julia> a*a
ERROR: MethodError: no method matching *(::Array{Int64,1}, ::Array{Int64,1})
Closest candidates are:
*(::Any, ::Any, ::Any, ::Any...) at operators.jl:529
*(... etc...)
julia> a'*a
4895
julia> a*a'
11x11 Array{Int64,2}:
0 0 0 0 0 0 0 0 0 0 0
0 1 1 2 3 5 8 13 21 34 55
0 1 1 2 3 5 8 13 21 34 55
0 2 2 4 6 10 16 26 42 68 110
0 3 3 6 9 15 24 39 63 102 165
0 5 5 10 15 25 40 65 105 170 275
0 8 8 16 24 40 64 104 168 272 440
0 13 13 26 39 65 104 169 273 442 715
0 21 21 42 63 105 168 273 441 714 1155
0 34 34 68 102 170 272 442 714 1156 1870
0 55 55 110 165 275 440 715 1155 1870 3025
julia> exit() # ou simplement <Control-d>
Deuxième contact
# Création d'une fonction courte (notation mathématique naturelle)
f(x) = -3x^2 + 2x + 1
# f (generic function with 1 method)
# Nous dit qu'il y une seule méthode de nom "f".
# Cette unique méthode ne fait pas du tout la même chose
# en fonction du type (inféré) des paramètres reçus.
f(3)
# => -20
f(3.14)
# =>-22.2988
f(22//7)
# => -1095//49
f(2.5+2im)
# => -0.75 - 26.0im
# On peut spécialiser une fonction pour un type donné :
f(x::Int) = 2x
# => f (generic function with 2 methods)
# nous dit maintenant qu'il y deux méthodes de nom "f".
# testons ces deux méthodes
f(3.0)
# => -20.0
f(3)
# => 6
# En fait le code LLVM (assembleur) pour le second appel est optimisé pour
# le type Int64.
# La macro julia @code_llvm montre que le code généré est complétement
# différent selon le type des paramètres passés :
# (les lignes commençant pas ";" sont des commentaires LLVM facilitant
# le repérage dans code source)
# La version prenant un entier (f(x::Int) = 2x) se contente de faire un
# décalage à gauche de 1 bit
@code_llvm f(3)
# ; @ REPL[6]:1 within `f'
# define i64 @julia_f_15975(i64) {
# top:
# ; ┌ @ int.jl:54 within `*'
# %1 = shl i64 %0, 1
# ; └
# ret i64 %1
# }
@code_llvm f(3.14)
# =>
# ; @ REPL[1]:1 within `f'
# define double @julia_f_15944(double) {
# top:
# ; ┌ @ intfuncs.jl:244 within `literal_pow'
# ; │┌ @ float.jl:399 within `*'
# %1 = fmul double %0, %0
# ; └└
# ; ┌ @ promotion.jl:314 within `*' @ float.jl:399
# %2 = fmul double %1, 3.000000e+00
# %3 = fmul double %0, 2.000000e+00
# %4 = fsub double %3, %2
# ; └
# ; ┌ @ operators.jl:529 within `+' @ promotion.jl:313 @ float.jl:395
# %5 = fadd double %4, 1.000000e+00
# ; └
# ret double %5
# }
Notation pointée (Dot Broadcast)
cf https://www.manhattanmetric.com/blog/2018/01/five-more-reasons-to-check-out-julia.html
Matlab propose la propagation de certains opérateurs appliqués aux matrices
à ses éléments par la notation pointée (e.g M1 * M2
versus M1 .* M2
).
Julia rigorise et généralise cette fonctionnalité à toute méthode (même
définie par l'utilisateur) pour diffuser un opérateur aux éléments d'une
collection quelconque.
Le prix à payer pour cette généralisation est que cette propagation n'est plus implicite et doit être spécifiée par l'utilisateur :
m = reshape(1:9, 3, 3) # => Comme en Fortran, les matrices sont rangées par colonne puis ligne # 1 4 7 # 2 5 8 # 3 6 9 m2 = m + 1 # => ERROR: MethodError: no method matching # +(::Base.ReshapedArray{Int64,2,UnitRange{Int64},Tuple{}}, ::Int64) # Closest candidates are: (etc...) m2 = m .+ 1 # => 3×3 Array{Int64,2}: # 2 5 8 # 3 6 9 # 4 7 10 Le choix de manipulation d'une collection par une méthode de manière globale ou au contraire par élément doit donc se faire lors de l'écriture de cette méthode. Pour manipuler des matrices terme à terme, la méthode de la section précédente devrait donc être réécrite comme suit : # reprenons notre fonction précédante f1(x) = -3x^2 + 2x + 1 # erreur avec une matrice # f2(x) = -3x^2 + 2x .+ 1 # possible avec une matrice f2(x) = -3x.^2 + 2x .+ 1 # entièrement terme à terme # => f2 (generic function with 1 method) f2(22//7) # => -1095//49 # fonctionne toujours comme précédemment f2(m) # => 3×3 Array{Int64,2}: # 0 -39 -132 # -7 -64 -175 # -20 -95 -224 En fait l'intérêt de la notation pointée de Julia est qu'elle peut s'appliquer à n'importe quelle méthode ou opérateur. La définition de la méthode f2 était donc inutile et on optient le même résultat en utilisant f1 avec la notation pointée "f1.(xxx)": f1.(m) # => 3×3 Array{Int64,2}: # 0 -39 -132 # -7 -64 -175 # -20 -95 -224 Cette fonctionnalité est très pratique est évite d'utiliser la méthode `map()` (application d'une méthode à chaque élément d'une collection) pour le même résultat.
La notation pointée permet également de manipuler des collections sans se préocupper des types concrets sous-jacents :
# un type range (qui est un type très peu encombrant en mémoire) a_rng = 1:20 # un tableau aléatoire de Float64 a_vec = rand(20) # Un tuple (20 jets de dé) a_tup = Tuple(rand(1:6, 20)) # une multiplication de ces trois collections result = a_rng .* a_vec .* a_tup
./