Руководство по стилю
В следующих разделах объясняются некоторые аспекты стилистических соглашений при написании кода на Julia. Ни одно из этих правил не является абсолютным; они представляют собой лишь рекомендации, призванные помочь вам познакомиться с языком и выбрать оптимальное решение среди альтернативных вариантов.
Отступы
Используйте 4 пробела на каждый уровень отступа.
Пишите функции, а не просто скрипты
Написание кода в виде последовательности шагов на верхнем уровне — быстрый способ начать решение проблемы, но вы должны постараться разделить программу на функции как можно скорее. Функции более переиспользуемы и тестируемы, а также проясняют, какие шаги выполняются, и каковы их входные и выходные данные. Кроме того, код внутри функций, как правило, выполняется намного быстрее, чем код на верхнем уровне, из-за того, как работает компилятор Julia.
Также стоит подчеркнуть, что функции должны принимать аргументы, а не работать напрямую с глобальными переменными (за исключением констант, таких как pi).
Избегайте излишне специфичных типов
Код должен быть максимально обобщенным. Вместо того, чтобы писать:
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 = 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.Все остальные. Любые другие аргументы.
Переменное количество аргументов. Это относится к аргументам, которые могут быть перечислены неограниченно в конце вызова функции. Например, в
Matrix{T}(undef, dims), размеры могут быть заданы какTuple, напримерMatrix{T}(undef, (1,2)), или какVararg, напримерMatrix{T}(undef, 1, 2).Именованные аргументы. В Julia именованные аргументы по умолчанию должны стоять в конце в определениях функций; они перечислены здесь для полноты.
Большинство функций не будут принимать все типы аргументов, перечисленных выше; числа просто обозначают приоритет, который должен использоваться для любых применимых аргументов функции.
Конечно, есть несколько исключений. Например, в convert тип всегда должен стоять первым. В setindex! значение стоит перед индексами, чтобы индексы можно было передать как переменное число аргументов.
При разработке 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–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.7.0/manual/style-guide/