1.10. Модули и пакеты#

1.10.1. Модули#

Совет

Подробнее в секции мануала Modules.

В Julia можно разбивать код программы на модули (modules). Модуль создаёт собственное пространство имён и может быть прекомпилирован.

Основной синтаксис выглядит так.

module Points

using LinearAlgebra

export dist, Point

include("types.jl")
include("functions.jl")

private_foo() = println("Hello!")

end # module

Всё, что между командами module ... end представляет собой модуль. В данном примере создаётся модуль Points.

Инструкция using LinearAlgebra импортирует публичные имена из модуля LinearAlgebra. При таком вызове, например, функция LinearAlgebra.norm из модуля доступна просто по имени norm. Фактически, программист так указывает зависимости модуля Points от других модулей.

Модуль Points также делает имена dist и Point публичными. Т.е., когда кто-нибудь импортирует Points командой using, то ему будут доступны имена dist и Point. Точно также где-то в исходном коде модуля LinearAlgebra происходит экспорт имени norm.

Функция include("<path_to_file>") делает подстановку содержимого файла в модуль. В Julia позднее связывание имён, поэтому вы можете спокойно экспортировать что-то, а объявить где-то позднее.

Как импортировать свой модуль?

Если модуль Points помещён в файл myscripts/Points.jl, то можно импортировать модуль так

include("myscripts/Points.jl")  # В общем случае полный путь до файла с исходным кодом модуля
using .Points

@show dist(Point(1, 2))

Здесь функцией include совершается подстановка содержимого файла с модулем, как будто бы мы его скопировали сюда. Затем используется using .Points – заметьте точку. По умолчанию Julia ищет модули (а точнее, пакет) в текущем окружении. Глобальное окружение вы можете посмотреть в REPL командой ] status – она покажет список установленных пакетов (но не покажет стандартные библиотеки). В нашем случае пакет не создавался и регистрировался в глобальном окружении, поэтому команда using Points привела бы к ошибке. Но, с помощью . поиск модуля осуществляется относительно скрипта, а не окружения. Кроме того, есть .. для обращения к родительскому модулю.

using и import

Для импортирования существуют инструкции using и import. Их главное отличие в назначении.

Инструкция using предназначена для использования кода модуля пользователем. Например, using LinearAlgebra позволяет нам, как пользователям модуля LinearAlgebra, использовать функции norm, cross

Инструкция import отличается от using тем, что позволяет переопределять и создавать новые методы для функций, определённых в импортированном модуле. Другими словами, import для разработчиков. Например, чтобы добавить метод скалярного произведения для собственного типа данных, придётся воспользоваться import LinearAlgebra.

Что импортируется, а что нет

Можно импортировать только некоторый функционал модуля, для этого используется двоеточие :. Можно переименовывать импортируемые имена с помощью as. В таблице ниже показано, какие имена доступны при использовании разных вариантов using и import.

Команда импорта

Какие имена доступны

using Points

Points, и публичные dist, Point, остальные через точку: Points.private_foo

using Points: dist

Только dist

using Points: dist as d

Только d

import Points

Points, остальные через точку

import Points as Pnts

Pnts, остальные через точку

1.10.1.1. Пример разработки модуля#

Ниже показана разработка модуля в несколько этапов. В нём привычная структура Point{T}, а её интерфейс оборачивается в модуль. Затем, для примера, структура встраивается в существующую экосистему языка: можно скалярно умножать точки, складывать или умножать на скаляр.

Points.jl

module Points

export dist
export Point

struct Point{T}
    x::T
    y::T
end

dist(p::Point) = sqrt(p.x^2 + p.y^2)
random_point() = Point(rand(2)...)

end # module

script.jl

include("path/to/Points.jl")
using .Points

println(dist(Point(3, 4)))
println(Points.random_point())

Points.jl

module Points

import LinearAlgebra  # для добавления метода к скалярному произведению

export dist
export Point

struct Point{T}
    x::T
    y::T
end

dist(p::Point) = sqrt(p.x^2 + p.y^2)
random_point() = Point(rand(2)...)

# Добавление метода к скалярному произведению LinearAlgebra.dot
LinearAlgebra.dot(p1::Point, p2::Point) = p1.x * p2.x + p1.y * p2.y

end # module

script.jl

include("path/to/Points.jl")
using .Points
using LinearAlbgebra  # Для dot(x, y)

println(dot(Point(-1, 2), Point(-2, -3)))

Points.jl

module Points

import LinearAlgebra

export dist
export Point

struct Point{T}
    x::T
    y::T
end

dist(p::Point) = sqrt(p.x^2 + p.y^2)
random_point() = Point(rand(2)...)

