Julia est un langage généraliste, qui peut servir à la plupart des tâches de programmation. En pratique, cependant, ses principaux cas d'usage se situent plutôt dans l'ingénierie et les sciences. Des calculs numériques rapides, polyvalents et sophistiqués sont au cœur de sa conception.
Un entier est un nombre « rond », sans virgule.
Dans le concept Bases, on a vu qu'une valeur entière peut être affectée à une variable sans préciser de type.
Pour la lisibilité, on peut utiliser des tirets bas comme séparateur de chiffres. Ils sont ignorés par le compilateur.
julia> x = 3
3
julia> typeof(x)
Int64
julia> large_number = 1_234_567_890
1234567890
En interne, le compilateur utilisera le type entier signé le plus approprié au processeur.
Sur les PC modernes, ce sera généralement Int64, ce qui est parfaitement suffisant pour la plupart des tâches.
Les types seront abordés plus en détail dans un concept futur. Pour l'instant, les types numériques devraient être assez intuitifs pour que ce document ait du sens.
Comme nous le verrons dans un concept ultérieur, Julia prend nativement en charge des tableaux multidimensionnels très grands. Ceux-ci peuvent mettre à rude épreuve aussi bien la mémoire disponible que la bande passante pour les transferts de données.
Pour donner au programmeur un contrôle plus fin, il est à la fois possible et assez courant de préciser le type entier signé ou non signé souhaité. Les nombreux apprenants de Julia qui ont déjà utilisé Python voudront peut-être comparer cela à NumPy, plutôt qu'au Python de base.
julia> y = Int8(42)
42
julia> typeof(y)
Int8
julia> z = UInt32(1024)
0x00000400
Note que Julia affiche par défaut les entiers non signés au format hexadécimal, car les types non signés servent souvent à des opérations sur les bits de bas niveau.
Les entiers peuvent aussi s'écrire en binaire, en octal ou en hexadécimal, avec les préfixes 0b, 0o et 0x respectivement.
julia> a = 0x10
0x10
julia> Int(a)
16
julia> b = 0b1010 # displays as hexadecimal
0x0a
BigInt
Chaque type entier a une valeur maximale et une valeur minimale qu'il peut stocker :
julia> typemax(Int8)
127
julia> typemin(Int8)
-128
Sortir de cette plage valide provoquera un débordement d'entier, avec des résultats qui semblent très étranges.
julia> c = Int8(126)
126
julia> c * c
4
On peut sans doute convenir que 126 * 126 ne devrait pas être égal à 4 !
Une explication détaillée demanderait de savoir comment les registres du processeur stockent les entiers, ce qui dépasse le cadre de ce concept.
On peut minimiser le problème en utilisant un type « plus large », jusqu'à Int128 ou UInt128 (la signification de « plus large » est abordée ci-dessous). Cependant, tous ont des limites strictes avant de rencontrer des problèmes de débordement.
Une solution consiste à utiliser le type BigInt, qui n'est limité que par la mémoire de ton ordinateur.
C'est l'implémentation par Julia de l'arithmétique à précision arbitraire.
julia> 2 ^ 70
0
julia> big = BigInt(2) ^ 70
1180591620717411303424
julia> UInt128(big)
0x00000000000000400000000000000000
Comme 2 puissance 70 ne tient pas dans un Int64, le premier calcul de l'exemple ci-dessus échoue : note cependant qu'il n'y a aucun message d'erreur.
Programmeur, méfiance !
Convertir 2 en BigInt donne la bonne réponse, et l'afficher en hexadécimal montre plus clairement qu'il s'agit d'une puissance de 2.
Il n'est pas surprenant que les nombres à virgule flottante puissent avoir une virgule, et une partie fractionnaire après cette virgule.
julia> f = 3.45
3.45
julia> typeof(f)
Float64
Bien sûr, la notation scientifique est prise en charge.
julia> avogadro = 6.02e23
6.02e23
Comme pour les entiers, le type par défaut convient à la plupart des usages, mais d'autres types signés sont disponibles. Il n'existe pas de type à virgule flottante non signé.
Pour faire plus court, on peut créer des valeurs Float32 en utilisant le suffixe f0.
julia> f32 = 4.56f0
4.56f0
julia> typeof(f32)
Float32
Les autres tailles de flottants nécessitent une conversion explicite, comme pour les entiers.
Les valeurs maximale et minimale peuvent surprendre :
julia> typemax(Float64)
Inf
julia> typemin(Float64)
-Inf
L'infini est une valeur valide !
Cependant, la plage utile des nombres à virgule flottante est limitée : tout ce qui est très grand se voit simplement attribuer une valeur Inf, et tout ce qui est très petit est arrondi à 0.0.
On peut utiliser une autre paire de fonctions pour visualiser ces limites : environ ± 10 ^ 308.
julia> floatmax(Float64)
1.7976931348623157e308
julia> floatmin(Float64)
2.2250738585072014e-308
De plus, la précision est limitée : un nombre grand mais fini de chiffres significatifs est représenté (environ 15 pour Float64).
Comme nous l'avons vu dans le concept Bases, les opérateurs arithmétiques fonctionnent pour l'essentiel comme l'arithmétique classique, telle qu'on l'enseigne aux enfants.
Note que l'exponentiation utilise ^, et non ** (les deux sont courants dans d'autres langages).
2 + 3 # 5 (addition)
2 - 3 # -1 (subtraction)
2 * 3 # 6 (multiplication)
8 / 2 # 4.0 (division)
8 % 3 # 2 (remainder)
2 ^ 3 # 8 (exponentiation)
Cependant, quelques détails propres à Julia méritent d'être examinés.
julia> x = 4.2
4.2
julia> 2 * x
8.4
julia> 2x
8.4
julia> 2.4x
10.08
Cela peut surprendre.
Il est toujours possible d'utiliser * comme opérateur infixe, comme dans la plupart des autres langages informatiques.
Cependant, Julia a été conçu par des gens qui pensent que le code devrait ressembler autant que possible à des équations mathématiques.
Comme les noms de variables doivent commencer par une lettre, faire précéder le nom d'un nombre (entier ou à virgule flottante) est interprété comme une multiplication implicite.
Par exemple, si on veut l'aire d'une sphère, au lieu de 4 * pi * r * r, on pourrait écrire ceci :
julia> surface(r) = 4π * r^2
surface (generic function with 1 method)
julia> surface(3)
113.09733552923255
Bien que π soit une constante intégrée, c'est aussi une lettre (grecque).
L'analyseur syntaxique a donc toujours besoin d'un * explicite pour séparer π de r.
Utiliser / comme opérateur infixe donnera toujours un résultat à virgule flottante, même pour des entrées entières.
Pour la division entière, il existe plusieurs options :
julia> 10 / 3
3.3333333333333335
julia> div(10, 3)
3
julia> 10 ÷ 3
3
julia> 10 // 3
10//3
La fonction div() sert à la division entière, le résultat étant tronqué vers zéro : vers le bas pour les nombres positifs, vers le haut pour les nombres négatifs.
Comme synonyme, on peut utiliser l'opérateur infixe ÷, là encore pour rendre le tout plus mathématique.
Si tu utilises un éditeur compatible avec Julia, saisis-le avec \div puis appuie sur la touche <Tab>.
L'opérateur // aura besoin d'un concept qui lui soit propre, plus tard dans le programme.
Pour l'instant, disons simplement que le résultat de // est un nombre « rationnel », le nom formel de ce que la plupart des gens appellent une fraction.
Les facteurs communs sont retirés du numérateur et du dénominateur, pour donner un rapport de deux entiers sous ce qu'on appelle la « forme irréductible ».
julia> rationalnum = 22 // 6
11//3
julia> typeof(rationalnum)
Rational{Int64}
On a les nombres rationnels. Et les nombres « irrationnels » ?
julia> π
π = 3.1415926535897...
julia> typeof(π)
Irrational{:π}
Un nombre irrationnel est un nombre qui ne peut pas être réduit à un rapport d'entiers.
Les exemples courants incluent π, e (nombre d'Euler), et de nombreuses racines comme √2 (la racine carrée de 2).
Julia essaie de faire des mathématiques correctement.
Cela peut souvent se produire automatiquement :
julia> x = 2 + 3.5
5.5
julia> typeof(x)
Float64
On a additionné un Int64 et un Float64, et obtenu un résultat Float64.
En fait, l'entier a été converti silencieusement en Float64 avant l'addition.
Julia a une notion de « largeur » des types numériques.
Int64 est plus large que Int16.Si une expression mélange plusieurs types, chacun est « promu » si nécessaire vers le type le plus large utilisé.
Pour forcer la conversion, on peut convertir un entier vers un type précis, comme dans Float64(5).
Sinon, il suffit d'utiliser float(5) et de laisser le compilateur choisir un type approprié.
Les conversions d'un flottant vers un entier sont forcément plus compliquées. Que veux-tu faire de ce qui suit la virgule ?
round() convertit vers l'entier le plus proche, les valeurs à mi-chemin comme 4.5 étant arrondies vers l'entier pair le plus proche.floor() arrondit vers le bas, ceil() vers le haut, trunc() vers zéro.Int32(), échouera avec une InexactError.Cependant, par défaut, ces fonctions ne renvoient pas le type entier que tu aurais peut-être souhaité. Le type de sortie souhaité peut être précisé.
julia> round(4.5)
4.0
julia> round(Int64, 4.5)
4
julia> round(Int, 4.5) # => default integer type
4
julia> ceil(Int16, 4.3)
5
Arrondir à un nombre précis de chiffres après la virgule est aussi possible avec le mot-clé digits.
julia> round(π, digits=10)
3.1415926536
Voir le manuel pour plus de détails.
Ça lève simplement une erreur, non ? En fait, la situation n'est pas si simple.
La division entière avec ÷ ou // donnera une erreur, comme on peut s'y attendre.
La division à virgule flottante avec / adopte ce qu'on pourrait considérer comme une approche d'ingénieur, plutôt qu'une approche informatique classique :
julia> 2 / 0
Inf
julia> 0 / 0
NaN
Comme nous l'avons vu dans une section précédente, l'infini est un nombre à virgule flottante valide en Julia, représenté par Inf.
Quand le numérateur est lui aussi nul, le résultat est mathématiquement indéfini.
Julia le traite alors comme un « n'est pas un nombre », représenté par NaN.
Si cela te semble étrange, pense au contexte du travail sur de grands tableaux de données du monde réel (et donc souvent assez désordonnées). Pour avancer, le mieux est de simplement signaler les valeurs problématiques et de passer à la suite.
Une vérification manuelle sans fin des valeurs serait pénible à programmer, et nuirait à coup sûr aux performances d'exécution.
S'arrêter avec un message d'erreur à la moindre anicroche rendrait ton programme très impopulaire auprès des utilisateurs !
Comme décrit dans le concept Conditionals, le test d'égalité se fait généralement avec l'opérateur ==.
Cela fonctionne bien pour les entiers, les caractères, les chaînes de caractères, etc.
Cependant, les valeurs à virgule flottante ont une précision limitée, et différentes façons de calculer un même résultat peuvent mener à des valeurs légèrement différentes.
Pour Float64, ce sera typiquement autour du 15e chiffre significatif : une petite différence, mais pas une « égalité ».
Traditionnellement, le conseil donné aux programmeurs est de ne jamais utiliser == avec des valeurs à virgule flottante : les résultats sont imprévisibles.
Une alternative très répandue consiste à comparer la valeur absolue de la différence à une tolérance autorisée (souvent appelée epsilon ou ϵ).
Ainsi, au lieu de a == b, on utilise abs(a - b) < epsilon.
Julia propose une alternative plus propre avec la fonction isapprox().
Pour une tolérance absolue, la syntaxe est isapprox(a, b, atol=epsilon).
Le mot-clé atol est requis dans ce cas.
La tolérance relative est souvent plus utile.
C'est la valeur par défaut, donc isapprox(a, b) essaiera de choisir une valeur raisonnable pour rtol (les règles sont assez compliquées).
On peut aussi préciser une tolérance relative, sous forme de fraction des valeurs comparées.
Ainsi, isapprox(a, b, rtol=0.01 vérifie que les valeurs sont à moins de 1 % l'une de l'autre.
Comme souvent avec Julia, il existe un opérateur mathématique synonyme du cas par défaut : a ≈ b (l'opérateur s'écrit \approx puis <tab>).
En plus des nombres rationnels, les parties suivantes du programme aborderont :
2.3 + 4.5im.