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

Модули

Модули в Julia помогают организовывать код в целостные единицы. Они синтаксически разграничиваются внутри module NameOfModule ... end и имеют следующие характеристики.

  1. Модули являются отдельными пространствами имен, каждое из которых представляет новую глобальную область. Это полезно, поскольку позволяет использовать одно и то же имя для разных функций или глобальных переменных без возникновения конфликта, если они находятся в разных модулях.

  2. В модулях есть средства для детального управления пространством имен: каждый модуль определяет набор имен, которые он экспортирует с помощью export, и может импортировать имена из других модулей с помощью операторов using и import (они будут рассматриваться ниже).

  3. Модули могут быть предварительно скомпилированы для ускорения загрузки. Они также могут содержать код для инициализации во время выполнения.

Как правило, в больших пакетах Julia вы увидите код модуля, организованный в файлы, например

module SomeModule

# здесь обычно находятся операторы export, using, import, которые будут рассматриваться ниже.

include("file1.jl")
include("file2.jl")

end

Файлы и имена файлов в основном не имеют отношения к модулям. Модули связаны только с выражениями модулей. Для каждого модуля может существовать несколько файлов, а для каждого файла — несколько модулей. Функция include ведет себя так, как если бы содержимое файла исходного кода обрабатывалось в глобальной области содержащего модуля. В этой главе приводятся короткие и упрощенные примеры, поэтому функция include использоваться не будет.

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

module FastThings

struct FastThing
    ...
end

end

Управление пространством имен

Управление пространством имен относится к возможностям языка, позволяющим сделать имена в модуле доступными в других модулях. Связанные концепции и функциональные возможности будут подробно рассматриваться далее.

Квалифицированные имена

Имена функций, переменных и типов в глобальной области, такие как sin, ARGS и UnitRange, всегда принадлежат модулю, называемому родительским модулем, который можно найти в интерактивном режиме с помощью функции parentmodule, например

julia> parentmodule(UnitRange)
Base

На эти имена можно ссылаться и вне родительского модуля, добавляя к ним префикс в виде модуля, например Base.UnitRange. Такое имя называется квалифицированным. Родительский модуль может быть доступен с помощью цепочки подмодулей типа Base.Math.sin, где Base.Math называется путем к модулю. Из-за синтаксической неоднозначности для квалификации имени, содержащего только символы, такие как оператор, требуется вставить двоеточие. Например, Base.:+. Для небольшого числа операторов дополнительно требуются круглые скобки. Например, Base.:(==).

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

В модуле имя переменной можно зарезервировать без присваивания, объявив его как global x. Это предотвращает конфликты имен для глобальных файлов, инициализируемых после загрузки. Синтаксис M.x = y не работает для присваивания глобального объекта в другом модуле. Присваивание глобального объекта всегда локально и выполняется в одном модуле.

Списки экспорта

Имена (относящиеся к функциям, типам, глобальным переменным и константам) можно добавить в список экспорта модуля с помощью оператора export: это символы, которые импортируются при использовании модуля (using). Обычно они находятся в верхней части определения модуля или вблизи нее, чтобы читатели исходного кода могли легко их находить, как показано в примере:

julia> module NiceStuff
       export nice, DOG
       struct Dog end      # Одинарный тип, не экспортируется
       const DOG = Dog()   # Именованный экземпляр, экспортируется
       nice(x) = "nice $x" # Функция, экспортируется
       end;

Но это всего лишь предложение по стилю — модуль может иметь несколько операторов export в произвольных местах.

Обычно экспортируются имена, которые являются частью API (интерфейса прикладного программирования). В приведенном выше коде список экспорта предлагает пользователям использовать nice и DOG. Однако, поскольку квалифицированные имена всегда делают идентификаторы доступными, это всего лишь вариант организации API: в отличие от других языков, в Julia нет средств, позволяющих действительно скрыть внутреннее содержимое модуля.

Кроме того, некоторые модули вообще не экспортируют имена. Обычно это происходит, если они используют в API общие слова, такие как derivative, что может привести к конфликтам со списками экспорта других модулей. Управление конфликтами имен будет рассматриваться далее.

Отдельные операторы using и import

