Руководство по стилю
В следующих разделах объясняются некоторые аспекты стилистики написания кода на языке Julia. Ни одно из этих правил не является абсолютным; они представляют собой лишь рекомендации, которые помогут вам освоить язык и сделать выбор между альтернативными решениями.
Отступы
Используйте 4 пробела на уровень отступа.
Пишите функции, а не просто скрипты
Написание кода в виде последовательности шагов на верхнем уровне — быстрый способ начать решение проблемы, но вы должны как можно скорее разделить программу на функции. Функции более повторно используемы и тестируемы, они проясняют, какие шаги выполняются и каковы их входные и выходные данные. Кроме того, код внутри функций, как правило, выполняется намного быстрее, чем код на верхнем уровне, из-за того, как работает компилятор Julia.
Также стоит подчеркнуть, что функции должны принимать аргументы, а не работать напрямую с глобальными переменными (кроме констант, таких как pi).
Избегайте чрезмерно специфичных типов
Код должен быть максимально обобщенным. Вместо написания:
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 + 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 = firstindex(a):lastindex(a)
a[i] *= 2
end
return a
end
используйте:
function double!(a::AbstractArray{<:Number})
for i = firstindex(a):lastindex(a)
a[i] *= 2
end
return a
end
Библиотека Julia Base использует эту конвенцию на протяжении всего кода и содержит примеры функций с формами копирования и изменения (например, sort и sort!), а также функции, которые только изменяют (например, push!, pop!, splice!). Обычно такие функции также возвращают измененный массив для удобства.
Избегайте странных объединений типов
Типы, такие как Union{Function,AbstractString}, часто указывают на то, что дизайн можно сделать более чистым.
Избегайте сложных типов контейнеров
Обычно нет большой пользы от создания массивов, подобных следующим:
a = Vector{Union{Int,AbstractString,Tuple,Array}}(undef, n)
В этом случае Vector{Any}(undef, n) лучше. Также компилятору полезнее аннотировать конкретные случаи использования (например, a[i]::Int) чем пытаться упаковать много альтернатив в один тип.
Предпочитайте экспортированные методы доступу к полям напрямую
Стиль кода Julia, как правило, должен рассматривать экспортированные методы модуля как интерфейс к его типам. Поля объекта, как правило, считаются деталями реализации, и пользовательский код должен обращаться к ним напрямую только в том случае, если это указано в API. Это имеет несколько преимуществ:
- Разработчики пакетов свободнее менять реализацию без нарушения кода пользователей.
- Методы можно передавать в такие высшие конструкции, как
map(например,map(imag, zs)) вместо[z.im for z in zs]. - Методы можно определять для абстрактных типов.
- Методы могут описывать концептуальную операцию, которую можно совместно использовать в разных типах (например,
real(z)работает с комплексными числами или кватернионами).
Система диспетчеризации Julia поощряет этот стиль, потому что play(x::MyType) определяет только метод play для этого конкретного типа, оставляя другим типам свою собственную реализацию.
Аналогично, неэкспортированные функции, как правило, являются внутренними и могут быть изменены, если в документации не указано иное. Имена иногда имеют префикс _ (или суффикс), чтобы еще больше подчеркнуть, что что-то является «внутренним» или деталью реализации, но это не правило.
Примеры исключений из этого правила включают NamedTuple, RegexMatch, StatStruct.
Использование соглашений об именовании, согласованных с Julia base/
- Имена модулей и типов используют прописные и смешанный регистр:
module SparseArrays,struct UnitRange. - Функции пишутся строчными буквами (
maximum,convert) и, если это читаемо, с несколькими словами, соединёнными вместе (isequal,haskey). При необходимости используйте подчеркивание для разделения слов. Подчеркивание также используется для обозначения комбинации концепций (remotecall_fetchкак более эффективной реализацииfetch(remotecall(...))или в качестве модификатора). - Функции, изменяющие по крайней мере один из своих аргументов, заканчиваются на
!. - Короткость ценится, но избегайте сокращений (
indexinвместоindxin). Они становятся сложными для запоминания, как и то, как определённые слова сокращаются.
Если имя функции требует нескольких слов, подумайте, может ли оно представлять больше одной концепции и не лучше ли разбить его на части.
Написание функций с порядком аргументов, аналогичным порядку в Julia Base
Как общее правило, библиотека Base использует следующий порядок аргументов функций, где это применимо:
Аргумент функции. Размещение аргумента функции в начале позволяет использовать блоки
doдля передачи многострочных анонимных функций.Поток ввода-вывода. Указание объекта
IOв начале позволяет передать функцию таким функциям, какsprint, напримерsprint(show, x).Мутируемый ввод. Например, в
fill!(x, v),x— это объект, который мутируется, и он предшествует значению, которое нужно вставить вx.Тип. Передача типа обычно означает, что результат будет иметь заданный тип. В
parse(Int, "1")тип предшествует строке для разбора. Существует множество таких примеров, где тип стоит первым, но полезно отметить, что вread(io, String)аргументIOпредшествует типу, что соответствует порядку, описанному здесь.Немутируемый ввод. В
fill!(x, v),vне мутируется, и он следует заx.Ключ. Для ассоциативных коллекций это ключ пары(ей) «ключ-значение». Для других индексированных коллекций это индекс.
Значение. Для ассоциативных коллекций это значение пары(ей) «ключ-значение». В таких случаях, как
fill!(x, v), этоv.Все остальное. Любые другие аргументы.
Varargs. Это относится к аргументам, которые могут быть перечислены неограниченно в конце вызова функции. Например, в
Matrix{T}(undef, dims), размерности могут быть заданы какTuple, напримерMatrix{T}(undef, (1,2)), или какVararg, напримерMatrix{T}(undef, 1, 2).Аргументы ключевых слов. В Julia аргументы ключевых слов должны идти в любом случае последними в определениях функций; они указаны здесь для полноты.
Подавляющее большинство функций не будут использовать каждый тип аргументов, перечисленных выше; числа просто обозначают приоритет, который следует использовать для любых применимых аргументов функции.
Конечно, есть несколько исключений. Например, в convert тип всегда должен стоять первым. В setindex! значение предшествует индексам, чтобы индексы можно было указать как varargs.
При разработке API придерживаться этого общего порядка, насколько это возможно, скорее всего, обеспечит пользователям ваших функций более согласованный опыт.
Не злоупотребляйте 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, который другой пакет может затем использовать для реализации более высокого уровня, дружественного к Julia API.
Будьте осторожны с равенством типов
В целом, вы хотите использовать isa и <: для проверки типов, а не ==. Проверка типов на точное равенство обычно имеет смысл только при сравнении с известным конкретным типом (например, 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–2022 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.8/manual/style-guide/