Руководство по стилю
В следующих разделах объясняются некоторые аспекты стилистики кода Julia. Ни одно из этих правил не является абсолютным; они представляют собой лишь рекомендации, призванные помочь вам ознакомиться с языком и сделать выбор среди альтернативных решений.
Пишите функции, а не просто скрипты
Написание кода в виде последовательности шагов на верхнем уровне — быстрый способ начать решение задачи, но вы должны постараться разбить программу на функции как можно скорее. Функции более повторно используемы и тестируемы, и они уточняют, какие шаги выполняются, а также их входные и выходные данные. Кроме того, код внутри функций, как правило, выполняется намного быстрее, чем код на верхнем уровне, из-за того, как работает компилятор Julia.
Также следует подчеркнуть, что функции должны принимать аргументы, а не работать непосредственно с глобальными переменными (кроме констант, таких как pi).
Избегайте чрезмерно специфичных типов
Код должен быть максимально обобщенным. Вместо написания:
convert(Complex{Float64}, x)
лучше использовать имеющиеся универсальные функции:
complex(float(x))
Вторая версия преобразует x в соответствующий тип, а не всегда в один и тот же тип.
Этот принцип особенно важен для аргументов функций. Например, не объявляйте аргумент типа Int или Int32, если он действительно может быть любым целым числом, выраженным с помощью абстрактного типа Integer. На самом деле, во многих случаях вы можете вообще опустить тип аргумента, если он не нужен для устранения неоднозначности с другими определениями методов, поскольку MethodError будет выброшен в любом случае, если будет передан тип, не поддерживающий необходимые операции. (Это известно как duck typing.)
Например, рассмотрим следующие определения функции addone , которая возвращает единицу, прибавленную к своему аргументу:
addone(x::Int) = x + 1 # works only for Int addone(x::Integer) = x + one(x) # any integer type addone(x::Number) = x + one(x) # any numeric type addone(x) = x + one(x) # any type supporting + and one
Последнее определение addone обрабатывает любой тип, поддерживающий one() (который возвращает 1 в том же типе, что и x, что предотвращает нежелательное повышение типа), и функцию + с этими аргументами. Важно понимать, что нет никакой потери производительности при определении только универсального addone(x) = x + one(x), потому что Julia автоматически скомпилирует специализированные версии по мере необходимости. Например, в первый раз при вызове addone(12), Julia автоматически скомпилирует специализированную функцию addone для аргументов x::Int, заменив вызов one() на его встроенное значение 1. Следовательно, первые три определения addone выше совершенно излишни.
Обработка избыточного разнообразия аргументов в вызывающей функции
Вместо:
function foo(x, y)
x = Int(x); y = Int(y)
...
end
foo(x, y)
используйте:
function foo(x::Int, y::Int)
...
end
foo(Int(x), Int(y))
Это лучший стиль, потому что foo на самом деле не принимает числа всех типов; ему действительно нужны Int.
Одна проблема заключается в том, что если функция изначально требует целых чисел, то может быть лучше заставить вызывающую функцию решить, как должны быть преобразованы нецелые числа (например, отбросить дробную часть или округлить). Другая проблема заключается в том, что объявление более специфичных типов оставляет больше «пространства» для будущих определений методов.
Добавляйте ! к именам функций, которые изменяют свои аргументы
Вместо:
function double{T<:Number}(a::AbstractArray{T})
for i = 1:endof(a); a[i] *= 2; end
a
end
используйте:
function double!{T<:Number}(a::AbstractArray{T})
for i = 1:endof(a); a[i] *= 2; end
a
end
Стандартная библиотека Julia использует эту конвенцию на протяжении всего проекта и содержит примеры функций с формами копирования и модификации (например, sort() и sort!()), а также другие, которые просто изменяют (например, push!(), pop!(), splice!()). Обычно такие функции также возвращают измененный массив для удобства.
Избегайте странных объединений типов
Типы, такие как Union{Function,AbstractString} , часто свидетельствуют о том, что некоторый дизайн можно улучшить.
Избегайте объединений типов в полях
При создании типа, такого как:
type MyType
...
x::Union{Void,T}
end
задайтесь вопросом, действительно ли необходима возможность для x быть nothing (типа Void). Вот некоторые альтернативы, которые следует рассмотреть:
- Найти безопасное значение по умолчанию для инициализации
x - Ввести другой тип, в котором отсутствует
x - Если есть много полей, похожих на
x, храните их в словаре - Определить, есть ли простое правило для того, когда
xявляетсяnothingНапример, часто поле сначала будетnothing, но будет инициализировано в определённый момент. В этом случае рассмотрите возможность оставить его неопределённым сначала. - Если
xдействительно должен содержать значение без значения в некоторых случаях, определите его как::Nullable{T}, поскольку это гарантирует стабильность типа в коде, обращающемся к этому полю (см. Типы, допускающие null)
Избегайте сложных типов контейнеров
Обычно не очень полезно создавать массивы, подобные следующим:
a = Array{Union{Int,AbstractString,Tuple,Array}}(n)
В этом случае Array{Any}(n) лучше. Также компилятору будет полезнее видеть конкретные использования (например, a[i]::Int ), чем пытаться упаковать множество альтернатив в один тип.
Используйте соглашения об именах, согласующиеся с base/ Julia
- Имена модулей и типов используют прописные и комбинированные буквы:
module SparseArrays,immutable UnitRange. - Функции пишутся строчными буквами (
maximum(),convert()) и, когда это читаемо, с несколькими словами, соединёнными вместе (isequal(),haskey()). При необходимости используйте подчёркивания в качестве разделителей слов. Подчёркивания также используются для указания комбинации понятий (remotecall_fetch()как более эффективной реализацииfetch(remotecall(...))) или в качестве модификаторов (sum_kbn()). - Лаконичность ценится, но избегайте сокращений (
indexin()вместоindxin()), поскольку становится трудно вспомнить, как и в каком случае те или иные слова сокращены.
Если имя функции требует нескольких слов, подумайте, не может ли она представлять собой более чем одну концепцию и не следует ли ее разделить на части.
Не злоупотребляйте try-catch
Лучше избегать ошибок, чем полагаться на их перехват.
Не заключайте условия в скобки
Julia не требует скобок вокруг условий в if и while. Пишите:
if a == b
вместо:
if (a == b)
Не злоупотребляйте …
Сцепление аргументов функции может быть привычкой. Вместо [a..., b...], просто используйте [a; b], которое уже конкатенирует массивы. collect(a) лучше, чем [a...], но так как a уже итерируемо, часто лучше его оставить без преобразования в массив.
Не используйте ненужные статические параметры
Подпись функции:
foo{T<:Real}(x::T) = ...
должна быть записана как:
foo(x::Real) = ...
вместо этого, особенно если T не используется в теле функции. Даже если T используется, её можно заменить на typeof(x), если это удобно. Разницы в производительности нет. Обратите внимание, что это не общее предостережение против статических параметров, а только против случаев, где они не нужны.
Обратите также внимание, что типам контейнеров, в частности, могут потребоваться параметризованные типы в вызовах функций. Дополнительную информацию см. в разделе FAQ Избегайте полей с абстрактными контейнерами.
Избегайте путаницы, касающейся того, является ли что-то экземпляром или типом
Наборы определений, подобные следующим, вызывают путаницу:
foo(::Type{MyType}) = ...
foo(::MyType) = foo(MyType)
Решите, как будет записываться интересующий концепция — как MyType или как MyType(), и придерживайтесь выбранного способа.
Предпочтительный стиль — использовать экземпляры по умолчанию, а методы, связанные с Type{MyType} , добавлять только при необходимости для решения какой-либо задачи.
Если тип фактически представляет собой перечисление, он должен быть определен как единственный (в идеале immutable ) тип, а значения перечисления — как экземпляры этого типа. Конструкторы и преобразования могут проверять, являются ли значения допустимыми. Этот дизайн предпочтительнее, чем сделать перечисление абстрактным типом, а «значения» — подтипами.
Не злоупотребляйте макросами
Поймите, когда макрос действительно может быть функцией.
Вызов eval() внутри макроса — это особенно тревожный признак; это означает, что макрос будет работать только при вызове на верхнем уровне. Если такой макрос написан как функция, он будет естественным образом иметь доступ к необходимым значениям времени выполнения.
Не раскрывайте небезопасные операции на уровне интерфейса
Если у вас есть тип, использующий указатель нативный:
type NativeType
p::Ptr{UInt8}
...
end
не пишите определения, подобные следующим:
getindex(x::NativeType, i) = unsafe_load(x.p, i)
Проблема в том, что пользователи этого типа могут написать x[i] , не осознавая, что операция небезопасна, а затем подвергнуться ошибкам памяти.
Такая функция должна либо проверять операцию на безопасность, либо иметь unsafe где-нибудь в своем имени, чтобы предупредить вызывающих стороны.
Не переопределяйте методы базовых типов контейнеров
Можно написать определения, подобные следующим:
show(io::IO, v::Vector{MyType}) = ...
Это позволило бы настраивать отображение векторов со специфическим новым типом элемента. Несмотря на заманчивость, этого следует избегать. Проблема в том, что пользователи ожидают, что хорошо известный тип, такой как Vector() , будет вести себя определенным образом, а чрезмерная настройка его поведения может затруднить работу с ним.
Будьте внимательны с равенством типов
В целом, для проверки типов следует использовать isa() и <: (issubtype()), а не ==. Проверка типов на точное равенство обычно имеет смысл только при сравнении с известным конкретным типом (например, T == Float64), или если вы действительно знаете, что делаете.
Не пишите x->f(x)
Поскольку высшего порядка функции часто вызываются с анонимными функциями, легко заключить, что это желательно или даже необходимо. Но любую функцию можно передать непосредственно, не «упаковывая» ее в анонимную функцию. Вместо написания map(x->f(x), a), используйте map(f, a).
Избегайте использования чисел с плавающей точкой для числовых литералов в обобщенном коде, когда это возможно
Если вы пишете обобщенный код, который обрабатывает числа и который может быть запущен с многими различными числовыми аргументами типа, попробуйте использовать литералы числового типа, которые будут минимально влиять на аргументы путем повышения.
Например,
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
в то время как
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(2) 4
Как видите, во втором варианте, где мы использовали Int литерал, тип входного аргумента сохранился, в то время как в первом — нет. Это происходит потому, что, например, promote_type(Int, Float64) == Float64, а повышение происходит при умножении. Аналогично, Rational литералы менее разрушительны для типов, чем Float64 литералы, но более разрушительны, чем Int:
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
Таким образом, используйте Int литералы, когда это возможно, с Rational{Int} для нецелых числовых литералов, чтобы сделать код проще в использовании.
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.5/manual/style-guide/