Modules

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

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

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

module SomeModule

# export, public, using, import statements are usually here; we discuss these below

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

end

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

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

module FastThings

struct FastThing
    ...
end

end

Namespace management

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

Qualified names

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

julia> parentmodule(UnitRange)
Base

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

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

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

Export lists

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

julia> module NiceStuff
       export nice, DOG
       struct Dog end      # singleton type, not exported
       const DOG = Dog()   # named instance, exported
       nice(x) = "nice $x" # function, exported
       end;

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

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

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

Чтобы пометить имя как публичное, не экспортируя его в пространство имен людей, которые используют using NiceStuff, можно использовать public вместо export. Это помечает публичное имя(имена) как часть публичного API, но не имеет никаких последствий для пространства имен. Ключевое слово public доступно только в Julia 1.11 и выше. Чтобы поддерживать совместимость с Julia 1.10 и ниже, используйте макрос @compat из пакета Compat.

Standalone using and import

Для интерактивного использования самым распространенным способом загрузки модуля является using ModuleName. Это loads код, связанный с 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, Random

using and import with specific identifiers, and adding methods

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

julia> using .NiceStuff: nice, DOG

импортируем имена nice и DOG.

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

julia> using .NiceStuff: nice, DOG, NiceStuff

Когда два или более пакета/модуля экспортируют имя, и это имя не ссылается на одно и то же в каждом из пакетов, и пакеты загружаются с помощью using без явного списка имен, то ссылка на это имя без квалификации является ошибкой. Поэтому рекомендуется, чтобы код, предназначенный для совместимости с будущими версиями его зависимостей и Julia, например, код в выпущенных пакетах, перечислял имена, которые он использует из каждого загруженного пакета, например, using Foo: Foo, f, а не using Foo.

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

julia> using .NiceStuff: nice

julia> struct Cat end

julia> nice(::Cat) = "nice 😸"
ERROR: invalid method definition in Main: 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, модуль не может создать свою собственную переменную с тем же именем. Импортированные переменные являются только для чтения; присваивание глобальной переменной всегда влияет на переменную, принадлежащую текущему модулю, или вызывает ошибку.

Renaming with 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.

Mixing multiple using and import statements

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

julia> using .NiceStuff         # exported names and the module name

julia> import .NiceStuff: nice  # allows adding methods to unqualified functions

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

Handling name conflicts

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

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
ERROR: UndefVarError: `f` not defined in `Main`
Hint: It looks like two or more modules export different bindings with this name, resulting in ambiguity. Try explicitly importing it from a particular module, or qualifying the name with the module it should come from.

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

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

  2. Используйте ключевое слово as выше, чтобы переименовать один или оба идентификатора, например

    ```jldoctest module_manual julia> using .A: f as f

    julia> using .B: f as g

    ```

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

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

Default top-level definitions and bare modules

Модули автоматически содержат 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

Standard modules

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

  • Core содержит все функции, "встроенные" в язык.
  • Base содержит базовую функциональность, которая полезна почти во всех случаях.
  • Main является модулем верхнего уровня и текущим модулем, когда Julia запускается.
Standard library modules

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

using Test

Submodules and relative paths

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

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

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

julia> module ParentModule
       module SubA
       export add_D  # exported interface
       const D = 3
       add_D(x) = x + D
       end
       using .SubA  # brings `add_D` into the namespace
       export add_D # export it from ParentModule too
       module SubB
       import ..SubA: add_D # relative path for a “sibling” module
       struct Infinity end
       add_D(x::Infinity) = x
       end
       end;

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

julia> import .ParentModule.SubA: add_D

Однако это работает через code loading, и, следовательно, работает только если ParentModule находится в пакете. Лучше использовать относительные пути.

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

module TestPackage

export x, y

x = 0

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

y = 1

end

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

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

module A

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

module C
using ..B
end

end

Module initialization and precompilation

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

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

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

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

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

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

    julia 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. Создание случайных "копий" глобального состояния из другого модуля, ссылаясь на него напрямую, а не через его путь поиска. Например, (в глобальной области):

    ```julia #mystdout = Base.stdout #= will not work correctly, since this will copy Base.stdout into this module =#

    instead use accessor functions:

    getstdout() = Base.stdout #= best option =#

    or move the assignment into the runtime:

    init() = global mystdout = Base.stdout #= also works =# ```

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

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

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