Документация Engee

Руководство по стилю и принципы проектирования

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

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

Однако у абстрактно типизированных методов есть два основных недостатка.

  1. Где-то глубоко в цепочке вызовов может оказаться, что вы работаете с

непредвиденными типами, что может привести к труднодиагностируемым ошибкам MethodError.

  1. Нетипизированные аргументы функции могут привести к проблемам с корректностью, если

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

Например, рассмотрим для примера следующую функцию.

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.

В качестве наглядного примера для второго пункта: VariableRef плюс Float64 дает AffExpr. Не следует предполагать, что +(::A, ::B) создает экземпляр типа A или B.

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:

Пользователь должен получать ошибку 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)

Что особенно важно, эти методы не обязательно должны быть определены в исходном пакете.

Некоторые варианты использования абстрактных типов допустимы. Например, в my_sum_option_1 мы допустили использование подтипа Number в качестве типа элемента T. Это довольно безопасно, но все же предполагает неявное допущение о том, что T поддерживает zero(T) и +(::T, ::T).

Вариант 2. Пишите код в защитном стиле и проверяйте все допущения.

Альтернативой является написание кода в защитном стиле, а также тщательное документирование и проверка всех допущений, которые делаются в коде. В частности:

  1. Все допущения касательно абстрактных типов, которые не гарантируются

определением абстрактного типа (например, необязательные методы без резервного варианта), должны быть задокументированы.

  1. Если это целесообразно, допущения следует проверять в коде, а пользователю

следует предоставлять информативные сообщения об ошибках, если допущения не соблюдаются. Как правило, такие проверки требуют затрат, поэтому лучше проводить их один раз на самом высоком уровне цепочки вызовов.

  1. Тесты должны охватывать особые случаи и типы аргументов.

Например:

"""
    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 с соответствующим файлом.