IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Think Julia


précédentsommairesuivant

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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
julia> x -> x^2 + 2x - 1
#1 (generic function with 1 method)
julia> function (x)
           x^2 + 2x - 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 :

 
Sélectionnez
1.
2.
julia> using Plots
julia> plot(x -> x^2 + 2x - 1, 0, 10, xlabel="x", ylabel="y")

La figure 19.2.1 montre le résultat de ces instructions.

Image non disponible

FIGURE 19.2.1 – Graphique de la fonction anonyme x-> x² + 2x -1 avec Plots.

19-2-2. Arguments nommés

Les arguments de fonctions peuvent également être nommés :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
julia> data = "Appréciez votre liberté,\nou vous la perdrez!\n"
"Appréciez votre liberté,\nou 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 à :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
:(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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
2.
julia> sum(a)
missing

Dans ce cas, il convient d'employer la fonction skpimissing pour passer outre les valeurs manquantes :

 
Sélectionnez
1.
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é :

 
Sélectionnez
1.
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.


précédentsommairesuivant
Citation de Richard M. Stallman.

Licence Creative Commons
Le contenu de cet article est rédigé par Thierry Lepoint et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2021 Developpez.com.