Возможно, самым распространенным способом загрузки модуля является использование оператора using ModuleName. Происходит загрузка кода, связанного с модулем ModuleName, и

  1. имя модуля

  2. и элементы списка экспорта добавляются в окружающее глобальное пространство имен.

Технически, оператор using ModuleName означает, что модуль с именем ModuleName будет доступен для разрешения имен по мере необходимости. Когда встречается глобальная переменная, не имеющая определения в текущем модуле, система будет искать ее среди переменных, экспортируемых модулем ModuleName, и использовать ее, если она там будет найдена. Это означает, что все случаи использования этой глобальной переменной в текущем модуле будут разрешаться в пользу определения этой переменной в модуле ModuleName.

Для загрузки модуля из пакета можно использовать оператор using ModuleName. Для загрузки модуля из локально определенного модуля перед именем модуля нужно добавить точку, например using .ModuleName.

Продолжим наш пример:

julia> using .NiceStuff

загрузит приведенный выше код, делая доступными NiceStuff (имя модуля), DOG и nice. Dog отсутствует в списке экспорта, но к нему можно получить доступ, если имя квалифицировано с помощью пути к модулю (который здесь является просто именем модуля) как NiceStuff.Dog.

Важно отметить, что using ModuleName — это единственная форма, для которой списки экспорта имеют значение.

Напротив,

julia> import .NiceStuff

добавляет в область только имя модуля. Для доступа к его содержимому пользователи должны будут использовать NiceStuff.DOG, NiceStuff.Dog и NiceStuff.nice. Обычно import ModuleName используется в контекстах, когда пользователь хочет сохранить пространство имен чистым. Как мы увидим в следующем разделе, import .NiceStuff эквивалентен using .NiceStuff: NiceStuff.

Несколько операторов using и import одного типа можно объединить в выражении, разделенном запятыми, например:

julia> using LinearAlgebra, Statistics

Операторы using и import с определенными идентификаторами и добавление методов

Когда за оператором using ModuleName: или import ModuleName: следует список имен, разделенных запятыми, модуль загружается, но оператор добавляет в пространство имен только эти конкретные имена. Например,

julia> using .NiceStuff: nice, DOG

будет импортировать имена nice и DOG.

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

julia> using .NiceStuff: nice, DOG, NiceStuff

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

julia> using .NiceStuff: nice

julia> struct Cat end

julia> nice(::Cat) = "nice 😸"
ERROR: error in method definition: function NiceStuff.nice must be explicitly imported to be extended
Stacktrace:
 [1] top-level scope
   @ none:0
 [2] top-level scope
   @ none:1

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

Есть два способа решения этой проблемы. Всегда можно уточнить имена функций с помощью пути к модулю.

julia> using .NiceStuff

julia> struct Cat end

julia> NiceStuff.nice(::Cat) = "nice 😸"

Или можно импортировать с помощью import конкретное имя функции.

julia> import .NiceStuff: nice

julia> struct Cat end

julia> nice(::Cat) = "nice 😸"
nice (generic function with 2 methods)

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

Как только переменная станет видимой при использовании оператора using или import, модуль не сможет создать собственную переменную с тем же именем. Импортированные переменные доступны только для чтения. Присваивание глобальной переменной всегда влияет на переменную, принадлежащую текущему модулю, в противном случае возникает ошибка.

Переименование с помощью ключевого слова as

Идентификатор, добавленный в область с помощью оператора import или using, можно переименовать, используя ключевое слово as. Это полезно для разрешения конфликтов имен, а также для сокращения имен. Например, модуль Base экспортирует имя функции read, но пакет CSV.jl также предоставляет CSV.read. Если планируется вызывать операцию чтения CSV много раз, целесообразно отказаться от классификатора CSV.. Но тогда остается неясным, на что мы ссылаемся — на Base.read или CSV.read.

julia> read;

julia> import CSV: read
WARNING: ignoring conflicting import of CSV.read into Main

Эту проблему можно решить путем переименования.

julia> import CSV: read as rd

Можно также переименовать сами импортированные пакеты.

import BenchmarkTools as BT

Ключевое слово as работает с оператором using только тогда, когда в область попадает один идентификатор. Например, using CSV: read as rd поддерживается, а using CSV as C нет, поскольку он работает со всеми экспортированными именами в CSV.

