Jusqu'ici, le programme n'a pas dit grand-chose sur les types, mais ils existent de toute évidence en Julia :
julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')
julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}
On n'a jamais précisé les types, mais Julia les a attribués quand même.
types, qui sont au cœur de sa conception.Type Inference.Le compilateur JIT parcourt (tout) le code, voit comment une variable est utilisée, et déduit un type par défaut adapté, compatible avec cet usage.
Les concepts précédents ont donné de nombreux exemples où des types numériques sont ramenés à une forme uniforme, à commencer par l'arithmétique la plus simple.
julia> 2 + 1.3
3.3
On a additionné un entier (Int64) et un nombre à virgule flottante (Float64), et le résultat est un Float64. De même :
julia> nums = (3, 4.1, 1//4)
(3, 4.1, 1//4)
julia> typeof(nums)
Tuple{Int64, Float64, Rational{Int64}}
julia> [nums...]
3-element Vector{Float64}:
3.0
4.1
0.25
Dans le cas ci-dessus, un tuple a conservé le type de chaque élément, mais la conversion en vecteur les a tous ramenés à un Float64 uniforme.
Le compilateur Julia sait quelles conversions de types sont possibles : d'entier vers flottant, aucun problème ; de flottant vers entier, on perd de la précision, donc une InexactError est levée.
La promotion de type convertit toutes les valeurs de l'expression vers un type commun, suffisamment souple pour être compatible avec toutes les entrées.
On peut effectuer la même conversion explicitement avec la fonction promote() :
julia> promote(nums...)
(3.0, 4.1, 0.25)
Se reposer sur l'inférence de type et la promotion de type suffit pour résoudre de simples exercices d'apprentissage, mais pour des programmes plus gros, tu auras probablement besoin d'un contrôle plus précis.
Sur la plupart des processeurs modernes, un entier a par défaut le type Int64.
On a vu dans le concept Numbers qu'on peut convertir une valeur vers un type particulier, autre que celui par défaut.
julia> x = Int16(42)
42
julia> typeof(x)
Int16
Cependant, la variable x peut toujours être réaffectée à un type différent :
julia> x = "changed"
"changed"
julia> typeof(x)
String
C'est ce qu'on appelle l'type instability, à savoir :
À la place, on peut fixer le type de x en utilisant l'opérateur :: :
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> y = "changed"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int16
The function `convert` exists, but no method is defined for this combination of argument types.
Désormais, y est de type Int16, et le restera toujours.
Ainsi, le compilateur sait combien d'octets réserver pour elle, et peut optimiser le reste du code en s'appuyant sur un type stable.
En cela, la variable ressemble plus ou moins à celles d'un langage à typage statique comme le C.
L'affectation de type, décrite ci-dessus, s'utilise à gauche d'une affectation de variable pour contraindre le type de cette variable.
Utiliser l'opérateur :: avec une valeur, ou avec quelque chose qui s'évalue en une valeur, constitue généralement une assertion : la valeur doit être de ce type, faute de quoi une erreur est levée.
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
On l'utilise souvent avec la valeur de retour d'une fonction, comme ultime vérification simple que la fonction s'est comportée comme prévu.
À noter qu'il existe une macro @assert pour d'autres formes d'assertion.
Int64, Int16, String, Char : d'où viennent ces types ?
Dans de nombreux langages orientés objet (OO), chaque type est une classe, la dérivation de sous-classes les range en une hiérarchie de classes, et les méthodes de classe définissent les comportements.
Java et Ruby sont des exemples évidents de ce schéma, mais même Python fonctionne de façon similaire en interne.
Julia n'a pas de classes.
La raison documentée est que les fonctionnalités orientées objet interfèrent avec le compilateur JIT et nuisent aux performances à l'exécution.
Va savoir, peut-être avaient-ils aussi lu cette citation :
« La programmation orientée objet est une idée exceptionnellement mauvaise, qui n'a pu voir le jour qu'en Californie. »
Elle est attribuée à Edsger Dijkstra, un informaticien brillant pendant plusieurs décennies à partir des années 1950 (même s'il n'était pas réputé pour son optimisme ensoleillé ni pour sa diplomatie subtile).
Et pourtant, regarde ce code :
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> supertype(Int16)
Signed
julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)
En détaillant un peu :
Int16 est un type, et on peut créer des variables de ce type.Int16 est un sous-type de Signed, et la fonction supertype() nous le montre.Integer, Real et Number jusqu'à Any au sommet, et subtypes() nous liste cette branche de la hiérarchie.Toutes les branches aboutissent à Any, qui a la particularité d'être son propre supertype.
julia> supertypes(String)
(String, AbstractString, Any)
julia> supertype(Any)
Any
Ainsi, Julia n'a pas de hiérarchie de class, mais elle possède bel et bien une hiérarchie de type.
Essayer d'afficher la hiérarchie entière donne un arbre gigantesque, impossible à vraiment visualiser.
En examiner des parties est une chose dont on discute en ligne.
Nous finirons par décortiquer tout ça, mais il reste bien des choses à explorer avant.
On peut utiliser typeof() pour tester l'égalité de la manière habituelle.
julia> typeof(11)
Int64
julia> typeof(11) == Int64
true
julia> typeof(11) == Number
false
L'égalité de type doit être exacte, car cette forme de comparaison ne comprend rien à la hiérarchie des types.
Plus souplement, isa nous dit si une valeur a le même type que le comparateur, ou un sous-type de celui-ci.
On peut l'utiliser sous forme infixe ou sous forme de fonction.
julia> 12 isa Int64
true
julia> 12 isa Number
true
julia> isa(12, Number)
true
julia> 12 isa String
false
À noter que isa attend une value à gauche, pas un type.
Comparer deux types de cette façon donnera des résultats inattendus.
Le bon opérateur est <:, que nous reverrons beaucoup dans les prochains concepts.
julia> Int64 isa Number ## Don't do this!
false
julia> Int64 <: Number
true
Tester les types peut servir à contrôler le flux dans une fonction, mais c'est relativement rare en Julia idiomatique.
Nous verrons dans le concept Multiple Dispatch qu'il est souvent plus efficace d'ajouter des types aux arguments d'une fonction et de laisser le mécanisme de dispatch de Julia gérer cette logique. Cependant, plusieurs autres concepts liés aux types restent à aborder avant d'arriver à Multiple Dispatch.
On a vu que la hiérarchie des types forme une arborescence (au sens informatique, avec la racine en haut).
Chaque élément de l'arbre est un node, et on peut les répartir en catégories :
abstract.concrete.julia> subtypes(Integer) # an abstract type
3-element Vector{Any}:
Bool
Signed
Unsigned
julia> subtypes(Int64) # a concrete type
Type[]
C'est une distinction importante, car seuls les types concrets peuvent être instantiated en variables.
julia> a::Int16 = 42
42
julia> typeof(a)
Int16
julia> b::Integer = 42
42
julia> typeof(b)
Int64
À noter qu'utiliser un type abstrait ne donne aucun message d'erreur (dans ce cas), mais le compilateur crée un type concret approprié : Int64 au lieu de Integer.
Au moins, cela empêche d'affecter à la variable une valeur non entière :
julia> f::Integer = "hello"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
The function `convert` exists, but no method is defined for this combination of argument types.
L'affectation de type avec un type abstrait est donc une contrainte sur le type de la variable, qui doit être un sous-type de Integer (dans le cas ci-dessus).
C'est chose inhabituelle dans le monde des langages de programmation : plus faible qu'une affectation de type en C, plus forte qu'une indication de type dans les versions récentes de Python.
Les fonctions isabstracttype() et isconcretetype() permettent de tester.
À noter que ce ne sont pas simplement les négations l'une de l'autre : nous verrons dans un concept ultérieur que certains types ne sont ni abstraits ni concrets.
julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)
julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)
# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)
Bien que Vector ne soit pas un type concret, ses éléments le sont.
La fonction eltype() (type des éléments) extrait ce type :
julia> eltype([1, 2.3])
Float64