Руководство по стилю
В следующих разделах объясняются некоторые аспекты стилистических особенностей написания кода на Julia. Ни одно из этих правил не является абсолютным; это лишь рекомендации, которые помогут вам познакомиться с языком и сделать выбор среди альтернативных вариантов проектирования.
Пишите функции, а не просто скрипты
Написание кода в виде последовательности шагов на верхнем уровне — быстрый способ начать решение проблемы, но вы должны постараться разделить программу на функции как можно скорее. Функции более переиспользуемы и тестируемы, а также проясняют, какие шаги выполняются и каковы их входные и выходные данные. Кроме того, код внутри функций, как правило, выполняется намного быстрее, чем код на верхнем уровне, благодаря тому, как работает компилятор Julia.
Стоит также подчеркнуть, что функции должны принимать аргументы вместо непосредственного использования глобальных переменных (кроме констант, таких как pi).
Избегайте чрезмерно специфичных типов
Код должен быть максимально обобщенным. Вместо написания:
convert(Complex{Float64}, x)
лучше использовать имеющиеся универсальные функции:
complex(float(x))
Вторая версия преобразует x в соответствующий тип вместо всегда одного и того же типа.
Этот пункт стиля особенно важен для аргументов функций. Например, не объявляйте аргумент типа Int или Int32, если он может быть любым целым числом, выраженным с абстрактным типом Integer. На самом деле, во многих случаях вы можете полностью опустить тип аргумента, если он не нужен для разграничения определений других методов, так как MethodError будет выброшен в любом случае, если будет передан тип, не поддерживающий необходимые операции. (Это известно как принцип утиной типизации).
Например, рассмотрим следующие определения функции addone , которая возвращает единицу больше своего аргумента:
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
Последнее определение addone обрабатывает любой тип, поддерживающий oneunit (который возвращает 1 в том же типе, что и x, что позволяет избежать нежелательного повышения типа) и функцию + с этими аргументами. Важный момент заключается в том, что нет штрафа за производительность при определении только обобщенной addone(x) = x + oneunit(x), потому что Julia автоматически скомпилирует специализированные версии по мере необходимости. Например, при первом вызове addone(12), Julia автоматически скомпилирует специализированную функцию addone для аргументов x::Int, заменив вызов oneunit на его инлайнированное значение 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(a::AbstractArray{<:Number})
for i = 1:endof(a)
a[i] *= 2
end
return a
end
используйте:
function double!(a::AbstractArray{<:Number})
for i = 1:endof(a)
a[i] *= 2
end
return a
end
Стандартная библиотека Julia использует эту соглашение во всем и содержит примеры функций с формами копирования и изменения (например, sort() и sort!()), и другие, которые только изменяют (например, push!(), pop!(), splice!()). Обычно такие функции также возвращают измененный массив для удобства.
Избегайте странных типов Union
Типы, такие как Union{Function,AbstractString}, часто указывают на то, что некоторый дизайн можно сделать более чистым.
Избегайте объединений типов в полях
При создании типа, такого как:
mutable struct MyType
...
x::Union{Void,T}
end
задумайтесь, действительно ли необходима возможность для x быть nothing (типа Void). Вот некоторые альтернативы, которые стоит рассмотреть:
Найдите безопасное значение по умолчанию для инициализации
xВведите другой тип, лишенный
xЕсли есть много полей, подобных
x, сохраните их в словареОпределите, есть ли простое правило для того, когда
xявляетсяnothing. Например, часто поле начинает какnothing, но инициализируется в какой-то определенный момент. В этом случае подумайте о том, чтобы оставить его неопределенным сначала.Если
xдействительно должен содержать пустое значение в определенные моменты времени, определите его как::Nullable{T}вместо этого, так как это гарантирует устойчивость типа в коде, обращающимся к этому полю (см. Типы с возможным отсутствием значения).
Избегайте сложных типов контейнеров
Обычно не имеет смысла создавать массивы, подобные следующим:
a = Array{Union{Int,AbstractString,Tuple,Array}}(n)
В этом случае лучше использовать Array{Any}(n) . Также компилятору будет полезнее анотировать конкретные использования (например, a[i]::Int ), чем пытаться упаковать множество альтернатив в один тип.
Используйте соглашения об именах, согласующиеся с основой Julia
модули и имена типов используют заглавные буквы и с префиксом большой буквы:
module SparseArrays,struct 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(x::T) where {T<:Real} = ...
должна быть записана как:
foo(x::Real) = ...
вместо, особенно если T не используется в теле функции. Даже если T используется, его можно заменить на typeof(x), если это удобно. Разницы в производительности нет. Обратите внимание, что это не общее предостережение против статических параметров, а только против случаев, когда они не нужны.
Обратите также внимание, что типам контейнеров могут потребоваться параметры типа в вызовах функций. Дополнительную информацию см. в FAQ Избегайте полей с абстрактными контейнерами.
Избегайте путаницы с тем, является ли что-то экземпляром или типом
Наборы определений, подобные следующим, вызывают путаницу:
foo(::Type{MyType}) = ...
foo(::MyType) = foo(MyType)
Решите, как будет записано данное понятие: как MyType или как MyType(), и придерживайтесь этого.
Предпочтительный стиль — использовать экземпляры по умолчанию и добавлять методы, включающие Type{MyType}, только если они станут необходимы для решения какой-либо проблемы.
Если тип фактически является перечислением, он должен быть определён как один (в идеале неизменяемый struct или примитивный) тип, а значения перечисления — экземплярами этого типа. Конструкторы и преобразования могут проверять, являются ли значения допустимыми. Этот дизайн предпочтительнее, чем делать перечисление абстрактным типом, с "значениями" как подтипами.
Не злоупотребляйте макросами
Понимайте, когда макрос действительно может быть функцией.
Вызов eval() внутри макроса — особенно тревожный признак; это означает, что макрос будет работать только при вызове на верхнем уровне. Если такой макрос написан как функция вместо этого, он естественным образом получит доступ к необходимым ему значениям во время выполнения.
Не раскрывайте небезопасные операции на уровне интерфейса
Если у вас есть тип, использующий указатель нативный:
mutable struct NativeType
p::Ptr{UInt8}
...
end
не пишите определения, подобные следующим:
getindex(x::NativeType, i) = unsafe_load(x.p, i)
Проблема в том, что пользователи этого типа могут написать x[i] без осознания того, что операция небезопасна, а затем столкнуться с ошибками памяти.
Такая функция должна либо проверять операцию, чтобы убедиться в ее безопасности, либо иметь unsafe где-то в ее имени, чтобы предупредить вызывающих стороны.
Не перегружайте методы базовых типов контейнеров
Можно написать определения, подобные следующим:
show(io::IO, v::Vector{MyType}) = ...
Это позволит настраиваемо отображать векторы с определенным новым типом элемента. Хотя это заманчиво, этого следует избегать. Проблема в том, что пользователи ожидают, что хорошо известный тип, например Vector(), будет вести себя определенным образом, а чрезмерная настройка его поведения может затруднить работу с ним.
Избегайте «пиратства типов»
«Пиратство типов» относится к практике расширения или переопределения методов в пакетах Base или других пакетах для типов, которые вы не определили. В некоторых случаях вы можете позволить себе «пиратство типов» без существенных последствий. Однако в крайних случаях вы можете даже вызвать сбой Julia (например, если ваше расширение или переопределение метода приводит к передаче некорректных данных в ccall). «Пиратство типов» может усложнить понимание кода и может привести к труднопредсказуемым и труднодиагностируемым несовместимостям.
В качестве примера предположим, что вы хотите определить умножение на символах в модуле:
module A import Base.* *(x::Symbol, y::Symbol) = Symbol(x,y) end
Проблема в том, что теперь любой другой модуль, использующий Base.*, также увидит это определение. Поскольку Symbol определен в Base и используется другими модулями, это может неожиданно изменить поведение несвязанного кода. В данном случае существуют альтернативные решения, включая использование другого имени функции или обертывание Symbol в другой тип, который вы определяете.
Иногда связанные пакеты могут использовать «пиратство типов» для разделения функций от определений, особенно когда пакеты были разработаны совместно авторами и определения являются многократно используемыми. Например, один пакет может предоставить некоторые типы, полезные для работы с цветами; другой пакет может определить методы для этих типов, которые позволяют выполнять преобразования между цветовыми пространствами. Другой пример — пакет, который выступает в качестве тонкого оболочки для некоторого кода на C, который другой пакет может затем «похитить» для реализации API более высокого уровня, дружественного для Julia.
Будьте осторожны с равенством типов
Обычно для проверки типов следует использовать 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(1) 2
Как видите, во втором варианте, где мы использовали литерал типа 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.6/manual/style-guide/