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

./

Autre section