LinearAlgebra.dot(p1::Point, p2::Point) = p1.x * p2.x + p1.y * p2.y

# Расширяение стандартной библиотеки языка, модуля Base
# `+` коммутативно
Base.:+(p1::Point, p2::Point) = Point(p1.x + p2.x, p1.y + p2.y)
# `*` не коммутативно
Base.:*(α::Number, p::Point) = Point(α * p.x, α * p.y)
Base.:*(p::Point, α::Number) = α * p

end # module

script.jl

include("path/to/Points.jl")
using .Points

println(Point(1, 2) + Point(3.0, 4.1))
println(2 * Point(1, 2))
println(Point(1, 2) * 2.0)

Разбиение исходного кода на файлы

Когда исходный код разрастается, его разбивают на отдельные файлы. Для библиотеки кода в Julia типично наличие корневого файла, в котором объявлен модуль, зависимости от других библиотек и экспортируемые имена, а оставшийся код библиотеки подставлятся с помощью include. Весь код библиотеки помещают в директорию src, а корневой файл называют также, как и главный модуль библиотеки.

Ниже показана типичная структура исходного когда библиотеки.

Структура библиотеки и Points.jl

Структура директории.

src/
  operators.jl
  interface.jl
  types.jl
  Points.jl

Код модуля src/Points.jl.

module Points

import LinearAlgebra

export dist
export Point

include("types.jl")
include("interface.jl")
include("operators.jl")

end # module
Остальной код

src/types.jl

struct Point{T}
    x::T
    y::T
end

src/interface.jl

dist(p::Point) = sqrt(p.x^2 + p.y^2)
random_point() = Point(rand(2)...)

src/operators.jl

LinearAlgebra.dot(p1::Point, p2::Point) = p1.x * p2.x + p1.y * p2.y

Base.:+(p1::Point, p2::Point) = Point(p1.x + p2.x, p1.y + p2.y)

Base.:*(α::Number, p::Point) = Point(α * p.x, α * p.y)
Base.:*(p::Point, α::Number) = α * p

1.10.2. Пакеты#

Пакет (package) это инструмент для распространения библиотеки кода среди пользователей. Пакет содержит саму библиотеку кода и метаинформацию о нём, необходимую для идентификации пакета в системе и корректной установки. Кроме того, пакет может содержать примеры применения и набор тестов.

Пакеты в Julia создаются с помощью Pkg-mode в Julia REPL. Сам пакет представляет из себя git-репозиторий со следующей структурой

AwesomeLibrary/
  src/
    AwesomeLibrary.jl
    ...
  tests/
    ...
  Project.toml

Непосредственный код библиотеки содержится в src/ и имеет ту же структуру, которую мы рассмотрели в примере разработки модуля. В tests/ содержится система тестирования кода библиотеки. В Project.toml содержится информация об авторе пакета, его уникальный номер, а также перечисление программных библиотек с версиями, которые необходимы для работы пакета (зависимостей).

Пакеты можно разрабатывать и использовать локально. Это лучше всего делать в директории ~/.julia/dev/. Также, если вы хотите внести изменения не в свой пакет, то для этого используется ] dev, а исходный код появится в ~/.julia/dev/.

Для распространения пакеты публикуются на git-платформе, обычно на GitHub, например, DataFrames.jl или Unitful.jl. Кроме того, пакеты обычно регистрируются в реестре пакетов для удобства поиска пользователями. Реестр также является git-репозиторием и хранит информацию о всех зарегистрированных версиях пакетов. В REPL вы можете посмотреть добавленные реестры командой ] registry status. Скорее всего, там окажется General реестр – официальный реестр пакетов для Julia. Можно искать пакеты в REPL, на github или на интернет-платформе, вроде такой.

Зарегистрированные в реестре пакеты устанавливаются просто: ] add PackageName, например, ] add DataFrames. После этой команды Julia запросит информацию в реестрах о пакете DataFrames, найдёт его фактическое расположение, скачает и установит все зависимости. Список установленных пакетов в окружении можно узнать командой ] status.

В Julia пакеты имеют независимые окружения. Т.е. зависимости пакета изолированы от остальных окружений, в том числе от глобального. Это уменьшает количество конфликтов версий между используемыми библиотеками.

Полная информация об окружении содержится в файле Project.toml. Если вы хотите поделиться кодом, который использует библиотеки, то можно вместе с ним передать Project.toml файл. Так поступают для Jupyter ноутбуков, а в Pluto.jl информация об окружении содержится внутри скрипта.

В окружение также входит файл Manifest.toml, создаваемый автоматически. В нём содержится полная и точная информация о зависимостях пакета для вашего компьютера. Т.е. какая библиотека откуда берётся.