Смешивание нескольких выражений с операторами using и import

Когда используется несколько операторов using или import любой из вышеперечисленных форм, их действие соединяется в порядке их появления. Например,

julia> using .NiceStuff         # Экспортированные имена и имя модуля

julia> import .NiceStuff: nice  # Позволяет добавлять методы в неполные функции

Введет все экспортируемые имена NiceStuff и само имя модуля в область, а также позволит добавлять методы в nice, не указывая имя модуля в качестве префикса.

Устранение конфликтов имен

Рассмотрим ситуацию, когда два пакета (или более) экспортируют одно и то же имя, как в примере ниже.

julia> module A
       export f
       f() = 1
       end
A
julia> module B
       export f
       f() = 2
       end
B

Оператор using .A, .B работает, но при попытке вызывать функцию f выводится предупреждение.

julia> using .A, .B

julia> f
WARNING: both B and A export "f"; uses of it in module Main must be qualified
ERROR: UndefVarError: `f` not defined

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

  1. Продолжение с использованием квалифицированных имен, таких как A.f и B.f. В этом случае контекст будет понятен читателю кода, особенно если функция f просто совпадает, но имеет разное значение в разных пакетах. Например, degree по-разному используется в математике, естественных науках и в повседневной жизни, и эти значения должны разделяться.

  2. Использование ключевого слова as для переименования одного идентификатора или обоих, например:

     julia> using .A: f as f
    
     julia> using .B: f as g

    B.f будет доступен как g. Здесь предполагается, что до этого вы не использовали оператор using A, что привело бы к добавлению функции f в пространство имен.

  3. Когда рассматриваемые имена имеют общее значение, как правило, один модуль импортирует его из другого или имеет упрощенный, базовый пакет с единственной функцией определения интерфейса, подобного этому, который могут использовать другие пакеты. Обычно имена таких пакетов заканчиваются на ...Base (что не имеет ничего общего с модулем Base в Julia).

Определения верхнего уровня по умолчанию и пустые модули

Модули автоматически содержат операторы using Core, using Base и определения функций eval и include, которые вычисляют выражения или файлы в глобальной области данного модуля.

Если эти определения по умолчанию не нужны, модули можно определить с помощью ключевого слова baremodule (примечание: модуль Core по-прежнему импортируется). С точки зрения модуля baremodule стандартный модуль module выглядит следующим образом.

baremodule Mod

using Base

eval(x) = Core.eval(Mod, x)
include(p) = Base.include(Mod, p)

...

end

Если даже модуль Core не требуется, модуль, который ничего не импортирует и не определяет никаких имен, может быть определен с помощью Module(:YourNameHere, false, false), а код в нем может быть обработан с помощью макроса @eval или функции Core.eval:

julia> arithmetic = Module(:arithmetic, false, false)
Main.arithmetic

julia> @eval arithmetic add(x, y) = $(+)(x, y)
add (generic function with 1 method)

julia> arithmetic.add(12, 13)
25

Стандартные модули

Существует три важных стандартных модуля.

  • Core contains all functionality "built into" the language.

  • Base contains basic functionality that is useful in almost all cases.

  • Main is the top-level module and the current module, when Julia is started.

По умолчанию в состав Julia входит несколько модулей стандартной библиотеки. Они функционируют как обычные пакеты Julia, за исключением того, что их не нужно устанавливать явным образом. Например, если вам требуется выполнить модульное тестирование, можно загрузить стандартную библиотеку Test следующим образом.

```julia
using Test
```

Подмодули и относительные пути

Модули могут содержать подмодули, в которых вложен тот же синтаксис module ... end. Их можно использовать для введения отдельных пространств имен, что может быть полезно для организации сложных баз кода. Обратите внимание, что каждый module вводит собственную область, поэтому подмодули автоматически не наследуют имена от своего родительского модуля.

Рекомендуется, чтобы подмодули ссылались на другие модули внутри охватывающего родительского модуля (включая последний), используя относительные квалификаторы модуля в операторах using и import. Относительный квалификатор модуля начинается с точки (.), что соответствует текущему модулю, а каждый последующий символ . ведет на родительский модуль текущего модуля. За ним должны следовать модули, если необходимо, и в итоге — фактическое имя, к которому нужно получить доступ. Все элементы разделяются символом ..

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

