19. Bonus : à propos de la syntaxe▲
Un des objectifs de ce livre a été d'enseigner l'essentiel de Julia. Lorsque deux façons de procéder coexistaient, l'une a été choisie tout en évitant de mentionner l'autre. Parfois, la seconde méthode faisait partie d'un exercice.
À présent, revenons sur quelques éléments que nous avons négligés. Julia fournit un certain nombre de fonctionnalités pas toujours nécessaires. Il est possible d'écrire un bon code sans elles, mais leur usage permet parfois d'écrire un code plus concis, plus lisible, plus efficace. Ces trois qualités peuvent même se conjuguer.
Ce chapitre et le suivant abordent les éléments laissés en suspens dans les chapitres précédents :
- des suppléments relatifs à la syntaxe ;
-
les fonctions, types et macros directement disponibles dans
Base
; -
les fonctions, types et macros de la bibliothèque standard (Standard Library).
19-1. Tuples nommés▲
On peut désigner les composants d'un tuple en lui attribuant un nom :
2.
3.
4.
julia>
x =
(
a=
1
, b=
1
+
1
)
(
a =
1
, b =
2
)
julia>
x.a
1
Avec les tuples nommés, il devient possible d'accéder aux champs par leur nom en utilisant la syntaxe par point (x.a).
19-2. Fonctions▲
En Julia, les fonctions peuvent être définies à l'aide d'une syntaxe compacte :
2.
julia>
f(
x,y)
=
x +
y
f (
generic function
with 1
method)
19-2-1. Fonctions anonymes▲
Une fonction peut être définie de manière non nominative :
2.
3.
4.
5.
6.
julia>
x ->
x^
2
+
2
x -
1
#1 (generic function with 1 method)
julia>
function
(
x)
x^
2
+
2
x -
1
end
#3 (generic function with 1 method)
Ces exemples correspondent à des fonctions « anonymes ». Les fonctions anonymes sont souvent utilisées comme argument pour une autre fonction :
2.
julia>
using
Plots
julia>
plot(
x ->
x^
2
+
2
x -
1
, 0
, 10
, xlabel=
"x"
, ylabel=
"y"
)
La figure 19.2.1 montre le résultat de ces instructions.
19-2-2. Arguments nommés▲
Les arguments de fonctions peuvent également être nommés :
2.
3.
4.
5.
julia>
function
myplot(
x, y; style=
"solid"
, width=
1
, color=
"black"
)
###
end
myplot (
generic function
with 1
method)
julia>
myplot(
0
:10
, 0
:10
, style=
"dotted"
, color=
"blue"
)
Les arguments nommés dans une fonction sont spécifiés après un point-virgule dans la signature, mais peuvent être appelés avec une virgule.
19-2-3. Fermetures▲
Une fermeture est une technique qui permet à une fonction de capturer une variable définie en dehors du champ d'application de la fonction.
2.
3.
4.
5.
6.
7.
8.
julia>
foo(
x)
=
()
->
x
foo (
generic function
with 1
method)
julia>
bar =
foo(
1
)
#1 (generic function with 1 method)
julia>
bar()
1
Dans cet exemple, la fonction foo retourne une fonction anonyme qui a accès à l'argument x de la fonction foo. bar pointe sur la fonction anonyme et retourne la valeur de l'argument de foo.
19-3. Blocs▲
Un bloc est un moyen de regrouper un certain nombre de déclarations. Un bloc commence par le mot-clé begin
et se termine par end
.
La macro @svg
a été présentée dans le chapitre 4Étude de cas : Conception d'une interface :
2.
3.
4.
5.
6.
🐢 =
Turtle()
@svg
begin
forward(
🐢, 100
)
turn(
🐢, -
90
)
forward(
🐢, 100
)
end
Dans cet exemple, la macro @svg
possède un seul argument, c'est-à-dire un bloc regroupant trois appels de fonction.
19-3-1. Bloc let▲
Un bloc let
s'avère utile pour créer de nouvelles associations (ou ligatures), c'est-à-dire des variables locales pointant vers des valeurs.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
julia>
x, y, z =
-
1
, -
1
, -
1
;
julia>
let
x =
1
, z
@show
x y z;
end
x =
1
y =
-
1
ERROR: UndefVarError
: z not defined
julia>
@show
x y z;
x =
1
y =
-
1
z =
-
1
Dans cet exemple, la première macro @show
affiche x en local, y en global et z (indéfini) en local. Les variables globales ne sont pas touchées.
19-3-2. Blocs do▲
Dans la section 14.2Lire et écrire, nous avons montré qu'il fallait fermer les fichiers après avoir effectué les opérations d'écriture. En réalité, cela peut être exécuté automatiquement en utilisant un bloc do :
2.
3.
4.
5.
6.
julia>
data =
"Appréciez votre liberté,
\n
ou vous la perdrez!
\n
"
"Appréciez votre liberté,
\n
ou vous la perdrez!
\n
"
julia>
open(
"output.txt"
, "w"
)
do
fout
write(
fout, data)
end
47
Dans cet exemple(48), fout est le flux de fichiers utilisé pour la sortie. En termes de fonctionnement, ceci est équivalent à :
2.
3.
4.
5.
6.
julia>
f =
fout ->
begin
write(
fout, data)
end
#3 (generic function with 1 method)
julia>
open(
f, "output.txt"
, "w"
)
47
La fonction anonyme est utilisée comme premier argument de la fonction open :
2.
3.
4.
5.
6.
7.
8.
function
open(
f::
Function
, args...)
io =
open(
args...)
try
f(
io)
finally
close(
io)
end
end
Un bloc do
peut « capturer » des variables à l'extérieur de son périmètre d'application. Par exemple, dans l'exemple ci-dessus, la variable data de « open ... do » est capturée alors qu'elle figure comme variable globale.
19-4. Structure de contrôle▲
19-4-1. Opérateur ternaire▲
L'opérateur ternaire ?: est une alternative à une déclaration if
-elseif
lorsqu'un choix entre plusieurs valeurs d'une seule expression s'avère nécessaire.
2.
3.
4.
julia>
a =
150
150
julia>
a %
2
==
0
? println(
"even"
)
: println(
"odd"
)
even
L'expression précédant le signe ? désigne une condition. Si la condition est true
, l'expression précédant : est évaluée, sinon l'expression suivant : est exécutée.
19-4-2. Évaluation en court-circuit▲
Les opérateurs &&
et ||
effectuent une évaluation en mode court-circuit : l'argument qui les suit n'est évalué que si nécessaire pour déterminer la valeur finale.
Par exemple, une routine factorielle récursive pourrait être définie ainsi :
2.
3.
4.
5.
function
fact(
n::
Integer
)
n >=
0
||
error(
"n ne peut pas être negatif"
)
n ==
0
&&
return
1
n *
fact(
n-
1
)
end
19-4-3. Tâches ou coroutines▲
Une tâche est une structure de contrôle qui peut passer le contrôle de manière coopérative sans retour. En Julia, une tâche peut être implémentée comme une fonction ayant en premier argument un objet Channel. Un canal est utilisé pour transmettre des valeurs de la fonction à l'énoncé qui l'appelle.
La suite de Fibonacci peut être calculée à l'aide d'une tâche.
2.
3.
4.
5.
6.
7.
8.
9.
function
fib(
c::
Channel)
a =
0
b =
1
put!(
c, a)
while
true
put!(
c, b)
(
a, b)
=
(
b, a+
b)
end
end
put! enregistre les valeurs dans un objet canal et take! lit les valeurs à partir de celui-ci :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
julia>
fib_gen =
Channel(
fib)
;
julia>
take!(
fib_gen)
0
julia>
take!(
fib_gen)
1
julia>
take!(
fib_gen)
1
julia>
take!(
fib_gen)
2
julia>
take!(
fib_gen)
3
Le constructeur Channel crée la tâche. La fonction fib est suspendue après chaque appel put! et reprise après take! Pour des raisons de performance, plusieurs valeurs de la séquence sont mises en mémoire tampon dans l'objet canal pendant un cycle de reprise/suspension.
Un objet canal peut également être utilisé comme itérateur :
2.
3.
4.
5.
julia>
for
val in
Channel(
fib)
print(
val, " "
)
val >
20
&&
break
end
0
1
1
2
3
5
8
13
21
19-5. Types▲
19-5-1. Types primitifs▲
Un type spécifique (ou particulier) composé de bits est appelé un type primitif. Contrairement à la plupart des langages, Julia permet de déclarer nos propres types primitifs. Les types primitifs standard sont définis de la même manière :
2.
3.
4.
primitive type
Float64
<:
AbstractFloat
64
end
primitive type
Bool
<:
Integer
8
end
primitive type
Char
<:
AbstractChar 32
end
primitive type
Int64
<:
Signed
64
end
Ces déclarations précisent le nombre de bits requis.
L'exemple suivant crée un type primitif Byte et un constructeur :
2.
3.
4.
5.
6.
julia>
primitive type
Byte 8
end
julia>
Byte(
val::
UInt8
)
=
reinterpret(
Byte, val)
Byte
julia>
b =
Byte(
0x01
)
Byte(
0x01
)
La fonction reinterpret sert à enregistrer les bits d'un entier non signé de huit bits (UInt8
) dans l'octet.
19-5-2. Types paramétriques▲
Le système de types de Julia est paramétrique, ce qui signifie que les types peuvent avoir des paramètres.
Les paramètres des types sont introduits après le nom du type, entouré d'accolades :
2.
3.
4.
struct
Point{
T<:
Real
}
x::
T
y::
T
end
Ceci définit un nouveau type paramétrique, Point{
T<:
Real
}
, contenant deux « coordonnées » de type T, qui peut être n'importe quel type ayant Real
comme supertype.
2.
julia>
Point(
0.0
, 0.0
)
Point{
Float64
}
(
0.0
, 0.0
)
En plus des types composites, les types abstraits et les types primitifs peuvent également avoir un paramètre de type.
Pour des raisons de performance, il est vivement recommandé d'avoir des types spécifiques pour les champs de structure. C'est là une bonne méthode pour rendre Point à la fois rapide et flexible.
19-5-3. Unions de types▲
À l'image d'une structure, une union de types est un regroupement d’objets de types différents. À la différence d'une structure, une union de types ne peut contenir qu'un seul de ses membres à la fois :
2.
3.
4.
5.
6.
julia>
IntOrString =
Union
{
Int64
, String
}
Union
{
Int64
, String
}
julia>
150
::
IntOrString
150
julia>
"Julia"
::
IntOrString
"Julia"
Julia permet à ses utilisateurs de tirer parti des unions, car un code efficace est produit en terme d'utilisation de la mémoire. L’accès aux champs d’une union est simplifié lorsque l'union est rendue « anonyme » au sein d’une autre structure ou d’une autre union.
19-6. Méthodes▲
19-6-1. Méthodes paramétriques▲
Les définitions de méthodes peuvent également comporter des paramètres de type caractérisant leur signature :
2.
3.
4.
5.
6.
julia>
isintpoint(
p::
Point{
T}
)
where
{
T}
=
(
T ===
Int64
)
isintpoint (
generic function
with 1
method)
julia>
p =
Point(
1
, 2
)
Point{
Int64
}
(
1
, 2
)
julia>
isintpoint(
p)
true
19-6-2. Objets foncteurs▲
En Julia, tous les objets sont « appelables ». Ces objets « appelables » sont des foncteurs (functors), c'est-à-dire des objets qui peuvent être traités à la manière d'une fonction.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
struct
Polynomial{
R}
coeff::
Vector{
R}
end
function
(
p::
Polynomial)(
x)
val =
p.coeff[end
]
for
coeff in
p.coeff[end
-
1
:-1:1
]
val =
val *
x +
coeff
end
val
end
Pour évaluer le polynôme :
2.
3.
4.
julia>
p =
Polynomial(
[1
,10
,100
])
Polynomial{
Int64
}
(
[1
, 10
, 100
])
julia>
p(
3
)
931
19-7. Constructeurs▲
Les types paramétriques peuvent être construits explicitement ou implicitement :
2.
3.
4.
5.
6.
julia>
Point(
1
,2
)
# implicit T
Point{
Int64
}
(
1
, 2
)
julia>
Point{
Int64
}
(
1
, 2
)
# explicit T
Point{
Int64
}
(
1
, 2
)
julia>
Point(
1
,2.5
)
# implicit T
ERROR: MethodError
: no method matching Point(
::
Int64
, ::
Float64
)
Des constructeurs internes et externes par défaut sont générés pour chaque T :
2.
3.
4.
5.
6.
7.
struct
Point{
T<:
Real
}
x::
T
y::
T
Point{
T}
(
x,y)
where
{
T<:
Real
}
=
new
(
x,y)
end
Point(
x::
T, y::
T)
where
{
T<:
Real
}
=
Point{
T}
(
x,y)
;
x et y doivent tous deux être du même type.
Lorsque x et y ont un type différent, le constructeur extérieur peut être défini comme suit :
Point(
x::
Real
, y::
Real
)
=
Point(
promote(
x,y)
...)
;
La fonction promote est traitée dans la sous-section 19.8.2Promotion.
19-8. Conversions et promotions▲
Julia dispose d'un système permettant de promouvoir des arguments de différentes sortes en un type commun. Bien qu'elle ne soit pas automatique, la promotion peut facilement être effectuée.
19-8-1. Conversion▲
Une valeur peut être convertie d'un type vers un autre :
2.
3.
4.
5.
6.
7.
8.
julia>
x =
12
12
julia>
typeof(
x)
Int64
julia>
convert(
UInt8
, x)
0x0c
julia>
typeof(
ans)
UInt8
Nous pouvons ajouter nos propres méthodes de conversion :
2.
3.
4.
julia>
Base.convert(
::
Type{
Point{
T}}
, x::
Array
{
T, 1
}
)
where
{
T<:
Real
}
=
Point(
x...)
julia>
convert(
Point{
Int64
}
, [1
, 2
])
Point{
Int64
}
(
1
, 2
)
19-8-2. Promotion▲
La promotion est la conversion des valeurs de types mixtes en un seul type commun :
2.
julia>
promote(
1
, 2.5
, 3
)
(
1.0
, 2.5
, 3.0
)
Les méthodes pour la fonction de promotion ne sont normalement pas directement définies. Cependant, la fonction auxiliaire promote_rule est utilisée pour spécifier les règles de promotion :
promote_rule(
::
Type{
Float64
}
, ::
Type{
Int32
}
)
=
Float64
19-9. Métaprogrammation▲
Le code Julia peut être représenté comme une structure de données du langage lui-même. Cela permet à un programme de transformer et de produire son propre code.
19-9-1. Expressions▲
En Julia, chaque programme commence par une chaîne :
2.
julia>
prog =
"1 + 2"
"1 + 2"
L'étape suivante consiste à analyser chaque chaîne de caractères en un objet appelé expression, représenté par le type Expr :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
julia>
ex =
Meta.parse(
prog)
:(
1
+
2
)
julia>
typeof(
ex)
Expr
julia>
dump(
ex)
Expr
head: Symbol call
args: Array
{
Any
}
((
3
,))
1
: Symbol +
2
: Int64
1
3
: Int64
2
La fonction dump affiche les objets Expr avec des annotations.
Les expressions sont construites directement avec le préfixe : suivi de parenthèses ou bien en utilisant un bloc quote :
2.
3.
julia>
ex =
quote
1
+
2
end
;
19-9-1-1. eval▲
Julia est à même d'évaluer un objet d'expression en utilisant eval :
2.
julia>
eval(
ex)
3
Chaque module a sa propre fonction d'évaluation qui calcule les expressions dans son champ d'application.
Lorsque le nombre de recours à la fonction eval est élevé, cela signifie souvent qu'un programme est mal conçu. L'usage d'eval est considéré comme une « mauvaise pratique ».
19-9-2. Macros▲
Les macros peuvent inclure le code produit dans un programme. Une macro associe un ensemble d'objets Expr directement à une expression compilée.
Voici une macro simple :
2.
macro
containervariable(
container, element)
return
esc(
:(
$
(
Symbol(
container,element))
=
$
container[$
element]))
end
Les macros sont appelées en préfixant leur nom par le symbole @. L'appel de macro @containervariable
letters 1
est remplacé par :
:(
letters1 =
letters[1
])
@macroexpand
@containervariable
letters 1
retourne cette expression, qui se révèle extrêmement utile pour le débogage.
Cet exemple illustre comment une macro accède au nom de ses arguments, ce qu'une fonction ne peut pas faire. L'expression retournée nécessite l'usage d'un échappement avec esc, du fait qu'elle doit être résolue dans l'environnement d'appel de la macro.
Pourquoi utiliser des macros ? Les macros produisent et incluent des fragments de code personnalisé pendant que l'analyse a lieu, c'est-à-dire avant que le programme complet ne soit exécuté.
19-9-3. Fonctions générées▲
La macro @generated
crée un code spécialisé pour les méthodes en fonction des types d'arguments :
2.
3.
4.
@generated
function
square(
x)
println(
x)
:(
x *
x)
end
Le corps retourne une expression citée comme une macro.
Pour l'appelant, la fonction générée se comporte comme une fonction régulière :
2.
3.
4.
5.
6.
7.
8.
julia>
x =
square(
2
)
; # note: sortie après l'instruction println() dans le corps
Int64
julia>
x # maintenant nous écrivons x
4
julia>
y =
square(
"bon"
)
;
String
julia>
y
"bonbon"
19-10. Valeurs manquantes▲
Les valeurs manquantes peuvent être représentées via l'objet missing
, qui est l'instance unique du type Missing
.
Les tableaux sont susceptibles de contenir des valeurs manquantes :
2.
3.
4.
julia>
a =
[1
, missing
]
2
-
element Array
{
Union
{
Missing
, Int64
}
,1
}
:
1
missing
Le type d'élément d'un tel tableau est Union
{
Missing
, T}
avec T le type des valeurs non manquantes.
Les fonctions de réduction retournent les valeurs manquantes lorsqu'elles sont invoquées sur des tableaux qui contiennent des valeurs manquantes :
2.
julia>
sum(
a)
missing
Dans ce cas, il convient d'employer la fonction skpimissing pour passer outre les valeurs manquantes :
2.
julia>
sum(
skipmissing(
[1
, missing
]))
1
19-11. Appel de code C et Fortran▲
Une quantité considérable de code est écrite en C ainsi qu'en Fortran. Réutiliser du code testé est une meilleure pratique que d'écrire sa propre version d'un algorithme. Julia peut appeler directement des bibliothèques C ou Fortran existantes en utilisant la syntaxe ccall.
Dans la section 14.6Bases de données (relative aux bases de données), nous avons introduit une interface Julia à la bibliothèque GDBM des fonctions de base de données. La bibliothèque est écrite en C. Pour fermer la base de données, un appel de fonction à close(
db)
doit être effectué :
2.
3.
4.
5.
Base.close(
dbm::
DBM)
=
gdbm_close(
dbm.handle)
function
gdbm_close(
handle::
Ptr
{
Cvoid}
)
ccall((
::
gdbm_close, "libgdbm"
)
, Cvoid, (
Ptr
{
Cvoid}
,)
, handle)
end
Un objet dbm a un champ handle de type Ptr
{
Cvoid}
. Ce champ contient un pointeur C qui fait référence à la base de données. Pour fermer la base de données, la fonction C gdbm_close doit être invoquée en ayant comme seul argument le pointeur C pointant vers la base de données et aucune valeur de retour. Julia effectue cela directement avec la fonction ccall ayant comme arguments :
-
un tuple consistant en un symbole contenant le nom de la fonction que nous voulons appeler :
:gdbm_close
et la bibliothèque partagée spécifiée sous forme de chaîne :"libgdm"
; -
le type de retour : Cvoid ;
- un tuple des types d'arguments :
(
Ptr
{
Cvoid}
,)
; -
les valeurs de l'argument : handle.
La cartographie complète de la bibliothèque GDBM peut être trouvée à titre d'exemple dans les sources de ThinkJulia.
19-12. Glossaire▲
fermeture fonction qui saisit les variables dans sa zone de portée.
bloc let
bloc créant de nouvelles associations (ou ligatures) de variables locales vers des valeurs.
fonction anonyme fonction définie de manière non nominative.
tuple nommé tuple dont les composants sont nommés.
arguments nommés arguments identifiables par leur nom, c'est-à-dire pas seulement par leur position.
bloc do
construction syntaxique utilisée pour définir et appeler une fonction anonyme similaire à un bloc de code usuel.
opérateur ternaire opérateur de flux de contrôle prenant trois opérandes : une condition, une expression à exécuter lorsque la condition retourne true
et une autre expression à exécuter lorsque la condition retourne false
.
évaluation en court-circuit évaluation d'un opérateur booléen pour lequel le deuxième argument est exécuté ou évalué, uniquement si le premier argument ne suffit pas à déterminer la valeur de l'expression.
tâche (ou coroutine) fonction de contrôle du flux qui permet la suspension et la reprise des calculs de manière flexible.
type primitif type spécifique dont les données sont constituées de simples bits.
type union type qui inclut comme objets toutes les instances de l'un ou l'autre de ses paramètres de type.
type paramétrique type qui possède des paramètres (autrement dit : type paramétré).
foncteur objet avec une méthode associée lui permettant d'être invoqué.
conversion procédé permettant de convertir une valeur d'un type en un autre.
promotion conversion des valeurs de types différents en un seul type commun.
expression type Julia qui contient une construction linguistique.
macro procédé permettant d'inclure du code généré dans le corps final d'un programme.
fonctions générées fonctions capables de produire un code spécialisé selon les types d'arguments.
valeurs manquantes instances qui représentent des données sans qu'une valeur leur soit attribuée.