Style Guide
Les sections suivantes expliquent quelques aspects du style de codage idiomatique en Julia. Aucune de ces règles n'est absolue ; ce ne sont que des suggestions pour vous aider à vous familiariser avec le langage et à vous aider à choisir parmi des conceptions alternatives.
Indentation
Utilisez 4 espaces par niveau d'indentation.
Write functions, not just scripts
Écrire du code sous forme de séries d'étapes au niveau supérieur est un moyen rapide de commencer à résoudre un problème, mais vous devriez essayer de diviser un programme en fonctions dès que possible. Les fonctions sont plus réutilisables et testables, et clarifient quelles étapes sont effectuées et quels sont leurs entrées et sorties. De plus, le code à l'intérieur des fonctions a tendance à s'exécuter beaucoup plus rapidement que le code au niveau supérieur, en raison du fonctionnement du compilateur de Julia.
Il convient également de souligner que les fonctions doivent prendre des arguments, au lieu d'opérer directement sur des variables globales (à l'exception des constantes comme pi
).
Avoid writing overly-specific types
Le code doit être aussi générique que possible. Au lieu d'écrire :
Complex{Float64}(x)
il est préférable d'utiliser les fonctions génériques disponibles :
complex(float(x))
La deuxième version convertira x
en un type approprié, au lieu d'utiliser toujours le même type.
Ce point de style est particulièrement pertinent pour les arguments de fonction. Par exemple, ne déclarez pas un argument comme étant de type Int
ou Int32
s'il peut vraiment être n'importe quel entier, exprimé avec le type abstrait Integer
. En fait, dans de nombreux cas, vous pouvez omettre complètement le type d'argument, à moins qu'il ne soit nécessaire pour disambiguïser d'autres définitions de méthode, puisque MethodError
sera de toute façon levé si un type est passé qui ne prend pas en charge l'une des opérations requises. (Ceci est connu sous le nom de duck typing.)
Par exemple, considérez les définitions suivantes d'une fonction addone
qui retourne un plus son argument :
addone(x::Int) = x + 1 # works only for Int
addone(x::Integer) = x + oneunit(x) # any integer type
addone(x::Number) = x + oneunit(x) # any numeric type
addone(x) = x + oneunit(x) # any type supporting + and oneunit
La dernière définition de addone
gère tout type prenant en charge oneunit
(qui retourne 1 dans le même type que x
, ce qui évite une promotion de type indésirable) et la fonction +
avec ces arguments. La chose clé à réaliser est qu'il n'y a aucun coût de performance à définir uniquement le général addone(x) = x + oneunit(x)
, car Julia compilera automatiquement des versions spécialisées au besoin. Par exemple, la première fois que vous appelez addone(12)
, Julia compilera automatiquement une fonction addone
spécialisée pour les arguments x::Int
, avec l'appel à oneunit
remplacé par sa valeur en ligne 1
. Par conséquent, les trois premières définitions de addone
ci-dessus sont complètement redondantes avec la quatrième définition.
Handle excess argument diversity in the caller
Au lieu de :
function foo(x, y)
x = Int(x); y = Int(y)
...
end
foo(x, y)
utiliser :
function foo(x::Int, y::Int)
...
end
foo(Int(x), Int(y))
C'est un meilleur style car foo
n'accepte pas vraiment des nombres de tous types ; il a vraiment besoin de Int
.
Un problème ici est que si une fonction nécessite intrinsèquement des entiers, il pourrait être préférable de forcer l'appelant à décider comment les non-entiers doivent être convertis (par exemple, arrondi à l'entier inférieur ou supérieur). Un autre problème est que déclarer des types plus spécifiques laisse plus d'"espace" pour de futures définitions de méthodes.
Append !
to names of functions that modify their arguments
Au lieu de :
function double(a::AbstractArray{<:Number})
for i in eachindex(a)
a[i] *= 2
end
return a
end
utiliser :
function double!(a::AbstractArray{<:Number})
for i in eachindex(a)
a[i] *= 2
end
return a
end
Julia Base utilise cette convention tout au long et contient des exemples de fonctions avec des formes de copie et de modification (par exemple, sort
et sort!
), et d'autres qui ne font que modifier (par exemple, push!
, pop!
, splice!
). Il est typique que de telles fonctions retournent également le tableau modifié pour plus de commodité.
Les fonctions liées à l'IO ou utilisant des générateurs de nombres aléatoires (RNG) sont des exceptions notables : Étant donné que ces fonctions doivent presque invariablement muter l'IO ou le RNG, les fonctions se terminant par !
sont utilisées pour signifier une mutation autre que la mutation de l'IO ou l'avancement de l'état du RNG. Par exemple, rand(x)
mute le RNG, tandis que rand!(x)
mute à la fois le RNG et x
; de même, read(io)
mute io
, tandis que read!(io, x)
mute les deux arguments.
Avoid strange type Union
s
Des types tels que Union{Function,AbstractString}
sont souvent un signe qu'un certain design pourrait être plus clair.
Avoid elaborate container types
Il n'est généralement pas très utile de construire des tableaux comme les suivants :
a = Vector{Union{Int,AbstractString,Tuple,Array}}(undef, n)
Dans ce cas, Vector{Any}(undef, n)
est préférable. Il est également plus utile pour le compilateur d'annoter des utilisations spécifiques (par exemple, a[i]::Int
) que d'essayer de regrouper de nombreuses alternatives dans un seul type.
Prefer exported methods over direct field access
Le code idiomatique en Julia devrait généralement traiter les méthodes exportées d'un module comme l'interface de ses types. Les champs d'un objet sont généralement considérés comme des détails d'implémentation et le code utilisateur ne devrait y accéder directement que si cela est indiqué comme faisant partie de l'API. Cela présente plusieurs avantages :
- Les développeurs de packages ont plus de liberté pour modifier l'implémentation sans casser le code des utilisateurs.
- Les méthodes peuvent être passées à des constructions d'ordre supérieur comme
map
(par exemplemap(imag, zs)
) plutôt que[z.im for z in zs]
). - Les méthodes peuvent être définies sur des types abstraits.
- Les méthodes peuvent décrire une opération conceptuelle qui peut être partagée entre des types disparates (par exemple,
real(z)
fonctionne sur des nombres complexes ou des quaternions).
Le système de dispatch de Julia encourage ce style car play(x::MyType)
ne définit la méthode play
que pour ce type particulier, laissant aux autres types le soin d'avoir leur propre implémentation.
De même, les fonctions non exportées sont généralement internes et sujettes à des modifications, sauf indication contraire dans la documentation. Les noms reçoivent parfois un préfixe (ou suffixe) _
pour suggérer davantage que quelque chose est "interne" ou un détail d'implémentation, mais ce n'est pas une règle.
Les contre-exemples à cette règle incluent NamedTuple
, RegexMatch
, StatStruct
.
Use naming conventions consistent with Julia base/
- les modules et les noms de type utilisent la capitalisation et le camel case :
module SparseArrays
,struct UnitRange
. - lesfonctionssontenminuscules(
maximum
,convert
) et, lorsqu'ellessontlisibles, avecplusieursmotscollésensemble(isequal
,haskey
). Lorsquec'estnécessaire, utilisezdesunderscorescommeséparateursdemots. Lesunderscoressontégalementutiliséspourindiquerunecombinaisondeconcepts(remotecall_fetch
commeuneimplémentationplusefficacedefetch(remotecall(...))
) oucomme_modificateurs. - les fonctions modifiant au moins un de leurs arguments se terminent par
!
. - la concision est valorisée, mais évitez les abréviations (
indexin
plutôt queindxin
) car il devient difficile de se souvenir si et comment certains mots sont abrégés.
Si un nom de fonction nécessite plusieurs mots, envisagez s'il pourrait représenter plus d'un concept et pourrait être mieux divisé en morceaux.
Write functions with argument ordering similar to Julia Base
En règle générale, la bibliothèque de base utilise l'ordre suivant des arguments pour les fonctions, le cas échéant :
Argument de fonction. Mettre un argument de fonction en premier permet d'utiliser des blocs
do
pour passer des fonctions anonymes multilignes.Flux I/O. Spécifier d'abord l'objet
IO
permet de passer la fonction à des fonctions telles quesprint
, par exemplesprint(show, x)
.Entrée en mutation. Par exemple, dans
fill!(x, v)
,x
est l'objet en mutation et il apparaît avant la valeur à insérer dansx
.Type. Passer un type signifie généralement que la sortie aura le type donné. Dans
parse(Int, "1")
, le type apparaît avant la chaîne à analyser. Il existe de nombreux exemples où le type apparaît en premier, mais il est utile de noter que dansread(io, String)
, l'argumentIO
apparaît avant le type, ce qui est conforme à l'ordre décrit ici.L'entrée n'est pas mutée. Dans
fill!(x, v)
,v
n'est pas muté et il vient aprèsx
.Clé. Pour les collections associatives, il s'agit de la clé de la ou des paires clé-valeur. Pour d'autres collections indexées, il s'agit de l'index.
Valeur. Pour les collections associatives, c'est la valeur de la ou des paires clé-valeur. Dans des cas comme
fill!(x, v)
, c'estv
.Tout le reste. Tous les autres arguments.
Varargs. Cela fait référence à des arguments qui peuvent être listés indéfiniment à la fin d'un appel de fonction. Par exemple, dans
Matrix{T}(undef, dims)
, les dimensions peuvent être données sous la forme d'unTuple
, par exempleMatrix{T}(undef, (1,2))
, ou sous la forme deVararg
, par exempleMatrix{T}(undef, 1, 2)
.Arguments de mot-clé. Dans Julia, les arguments de mot-clé doivent de toute façon venir en dernier dans les définitions de fonction ; ils sont listés ici pour des raisons de complétude.
La grande majorité des fonctions ne prendra pas tous les types d'arguments énumérés ci-dessus ; les numéros ne font que désigner la priorité qui doit être utilisée pour les arguments applicables à une fonction.
Il y a bien sûr quelques exceptions. Par exemple, dans convert
, le type doit toujours venir en premier. Dans setindex!
, la valeur vient avant les indices afin que les indices puissent être fournis en tant que varargs.
Lors de la conception d'APIs, respecter cet ordre général autant que possible est susceptible d'offrir aux utilisateurs de vos fonctions une expérience plus cohérente.
Don't overuse try-catch
Il vaut mieux éviter les erreurs que de compter sur leur détection.
Don't parenthesize conditions
Julia n'exige pas de parenthèses autour des conditions dans if
et while
. Écrivez :
if a == b
au lieu de :
if (a == b)
Don't overuse ...
La fonction de découpage des arguments peut être addictive. Au lieu de [a..., b...]
, utilisez simplement [a; b]
, qui concatène déjà les tableaux. collect(a)
est meilleur que [a...]
, mais puisque a
est déjà itérable, il est souvent encore mieux de le laisser tel quel et de ne pas le convertir en tableau.
Ensure constructors return an instance of their own type
When a method T(x)
is called on a type T
, it is generally expected to return a value of type T. Defining a constructor that returns an unexpected type can lead to confusing and unpredictable behavior:
julia> struct Foo{T}
x::T
end
julia> Base.Float64(foo::Foo) = Foo(Float64(foo.x)) # Do not define methods like this
julia> Float64(Foo(3)) # Should return `Float64`
Foo{Float64}(3.0)
julia> Foo{Int}(x) = Foo{Float64}(x) # Do not define methods like this
julia> Foo{Int}(3) # Should return `Foo{Int}`
Foo{Float64}(3.0)
Pour maintenir la clarté du code et garantir la cohérence des types, concevez toujours les constructeurs pour qu'ils retournent une instance du type qu'ils sont censés construire.
Don't use unnecessary static parameters
Une signature de fonction :
foo(x::T) where {T<:Real} = ...
devrait être écrit comme :
foo(x::Real) = ...
au lieu de cela, surtout si T
n'est pas utilisé dans le corps de la fonction. Même si T
est utilisé, il peut être remplacé par typeof(x)
si cela est pratique. Il n'y a pas de différence de performance. Notez que ce n'est pas un avertissement général contre les paramètres statiques, mais simplement contre les utilisations où ils ne sont pas nécessaires.
Notez également que les types de conteneurs, en particulier, peuvent nécessiter des paramètres de type dans les appels de fonction. Consultez la FAQ Avoid fields with abstract containers pour plus d'informations.
Avoid confusion about whether something is an instance or a type
Des ensembles de définitions comme les suivants sont déroutants :
foo(::Type{MyType}) = ...
foo(::MyType) = foo(MyType)
Décidez si le concept en question sera écrit comme MyType
ou MyType()
, et tenez-vous-en à cela.
Le style préféré est d'utiliser des instances par défaut, et d'ajouter uniquement des méthodes impliquant Type{MyType}
plus tard si cela devient nécessaire pour résoudre certains problèmes.
Si un type est effectivement une énumération, il doit être défini comme un type unique (idéalement un struct ou un type primitif immuable), les valeurs de l'énumération étant des instances de celui-ci. Les constructeurs et les conversions peuvent vérifier si les valeurs sont valides. Ce design est préféré à celui qui consiste à faire de l'énumération un type abstrait, avec les "valeurs" comme sous-types.
Don't overuse macros
Soyez conscient de quand un macro pourrait vraiment être une fonction à la place.
Appeler eval
à l'intérieur d'une macro est un signe d'avertissement particulièrement dangereux ; cela signifie que la macro ne fonctionnera que lorsqu'elle est appelée au niveau supérieur. Si une telle macro est écrite comme une fonction à la place, elle aura naturellement accès aux valeurs d'exécution dont elle a besoin.
Don't expose unsafe operations at the interface level
Si vous avez un type qui utilise un pointeur natif :
mutable struct NativeType
p::Ptr{UInt8}
...
end
ne pas écrire de définitions comme celles-ci :
getindex(x::NativeType, i) = unsafe_load(x.p, i)
Le problème est que les utilisateurs de ce type peuvent écrire x[i]
sans réaliser que l'opération est dangereuse, et peuvent alors être sujets à des bugs de mémoire.
Une telle fonction devrait soit vérifier l'opération pour s'assurer qu'elle est sûre, soit avoir unsafe
quelque part dans son nom pour alerter les appelants.
Don't overload methods of base container types
Il est possible d'écrire des définitions comme les suivantes :
show(io::IO, v::Vector{MyType}) = ...
Cela fournirait une présentation personnalisée des vecteurs avec un type d'élément nouveau spécifique. Bien que cela soit tentant, cela devrait être évité. Le problème est que les utilisateurs s'attendront à ce qu'un type bien connu comme Vector()
se comporte d'une certaine manière, et une personnalisation excessive de son comportement peut rendre son utilisation plus difficile.
Avoid type piracy
"Le piratage de type" fait référence à la pratique d'étendre ou de redéfinir des méthodes dans Base ou d'autres packages sur des types que vous n'avez pas définis. Dans des cas extrêmes, vous pouvez faire planter Julia (par exemple, si votre extension ou redéfinition de méthode entraîne le passage d'une entrée invalide à un ccall
). Le piratage de type peut compliquer le raisonnement sur le code et peut introduire des incompatibilités difficiles à prévoir et à diagnostiquer.
Par exemple, supposons que vous souhaitiez définir la multiplication sur des symboles dans un module :
module A
import Base.*
*(x::Symbol, y::Symbol) = Symbol(x,y)
end
Le problème est que maintenant tout autre module qui utilise Base.*
verra également cette définition. Étant donné que Symbol
est défini dans Base et est utilisé par d'autres modules, cela peut modifier le comportement de code non lié de manière inattendue. Il existe plusieurs alternatives ici, y compris l'utilisation d'un nom de fonction différent ou l'encapsulation des Symbol
s dans un autre type que vous définissez.
Parfois, des paquets couplés peuvent s'engager dans la piraterie de type pour séparer les fonctionnalités des définitions, en particulier lorsque les paquets ont été conçus par des auteurs collaborant, et lorsque les définitions sont réutilisables. Par exemple, un paquet pourrait fournir certains types utiles pour travailler avec les couleurs ; un autre paquet pourrait définir des méthodes pour ces types qui permettent des conversions entre les espaces colorimétriques. Un autre exemple pourrait être un paquet qui agit comme un mince wrapper pour du code C, que d'autres paquets pourraient ensuite pirater pour implémenter une API de niveau supérieur, conviviale avec Julia.
Be careful with type equality
Vous voulez généralement utiliser isa
et <:
pour tester les types, pas ==
. Vérifier les types pour une égalité exacte n'a généralement de sens que lorsque vous comparez à un type concret connu (par exemple, T == Float64
), ou si vous savez vraiment, vraiment ce que vous faites.
Don't write a trivial anonymous function x->f(x)
for a named function f
Puisque les fonctions d'ordre supérieur sont souvent appelées avec des fonctions anonymes, il est facile de conclure que cela est souhaitable ou même nécessaire. Mais toute fonction peut être passée directement, sans être "enveloppée" dans une fonction anonyme. Au lieu d'écrire map(x->f(x), a)
, écrivez map(f, a)
.
Avoid using floats for numeric literals in generic code when possible
Si vous écrivez du code générique qui gère des nombres et qui peut être attendu pour fonctionner avec de nombreux arguments de types numériques différents, essayez d'utiliser des littéraux d'un type numérique qui affectera les arguments le moins possible par le biais de la promotion.
Par exemple,
julia> f(x) = 2.0 * x
f (generic function with 1 method)
julia> f(1//2)
1.0
julia> f(1/2)
1.0
julia> f(1)
2.0
tandis que
julia> g(x) = 2 * x
g (generic function with 1 method)
julia> g(1//2)
1//1
julia> g(1/2)
1.0
julia> g(1)
2
Comme vous pouvez le voir, la deuxième version, où nous avons utilisé un littéral Int
, a préservé le type de l'argument d'entrée, tandis que la première ne l'a pas fait. Cela est dû au fait que par exemple promote_type(Int, Float64) == Float64
, et la promotion se produit avec la multiplication. De même, les littéraux Rational
sont moins perturbateurs pour le type que les littéraux Float64
, mais plus perturbateurs que les Int
s :
julia> h(x) = 2//1 * x
h (generic function with 1 method)
julia> h(1//2)
1//1
julia> h(1/2)
1.0
julia> h(1)
2//1
Ainsi, utilisez des littéraux Int
lorsque cela est possible, avec Rational{Int}
pour les nombres non entiers littéraux, afin de faciliter l'utilisation de votre code.