julia> module ParentModule
       module SubA
       export add_D  # Экспортируемый интерфейс
       const D = 3
       add_D(x) = x + D
       end
       using .SubA  # Привносит `add_D` в пространство имен
       export add_D # Экспортирует его из модуля ParentModule
       module SubB
       import ..SubA: add_D # Относительный путь для одноуровневого модуля
       struct Infinity end
       add_D(x::Infinity) = x
       end
       end;

Вы можете увидеть код в пакетах, который в аналогичной ситуации использует

julia> import .ParentModule.SubA: add_D

Однако это работает через загрузку кода и поэтому действует только в том случае, если модуль ParentModule находится в пакете. Рекомендуется использовать относительные пути.

Обратите внимание, что при вычислении значений также важен порядок определений. Рассмотрим

module TestPackage

export x, y

x = 0

module Sub
using ..TestPackage
z = y # ERROR: UndefVarError: `y` not defined
end

y = 1

end

где модуль Sub пытается использовать переменную TestPackage.y до того, как она была определена, поэтому у него нет значения.

По аналогичным причинам невозможно использовать циклическое упорядочение.

module A

module B
using ..C # ERROR: UndefVarError: `C` not defined
end

module C
using ..B
end

end

Инициализация и предварительная компиляция модуля

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

При загрузке модуля с помощью оператора import или using автоматически создаются и используются предварительно скомпилированные файлы модуля (иногда называемые «файлами кэша»). Если файлов кэша еще нет, модуль будет скомпилирован и сохранен для повторного использования в будущем. Чтобы создать эти файлы, не загружая модуль, можно также вызвать Base.compilecache(Base.identify_package("modulename")) вручную. Результирующие файлы кэша будут сохранены во вложенной папке compiled в папке DEPOT_PATH[1]. Если в вашей системе ничего не изменилось, эти файлы кэша будут использоваться при загрузке модуля с помощью оператора import или using.

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

Однако если вы обновите зависимости модуля или измените его исходный код, модуль будет автоматически перекомпилирован при использовании оператора using или import. Зависимостями являются импортируемые модули, сборка Julia, включаемые файлы или явные зависимости, объявленные с помощью функции include_dependency(path) в файлах модуля.

Для зависимостей файла изменение определяется путем анализа того, является ли время модификации (mtime) каждого файла, загруженного с помощью include или явно добавленного с помощью include_dependency, неизменным или равным времени модификации, усеченному до ближайшей секунды (для систем, которые не могут копировать mtime с точностью в долях секунды). Также учитывается, совпадает ли путь к файлу, выбранный логикой поиска в require, с путем, по которому был создан файл предварительной компиляции. Кроме того, принимается во внимание набор зависимостей, уже загруженных в текущий процесс. Эти модули не будут перекомпилированы, даже если их файлы изменятся или исчезнут, чтобы исключить возникновение несовместимости между работающей системой и кешем предварительной компиляции. Наконец, принимаются во внимание изменения в любых настройках времени компиляции.

Если известно, что предварительная компиляция модуля небезопасна (например, по одной из причин, описанных ниже), необходимо поместить функцию __precompile__(false) в файл модуля (обычно в верхнюю часть). В результате функция Base.compilecache выдаст ошибку, а оператор using или import загрузит модуль непосредственно в текущий процесс и пропустит процедуры предварительной компиляции и кеширования. Это также предотвращает импорт модуля любым другим предварительно скомпилированным модулем.

Возможно, вам потребуется быть в курсе некоторых особенностей поведения, присущих созданию добавочных общих библиотек и требующих внимания при написании модуля. Например, внешнее состояние не сохраняется. Чтобы учесть этот момент, следует явным образом отделить шаги инициализации, которые должны осуществляться во время выполнения, от шагов, которые могут происходить во время компиляции. Для этого Julia позволяет определить функцию __init__() в модуле, которая выполняет любые шаги инициализации, которые должны происходить во время выполнения. Эта функция не будет вызываться во время компиляции (--output-*). По сути, можно предположить, что она будет выполнена ровно один раз за все время существования кода. Конечно, при необходимости вы можете вызвать ее вручную, но по умолчанию считается, что эта функция работает с состоянием вычислений для локального компьютера, которое не нужно — или даже не следует — фиксировать в скомпилированном образе. Функция будет вызвана после загрузки модуля в процесс, в том числе если он загружается в процесс добавочной компиляции (--output-incremental=yes), но не будет вызвана, если модуль загружается в процесс полной компиляции.

