Spec-Zone.ru › Julia 0.5

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

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

END_OF_DOCUMENT_MARKER

Не раскрывайте небезопасные операции на уровне интерфейса

Если у вас есть тип, использующий указатель нативный:

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/

Spec-Zone.ru

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