Руководство по стилю и принципы проектирования
Руководство по стилю
В этом разделе описываются правила написания кода для JuMP, которые мы рекомендуем для моделей JuMP и связанного с ними кода Julia. Руководство по стилю преследует следующие цели:
-
применение передового опыта для написания кода, который легко читать и сопровождать;
-
сокращение времени, которое тратится впустую вследствие «эффекта велосипедного сарая», благодаря основным правилам именования и форматирования;
-
снижение входного барьера для новых разработчиков путем кодификации существующих практик (например, ваш код скорее пройдет рецензирование, если вы будете следовать руководству по стилю).
В некоторых случаях руководство по стилю JuMP расходится с руководством по стилю Julia. Все такие случаи будут отмечены отдельно с обоснованием.
В руководстве по стилю JuMP учтены многие рекомендации из руководств по стилю Google.
Руководство по стилю постоянно улучшается, поэтому не весь код JuMP соответствует правилам. При внесении изменений в JuMP исправляйте нарушения стиля в окружающем коде (то есть оставляйте код после себя более опрятным, чем он был изначально). Если необходимы масштабные изменения, возможно, будет лучше выделить их в другой запрос на вытягивание. |
JuliaFormatter
В JuMP в качестве инструмента автоматического форматирования используется JuliaFormatter.jl.
При этом применяются параметры, содержащиеся в .JuliaFormatter.toml
.
Для форматирования кода перейдите в каталог JuMP с помощью команды cd
и выполните следующие команды:
] add JuliaFormatter@1
using JuliaFormatter
format("docs")
format("src")
format("test")
Форматирование всех запросов на вытягивание, отправляемых в репозиторий JuMP, проходит непрерывную проверку интеграции. |
В следующих разделах приводятся дополнительные пункты руководства по стилю, которые JuliaFormatter не исправляет автоматически.
Абстрактные типы и композиция
Указывать типы для аргументов методов в Julia, как правило, необязательно. Преимущество абстрактных аргументов методов заключается в том, что они позволяют использовать функции и типы из одного пакета с функциями и типами из другого пакета посредством множественной диспетчеризации.
Однако у абстрактно типизированных методов есть два основных недостатка.
-
Где-то глубоко в цепочке вызовов может оказаться, что вы работаете с
непредвиденными типами, что может привести к труднодиагностируемым ошибкам MethodError
.
-
Нетипизированные аргументы функции могут привести к проблемам с корректностью, если
выбранный пользователем тип входных данных не соответствует допущениям, сделанным разработчиком функции.
Например, рассмотрим для примера следующую функцию.
julia> function my_sum(x)
y = 0.0
for i in 1:length(x)
y += x[i]
end
return y
end
my_sum (generic function with 1 method)
В этой функции делается ряд неявных допущений касательно типа x
:
-
x
поддерживает отсчитываемый от 1 индексgetindex
и реализуетlength
. -
Тип элемента
x
поддерживает сложение с0.0
, а затем с результатомx + 0.0
.
В качестве наглядного примера для второго пункта: |
my_sum
работает правильно, если пользователь передает Vector{Float64}
:
julia> my_sum([1.0, 2.0, 3.0])
6.0
но типы входных данных не сохраняются, например, при передаче Vector{Int}
возвращается Float64
:
julia> my_sum([1, 2, 3])
6.0
но при передаче String
выдается ошибка MethodError
:
julia> my_sum("abc")
ERROR: MethodError: no method matching +(::Float64, ::Char)
[...]
Такую ошибку MethodError
трудно отладить, особенно новым пользователям, так как в ней упоминаются +
, Float64
и Char
, которые не вызывались и не передавались пользователем.
Обработка ошибок MethodError
Этот раздел расходится с руководством по стилю Julia, а также другими распространенными руководствами, такими как SciML. Следующие предложения направлены на то, чтобы сделать работу удобнее для начинающих программистов на Julia ценой ограничения возможностей и гибкости для опытных программистов.
Код должен следовать принципу MethodError
:
Пользователь должен получать ошибку |
Неправильно:
_internal_function(x::Integer) = x + 1
# Пользователь получает MethodError для _internal_function при вызове
# public_function("строка"). Это не особо полезно.
public_function(x) = _internal_function(x)
Правильно:
_internal_function(x::Integer) = x + 1
# Пользователь получает MethodError для public_function при вызове
# public_function("строка"). Это будет понятно.
public_function(x::Integer) = _internal_function(x)
Если предоставить сообщение об ошибке в начале цепочки вызовов сложно, подойдет и такая схема:
_internal_function(x::Integer) = x + 1
function _internal_function(x)
error(
"Internal error. This probably means that you called " *
"`public_function()`s with the wrong type.",
)
end
public_function(x) = _internal_function(x)
Обеспечение корректности
Обеспечить корректность труднее, так как в Julia нет возможности формально указать интерфейсы, которые должны быть реализованы абстрактными типами. Вместо этого при написании универсального кода и взаимодействии с ним есть два варианта.
Вариант 1. Используйте конкретные типы и разрешите пользователям расширять новые методы.
При таком варианте следует явно ограничить входные аргументы конкретными типами, которые были протестированы и проверены на корректность. Например:
julia> function my_sum_option_1(x::Vector{Float64})
y = 0.0
for i in 1:length(x)
y += x[i]
end
return y
end
my_sum_option_1 (generic function with 1 method)
julia> my_sum_option_1([1.0, 2.0, 3.0])
6.0
Использование конкретных типов соответствует принципу MethodError
:
julia> my_sum_option_1("abc")
ERROR: MethodError: no method matching my_sum_option_1(::String)
и позволяет обеспечить поддержку других типов в будущем путем определения новых методов:
julia> function my_sum_option_1(x::Array{T,N}) where {T<:Number,N}
y = zero(T)
for i in eachindex(x)
y += x[i]
end
return y
end
my_sum_option_1 (generic function with 2 methods)
Что особенно важно, эти методы не обязательно должны быть определены в исходном пакете.
Некоторые варианты использования абстрактных типов допустимы. Например, в |
Вариант 2. Пишите код в защитном стиле и проверяйте все допущения.
Альтернативой является написание кода в защитном стиле, а также тщательное документирование и проверка всех допущений, которые делаются в коде. В частности:
-
Все допущения касательно абстрактных типов, которые не гарантируются
определением абстрактного типа (например, необязательные методы без резервного варианта), должны быть задокументированы.
-
Если это целесообразно, допущения следует проверять в коде, а пользователю
следует предоставлять информативные сообщения об ошибках, если допущения не соблюдаются. Как правило, такие проверки требуют затрат, поэтому лучше проводить их один раз на самом высоком уровне цепочки вызовов.
-
Тесты должны охватывать особые случаи и типы аргументов.
Например:
"""
test_my_sum_defensive_assumptions(x::AbstractArray{T}) where {T}
Test the assumptions made by `my_sum_defensive`.
"""
function test_my_sum_defensive_assumptions(x::AbstractArray{T}) where {T}
try
# Для некоторых типов ноль может быть не определен.
@assert zero(T) isa T
# Проверка поддержки итерации
@assert iterate(x) isa Union{Nothing,Tuple{T,Int}}
# Проверка определения оператора +
@assert +(zero(T), zero(T)) isa Any
catch err
error(
"Unable to call my_sum_defensive(::$(typeof(x))) because " *
"it failed an internal assumption",
)
end
return
end
"""
my_sum_defensive(x::AbstractArray{T}) where {T}
Return the sum of the elements in the abstract array `x`.
## Допущения
This function makes the following assumptions:
* That `zero(T)` is defined
* That `x` supports the iteration interface
* That `+(::T, ::T)` is defined
"""
function my_sum_defensive(x::AbstractArray{T}) where {T}
test_my_sum_defensive_assumptions(x)
y = zero(T)
for xi in x
y += xi
end
return y
end
# output
my_sum_defensive
Эта функция работает с Vector{Float64}
:
julia> my_sum_defensive([1.0, 2.0, 3.0])
6.0
и с Matrix{Rational{Int}}
:
julia> my_sum_defensive([(1//2) + (4//3)im; (6//5) + (7//11)im])
17//10 + 65//33*im
но выдает ошибку, если допущения не соблюдаются:
julia> my_sum_defensive(['a', 'b', 'c'])
ERROR: Unable to call my_sum_defensive(::Vector{Char}) because it failed an internal assumption
[...]
В качестве альтернативы можно не вызывать функцию test_my_sum_defensive_assumptions
внутри my_sum_defensive
, а вместо этого попросить пользователей my_sum_defensive
вызывать ее в своих тестах.
Умножение без знака умножения
Используйте форму записи умножения без знака умножения, только если справа находится символ.
Правильно:
2x # Допустимо, если место ограничено.
2 * x # Если места хватает, такой вариант предпочтительнее.
2 * (x + 1)
Неправильно:
2(x + 1)
Пустые векторы
Для типа T
, T[]
и Vector{T}()
являются эквивалентными способами создания пустого вектора с типом элементов T
. Лучше использовать T[]
как более лаконичную форму.
Комментарии
Для общей ясности и по той причине, что не все пользователи могут владеть языком разработчика, комментарии в коде должны представлять собой правильные предложения на английском языке с соответствующей пунктуацией.
Правильно:
# Это пример хорошего комментария.
Неправильно:
# плохой комментарий
Синтаксис макросов JuMP
Для единообразия всегда используйте круглые скобки.
Правильно:
@variable(model, x >= 0)
Неправильно:
@variable model x >= 0
Для единообразия всегда используйте порядок записи constant * variable
, а не variable * constant
. Это облегчает понимание моделей в неоднозначных случаях, таких как a * x
.
Правильно:
a = 4
@constraint(model, 3 * x <= 1)
@constraint(model, a * x <= 1)
Неправильно:
a = 4
@constraint(model, x * 3 <= 1)
@constraint(model, x * a <= 1)
Для сокращения объема шаблонного кода лучше использовать множественную форму макросов вместо многократных повторных вызовов формы единственного числа.
Правильно:
@variables(model, begin
x >= 0
y >= 1
z <= 2
end)
Неправильно:
@variable(model, x >= 0)
@variable(model, y >= 1)
@variable(model, z <= 2)
Исключение делается для вызовов со множеством именованных аргументов, так как для правильного анализа их необходимо заключить в скобки.
Допустимо:
@variable(model, x >= 0, start = 0.0, base_name = "my_x")
@variable(model, y >= 1, start = 2.0)
@variable(model, z <= 2, start = -1.0)
Также допустимо:
@variables(model, begin
x >= 0, (start = 0.0, base_name = "my_x")
y >= 1, (start = 2.0)
z <= 2, (start = -1.0)
end)
Хотя в циклах for
всегда используется in
, в объявлениях контейнеров в макросах JuMP допускается использовать =
.
Правильно:
@variable(model, x[i=1:3])
Тоже правильно:
@variable(model, x[i in 1:3])
Именование
module SomeModule end
function some_function end
const SOME_CONSTANT = ...
struct SomeStruct
some_field::SomeType
end
@enum SomeEnum ENUM_VALUE_A ENUM_VALUE_B
some_local_variable = ...
some_file.jl # Кроме ModuleName.jl.
Экспортируемые и неэкспортируемые имена
Имена частных функций и констант на уровне модуля должны начинаться с символа подчеркивания. Все остальные объекты в области модуля должны экспортироваться (пример см. в JuMP.jl).
Имена, начинающиеся с символа подчеркивания, следует использовать только для различения экспортируемых (общедоступных) и неэкспортируемых (частных) объектов. Поэтому имя локальной переменной ни в коем случае не должно начинаться с символа подчеркивания.
module MyModule
export public_function, PUBLIC_CONSTANT
function _private_function()
local_variable = 1
return
end
function public_function end
const _PRIVATE_CONSTANT = 3.14159
const PUBLIC_CONSTANT = 1.41421
end
Использование символов подчеркивания внутри имен
В руководстве по стилю Julia рекомендуется избегать символов подчеркивания, если это не мешает удобочитаемости, например haskey
, isequal
, remotecall
и remotecall_fetch
. Это соглашение может создавать «эффект велосипедного сарая» и заставлять пользователя вспоминать, есть ли в имени символ подчеркивания, например, называется ли аргумент basename
или base_name
. Для единообразия всегда используйте символ подчеркивания в именах переменных и функций для разделения слов.
Использование символа !
В Julia принято соглашение добавлять символ !
к имени функции, если она изменяет свои аргументы. Мы рекомендуем следующее.
-
Не добавляйте
!
, если из самого имени ясно, что происходит изменение, напримерadd_constraint
иset_name
. Здесь мы отступаем от руководства по стилю Julia, так как в данном случае символ!
не несет никакой дополнительной информации и это соглашение иногда не соблюдается даже в базовых модулях Julia (см.Base.println
иBase.finalize
). -
Во всех остальных случаях используйте символ
!
. В частности, он может использоваться для различения изменяющего и неизменяющего вариантов одной и той же функции, напримерscale
иscale!
.
Обратите внимание, что символ !
не является самодокументируемым элементом, так как при наличии нескольких аргументов по-прежнему остается неясным, какие из них изменяются. Обязательно задокументируйте изменяемые аргументы в docstring метода.
См. также представленные в руководстве по стилю Julia рекомендации по порядку следования аргументов функций.
Сокращения
Сокращайте имена, чтобы код было удобнее читать, а не чтобы вводить меньше символов. Не сокращайте слова, удаляя произвольные буквы (например, indx
). Используйте сокращения единообразно в пределах всего кода (например, не смешивайте con
и constr
, idx
и indx
).
Стандартные сокращения:
-
num
дляnumber
; -
con
дляconstraint
.
Не используйте имена переменных из одной буквы
По возможности избегайте использования имен переменных из одной буквы.
Используйте model = Model()
вместо m = Model()
Исключение делается для индексов в циклах.
@enum
и Symbol
Макрос @enum
позволяет определять типы с конечным числом явно перечислимых значений (аналогично enum
в C/C++). Объекты Symbol
— это простые строки, которые служат для представления имен в Julia (например, :x
).
@enum
обеспечивает безопасность типов и может сопровождаться docstring с описанием возможных значений. Используйте @enum
в соответствующих ситуациях, например для сообщения статуса. Используйте строки для предоставления подробной дополнительной информации, например сообщений об ошибках.
Символы Symbol
обычно должны быть зарезервированы для имен, например для поиска в модели JuMP (model[:my_variable]
).
using
и import
using ModuleName
вводит в область все символы, экспортируемые модулем ModuleName
, тогда как import ModuleName
вводит в область только сам модуль. Примеры и дополнительные сведения см. в руководстве Julia.
По той же причине, по которой from <module> import *
не рекомендуется использовать в Python (PEP 8), избегайте использования using ModuleName
, за исключением одноразовых скриптов или команд в REPL. Оператор using
затрудняет отслеживание источника символов и приводит к неоднозначности кода, когда один и тот же символ экспортируется из двух модулей.
Вместо import ModuleName.x, ModuleName.p
и import MyModule: x, p
лучше использовать using ModuleName: x, p
, так как версии с import
допускают расширение метода без уточнения имени модуля.
Аналогичным образом, using ModuleName: ModuleName
является приемлемой заменой для import ModuleName
, так как не вводит в область все символы, экспортируемые модулем ModuleName
. Однако для единообразия предпочтительнее вариант import ModuleName
.
Документация
В этом разделе описывается стиль написания документации для JuMP (и вспомогательных пакетов).
В качестве общих правил написания документации мы можем порекомендовать руководства по стилю оформления документации от Divio, Google и Write the Docs. В данном документе полное раскрытие темы доверяется этим руководствам, а подробно рассматриваются лишь те моменты, которые характерны для Julia и документации, созданной с использованием Documenter.
-
Будьте лаконичны.
-
Используйте списки вместо длинных предложений.
-
При описании последовательности используйте нумерованные списки, например: 1) сделать X; 2) затем сделать Y.
-
Если элементы не упорядочены, используйте маркированные списки.
-
Код примеров должен быть покрыт тестами doctest.
-
Если слово является символом Julia, а не обычным английским словом, заключите его в обратные апострофы. Кроме того, если для него есть docstring в текущей документации, добавьте ссылку с помощью
@ref
. Если символ имеет форму множественного числа, добавьте букву s после закрывающего обратного апострофа. Примеры:`VariableRef`s
-
Используйте блоки
@meta
для TODO и других комментариев, которые не должны быть видны читателям. Пример:```@meta # TODO: Также упомянуть X, Y и Z. ```
Docstring
-
У каждого экспортируемого объекта должен быть docstring.
-
Все примеры в docstring должны быть
jldoctests
. -
Всегда используйте полные английские предложения с правильной пунктуацией.
-
Не ставьте знаки препинания в конце элементов списков.
Вот пример:
"""
signature(args; kwargs...)
Short sentence describing the function.
Optional: add a slightly longer paragraph describing the function.
## Примечания
- List any notes that the user should be aware of
## Пример
```jldoctest
julia> 1 + 1
2
```
"""
Тестирование
Используйте модуль для инкапсуляции тестов и структурируйте все тесты как функции. Это позволит избежать утечки локальных переменных между тестами.
Вот базовая структура:
module TestPkg
using Test
function runtests()
for name in names(@__MODULE__; all = true)
if startswith("$(name)", "test_")
@testset "$(name)" begin
getfield(@__MODULE__, name)()
end
end
end
return
end
_helper_function() = 2
function test_addition()
@test 1 + 1 == _helper_function()
return
end
end # модуль TestPkg
TestPkg.runtests()
Разделяйте тесты на несколько файлов, по одному модулю на файл, чтобы части базы кода можно было протестировать, вызвав include
с соответствующим файлом.