В частности, если вы определите функцию function __init__() в модуле, Julia вызовет функцию __init__() сразу после загрузки модуля (например, с помощью операторов import, using или require) во время выполнения в первый раз (т. е. функция __init__ вызывается только один раз и только после выполнения всех операторов в модуле). Поскольку функция вызывается после полного импорта модуля, функции __init__ подмодулей или других импортированных модулей вызываются перед функцией __init__ охватывающего модуля.

Два типичных применения функции __init__ — это вызов функций инициализации среды выполнения внешних библиотек C и инициализация глобальных констант, которые предусматривают использование указателей, возвращаемых внешними библиотеками. Например, предположим, что нужно вызвать библиотеку C libfoo, для которой требуется вызвать функцию инициализации foo_init() во время выполнения. Предположим, что также следует опередить глобальную константу foo_data_ptr, которая будет хранить возвращаемое значение функции void *foo_data(), определенной библиотекой libfoo. Эта константа должна быть инициализирована во время выполнения (не во время компиляции), поскольку адрес указателя будет меняться в каждом выполнении. Это можно сделать, определив в модуле следующую функцию __init__.

const foo_data_ptr = Ref{Ptr{Cvoid}}(0)
function __init__()
    ccall((:foo_init, :libfoo), Cvoid, ())
    foo_data_ptr[] = ccall((:foo_data, :libfoo), Ptr{Cvoid}, ())
    nothing
end

Обратите внимание, что вполне возможно определить глобальный объект внутри функции типа __init__. Это одно из преимуществ использования динамического языка. Сделав объект константой в глобальной области, можно гарантировать, что тип известен компилятору, и позволить ему генерировать более оптимизированный код. Очевидно, что любые другие глобальные объекты в модуле, который зависит от foo_data_ptr, также должны быть инициализированы в функции __init__.

Константы, связанные с большинством объектов Julia, которые не создаются с помощью ключевого слова ccall, не нужно помещать в функцию __init__: их определения могут быть предварительно скомпилированы и загружены из образа кешированного модуля. К ним относятся сложные выделяемые в куче объекты, такие как массивы. Однако любую подпрограмму, возвращающую необработанное значение указателя, следует вызывать во время выполнения, в противном случае предварительная компиляция выполнена не будет (объекты Ptr превратятся в нулевые указатели, если только они не скрыты внутри объекта isbits). Сюда входят возвращаемые значения функций Julia @cfunction и pointer.

Ситуация со словарными типами и типами наборов или вообще со всем, что зависит от вывода метода hash(key), намного сложнее. В общем случае, когда ключами являются числа, строки, символы, диапазоны, выражения Expr или композиции этих типов (через массивы, кортежи, наборы, пары и т. д.), предварительная компиляция выполняется без проблем. Однако для некоторых других типов ключей, таких как Function или DataType, и универсальных определяемых пользователем типов, для которых не определен метод hash, резервный метод hash зависит от адреса памяти объекта (через его идентификатор objectid) и, следовательно, может меняться в каждом выполнении. Если у вас есть один из этих типов ключей или если вы не уверены, в целях безопасности можно инициализировать этот словарь в функции __init__. В качестве альтернативы можно использовать словарный тип IdDict, который специально обрабатывается в рамках предварительной компиляции, чтобы его можно было безопасно инициализировать во время компиляции.

При использовании предварительной компиляции важно четко понимать различие между этапом компиляции и этапом выполнения. В этом режиме часто гораздо более очевидно, что Julia является компилятором, который позволяет выполнять произвольный код Julia, а не отдельным интерпретатором, который также генерирует скомпилированный код.

