Spec-Zone.ru › Julia 1.10

Руководство по стилю

В следующих разделах объясняются некоторые аспекты стилистических рекомендаций для кодирования на языке 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!). Обычно такие функции также возвращают измененный массив для удобства.

Функции, связанные с Ввод/вывод или использующие генераторы случайных чисел (RNG), являются заметными исключениями: поскольку эти функции почти всегда должны изменять Ввод/вывод или RNG, функции, оканчивающиеся на ! используются для обозначения изменения кроме изменения Ввод/вывод или продвижения состояния RNG. Например, rand(x) изменяет RNG, в то время как rand!(x) изменяет и RNG, и x; аналогично, read(io) изменяет io, в то время как read!(io, x) изменяет оба аргумента.

Избегайте странных объединений типов

Типы, такие как 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

  • Модули и имена типов используют заглавные буквы и camelCase: module SparseArrays, struct UnitRange.
  • Функции используют строчные буквы (maximum, convert) и, если это читабельно, с несколькими словами, объединенными (isequal, haskey). При необходимости используйте подчеркивание для разделения слов. Подчеркивание также используется для обозначения сочетания понятий (remotecall_fetch как более эффективная реализация fetch(remotecall(...))) или в качестве модификаторов.
  • Функции, изменяющие хотя бы один из своих аргументов, заканчиваются на !.
  • Ценится краткость, но избегайте сокращений (indexin вместо indxin) , поскольку становится трудно запомнить, какие слова сокращены и как.

Если имя функции требует нескольких слов, подумайте, не может ли оно представлять более одного понятия и не лучше ли разделить его на части.

Пишите функции с порядком аргументов, аналогичным Julia Base

В целом, в библиотеке Base используются следующие порядки аргументов для функций, если применимо:

  1. Аргумент функции. Размещение аргумента функции в начале позволяет использовать блоки do для передачи многострочных анонимных функций.

  2. Поток ввода-вывода. Указание объекта IO в начале позволяет передавать функцию таким функциям, как sprint, например sprint(show, x).

  3. Изменяемый ввод. Например, в fill!(x, v), x — это изменяемый объект, который предшествует значению, которое необходимо вставить в x.

  4. Тип. Передача типа обычно означает, что вывод будет иметь указанный тип. В parse(Int, "1") тип предшествует строке для разбора. Есть много таких примеров, где тип стоит первым, но полезно отметить, что в read(io, String) аргумент IO предшествует типу, что соответствует здесь описанному порядку.

  5. Неизменяемый ввод. В fill!(x, v), v не изменяется и следует за x.

  6. Ключ. Для ассоциативных коллекций это ключ пары(й) ключ-значение. Для других индексированных коллекций это индекс.

  7. Значение. Для ассоциативных коллекций это значение пары(й) ключ-значение. В таких случаях, как fill!(x, v), это v.

  8. Всё остальное. Любые другие аргументы.

  9. Varargs. Это относится к аргументам, которые могут быть перечислены неограниченно в конце вызова функции. Например, в Matrix{T}(undef, dims), размеры могут быть заданы как Tuple, например Matrix{T}(undef, (1,2)), или как Vararg, например Matrix{T}(undef, 1, 2).

  10. Аргументы ключевых слов. В 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} добавлять позже, если они понадобятся для решения каких-либо проблем.

Если тип фактически представляет собой перечисление, он должен быть определён как один (в идеале неизменяемый структурой или примитивный) тип, а значения перечисления должны быть экземплярами этого типа. Конструкторы и преобразования могут проверять, являются ли значения допустимыми. Этот дизайн предпочтительнее, чем сделать перечисление абстрактным типом с «значениями» в качестве подтипов.

Не злоупотребляйте макросами

Следите за тем, когда макрос может быть функцией.

Вызов 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) для именованной функции f

Поскольку высокоуровневые функции часто вызываются с анонимными функциями, легко сделать вывод, что это желательно или даже необходимо. Но любая функция может быть передана напрямую, без «обертывания» в анонимную функцию. Вместо написания 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–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.10/manual/style-guide/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API