Ниже приводятся другие известные потенциальные сценарии сбоя:

  1. Глобальные счетчики (например, для попытки уникальной идентификации объектов). Рассмотрим следующий фрагмент кода.

     mutable struct UniquedById
         myid::Int
         let counter = 0
             UniquedById() = new(counter += 1)
         end
     end

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

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

    Альтернативой может быть использование макроса для записи модуля @__MODULE__ и его отдельного хранения с текущим значением счетчика counter. Однако, возможно, лучше перепроектировать код так, чтобы он не зависел от этого глобального состояния.

  2. Ассоциативные коллекции (такие как Dict и Set) должны быть повторно хешированы в функции __init__. (В будущем может быть предусмотрен механизм для регистрации функции инициализатора.)

  3. В зависимости от времени компиляции во время загрузки сохраняются побочные эффекты. Пример: изменение массивов или других переменных в других модулях Julia; поддержка дескрипторов на открытые файлы или устройства; хранение указателей на другие системные ресурсы (включая память). 4. Создание случайных копий глобального состояния из другого модуля путем указания его напрямую, а не через путь поиска. Например, (в глобальной области):

     #mystdout = Base.stdout #= будет работать некорректно, поскольку Base.stdout будет скопирован в этот модуль =#
     # Вместо этого используйте функции метода доступа:
     getstdout() = Base.stdout #= наилучший вариант =#
     # Или переместите присваивание в среду выполнения:
     __init__() = global mystdout = Base.stdout #= также работает =#

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

  1. Вызов функции eval для возникновения побочного эффекта в другом модуле. Это также приведет к выдаче предупреждения , если установлен флаг добавочной предварительной компиляции.

  2. Операторы global const из локальной области после запуска функции __init__() (см. описание проблемы № 12010 с планами на добавление соответствующей ошибки).

  3. Замена модуля является ошибкой времени выполнения при выполнении добавочной предварительной компиляции.

Ниже перечислено еще несколько моментов, на которые следует обратить внимание.

  1. Перезагрузка кода или аннулирование кеша не выполняется после внесения изменений в сами исходные файлы (в том числе с помощью Pkg.update), и очистка не производится после Pkg.rm.

  2. При предварительной компиляции игнорируется поведение совместного использования памяти массива с измененной формой (каждое представление получает свою копию).

  3. Ожидание неизменности файловой системы между временем компиляции и временем выполнения, например @__FILE__ или source_path() для поиска ресурсов во время выполнения, или макрос BinDeps @checked_lib. Иногда это неизбежно. Однако, когда это возможно, рекомендуется скопировать ресурсы в модуль во время компиляции, чтобы их не нужно было искать во время выполнения.

  4. На данный момент сериализатор не обрабатывает должным образом объекты и финализаторы WeakRef (это будет исправлено в ближайшем выпуске).

  5. Обычно лучше избегать записи ссылок на экземпляры внутренних объектов метаданных, таких как Method, MethodInstance, MethodTable, TypeMapLevel, TypeMapEntry, и полей этих объектов, так как это может дезориентировать сериализатор и не привести к желаемому результату. Запись не обязательно будет считаться ошибкой, но вы просто должны быть готовы к тому, что система попытается скопировать одни из них и создать единственный уникальный экземпляр других.

Иногда во время разработки модуля целесообразно отключить добавочную предварительную компиляцию. Включать или отключать предварительную компиляцию модуля можно с помощью флага командной строки --compiled-modules={yes|no}. Когда Julia запускается с установленным флагом --compiled-modules=no, сериализованные модули в кеше компиляции игнорируются при загрузке модулей и зависимостей модулей. Более детальный контроль обеспечивается параметром --pkgimages=no, который подавляет только хранение машинного кода во время предварительной компиляции. Функцию Base.compilecache по-прежнему можно вызывать вручную. Состояние этого флага командной строки передается скрипту Pkg.build для отключения автоматического запуска предварительной компиляции при установке, обновлении и явной сборке пакетов.

Некоторые сбои предварительной компиляции можно также отладить с помощью переменных среды. Если задать JULIA_VERBOSE_LINKING=true, это может помочь устранить сбои при компоновке общих библиотек скомпилированного машинного кода. В документации для разработчиков в руководстве Julia есть раздел, посвященный особенностям внутреннего устройства Julia, в главе «Образы пакетов», где можно найти более подробные сведения.