Engee documentation

Modules

Modules in Julia help to organize the code into complete units. They are syntactically delimited inside the module NameOfModule …​ end+ and have the following characteristics.

  1. Modules are separate namespaces, each representing a new global scope. This is useful because it allows you to use the same name for different functions or global variables without causing conflict if they are in different modules.

  2. Modules have tools for detailed namespace management: each module defines a set of names that it exports using export and marks as public, and can import names from other modules using the using and import operators (they will be discussed below).

  3. Modules can be pre-compiled to speed up loading. They may also contain code for initialization at runtime.

As a rule, in large Julia packages you will see the module code organized into files, for example

module SomeModule

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

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

end

Files and file names are mostly unrelated to modules. Modules are only related to module expressions. There can be multiple files for each module, and multiple modules for each file. The 'include` function behaves as if the contents of the source code file were processed in the global scope of the containing module. This chapter provides short and simplified examples, so the 'include` function will not be used.

It is recommended not to indent the module body, as this usually leads to indentation in entire files. In addition, it is common to use the UpperCamelCase style for module names (as well as for types) and the plural form, if applicable, especially if the module contains an identifier with a similar name, which will avoid name-level conflicts. Examples:

module FastThings

struct FastThing
    ...
end

end

Namespace Management

Namespace management refers to the language’s capabilities to make names in a module available in other modules. Related concepts and functionality will be discussed in detail below.

Qualified names

The names of functions, variables, and types in the global scope, such as sin, ARGS, and UnitRange, always belong to a module called the parent module_, which can be found interactively using the function parentmodule, for example

julia> parentmodule(UnitRange)
Base

These names can also be referenced outside of the parent module by prefixing them as a module, for example, Base.UnitRange. This name is called qualified. The parent module can be accessed using a chain of submodules of type Base.Math.sin, where Base.Math is called the _ path to module. Due to syntactic ambiguity, a colon must be inserted to qualify a name that contains only characters such as an operator. For example, Base.:+. For a small number of operators, parentheses are additionally required. For example, `Base.:(==)'.

If the name is qualified, it is always available, and if it is a function, methods can be added to the name using the qualified name as the function name.

In the module, the variable name can be reserved without assignment by declaring it as global x'. This prevents name conflicts for global files that are initialized after uploading. The syntax of `M.x = y does not work for assigning a global object in another module. Assignment of a global object is always local and is performed in a single module.

Export Lists

Names (related to functions, types, global variables, and constants) can be added to the module’s export list using the 'export` operator: these are characters that are imported when using the module (using). They are usually located at or near the top of the module definition so that readers of the source code can easily find them, as shown in the example.:

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

But this is just a style suggestion — a module can have multiple export statements in arbitrary locations.

Names that are part of the API (application programming interface) are usually exported. In the above code, the export list prompts users to use nice and `DOG'. However, since qualified names always make identifiers available, this is just an option for organizing the API: unlike other languages, Julia does not have the means to truly hide the internal contents of a module.

In addition, some modules do not export names at all. This usually happens if they use common words in the API, such as derivative, which can lead to conflicts with the export lists of other modules. The management of name conflicts will be discussed further.

To mark a name as public without exporting it to the namespace of those who call using NiceStuff, you can use public instead of export. This marks public names as part of the public API without affecting the namespace. The public keyword is only available in Julia 1.11 and later versions. To maintain compatibility with Julia 1.10 and earlier versions, use the macro @compat from the package https://github.com/JuliaLang/Compat.jl [Compat].

Separate using and import operators

For interactive use, the most common way to load a module is to use the using ModuleName operator. Happens download the code associated with the ModuleName module, and

  1. module name

  2. and the export list items are added to the surrounding global namespace.

Technically, the using ModuleName operator means that a module named ModuleName will be available for name resolution as needed. When a global variable is encountered that has no definition in the current module, the system will search for it among the variables exported by the ModuleName module and use it if it is found there. This means that all use cases of this global variable in the current module will be resolved in favor of defining this variable in the ModuleName module.

To load a module from a package, you can use the using ModuleName operator. To load a module from a locally defined module, add a dot before the module name, for example using .ModuleName.

Let’s continue with our example:

julia> using .NiceStuff

loads the above code, making available NiceStuff' (module name), `DOG and nice'. `Dog is not in the export list, but it can be accessed if the name is qualified using the module path (which here is just the module name) like `NiceStuff.Dog'.

It is important to note that using ModuleName is the only form for which export lists have a value.

In front of:

julia> import .NiceStuff

adds only the module name to the scope. To access its contents, users will need to use NiceStuff.DOG, NiceStuff.Dog and NiceStuff.nice'. Usually, `import ModuleName is used in contexts where the user wants to keep the namespace clean. As we will see in the next section, import .NiceStuff is `equivalent to `using .NiceStuff: NiceStuff.

Several using and import operators of the same type can be combined in a comma-separated expression, for example:

julia> using LinearAlgebra, Random

The using and import operators with specific identifiers and adding methods

When following the using ModuleName: or import ModuleName' operator: a comma-separated list of names follows, the module is loaded, but the _ operator adds only these specific names_ to the namespace. Examples:

julia> using .NiceStuff: nice, DOG

it will import the names nice and `DOG'.

It is important to note that the module name `NiceStuff' will not be present in the namespace. To make it available, it must be listed explicitly, as shown below.

julia> using .NiceStuff: nice, DOG, NiceStuff

If two or more packages/modules export a name, and this name does not refer to the same object in each package, and packages are loaded by using without an explicit list of names, then it is an error to refer to this name without specifying it. Therefore, it is recommended that code designed to be further compatible with future versions of its dependencies and Julia, such as code in released packages, specify the names it uses from each downloaded package, such as using Foo: Foo, f rather than `using Foo'.

In Julia, there are two forms for seemingly the same action, because only the import ModuleName:f operator allows you to add methods to f _ without the path to module_. That is, the following example will lead to an error:

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

This error prevents accidental addition of methods to functions in other modules that were just planned to be used.

There are two ways to solve this problem. You can always specify function names using the module path.

julia> using .NiceStuff

julia> struct Cat end

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

Or you can import a specific function name using import.

julia> import .NiceStuff: nice

julia> struct Cat end

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

Their choice depends on the style. The first form makes it clear that you are adding a method to a function in another module (remember that the import and method definition can be in separate files), and the second form is shorter, which is especially convenient if you define several methods.

As soon as a variable becomes visible when using the using or import operator, the module will not be able to create its own variable with the same name. Imported variables are read-only. Assigning a global variable always affects the variable belonging to the current module, otherwise an error occurs.

Renaming using the keyword as

An identifier added to an area using the import or using operator can be renamed using the as keyword. This is useful for resolving name conflicts as well as shortening names. For example, the Base module exports the function name read', but the package is CSV.jl also provides 'CSV.read'. If you plan to call the CSV read operation many times, it is advisable to abandon the `CSV. classifier. But then it remains unclear what we are referring to — Base.read or `CSV.read'.

julia> read;

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

This problem can be solved by renaming.

julia> import CSV: read as rd

You can also rename the imported packages themselves.

import BenchmarkTools as BT

The as keyword works with the using operator only when a single identifier falls into the scope. For example, using CSV: read as rd is supported, but using CSV as C' is not, because it works with all exported names in `CSV.

Mixing multiple expressions with the using and import operators

When multiple using or import operators of any of the above forms are used, their action is combined in the order they appear. Examples:

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

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

It will enter all exported names NiceStuff and the module name itself into the scope, and will also allow adding methods to nice without specifying the module name as a prefix.

Resolving name conflicts

Consider a situation where two or more packages export the same name, as in the example below.

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

The using' operator .A, .B works, but when you try to call the function f, an error with a prompt is displayed.

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.

Here Julia cannot determine which function f you are referring to, so you will have to make the choice. The following solutions are commonly used.

  1. Continued using qualified names such as A.f and B.f. In this case, the context will be clear to the reader of the code, especially if the function f simply matches, but has different meanings in different packages. For example, degree is used differently in mathematics, natural sciences, and everyday life, and these meanings should be separated.

  2. Using the keyword as to rename one identifier or both, for example:

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

    'B.f` will be available as g. This assumes that you have not used the using A operator before, which would have added the f function to the namespace.

  3. When the names in question have a common meaning, as a rule, one module imports it from another or has a simplified, basic package with the only function of defining an interface like this that other packages can use. Usually the names of such packages end with '...Base (which has nothing to do with the Base module in Julia).

Default top-level definitions and empty modules

Modules automatically contain the operators using Core, using Base and function definitions. eval and include, which evaluate expressions or files in the global scope of this module.

If these definitions are not needed by default, modules can be defined using a keyword baremodule (note: The Core module is still being imported). From the point of view of the baremodule module` the standard module looks like this.

baremodule Mod

using Base

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

...

end

Even if the Core module is not required, a module that does not import anything and does not define any names can be defined using Module(:YourNameHere, false, false), and the code in it can be processed using a macro @eval or functions 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

There are three important standard modules.

  • 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.

!!! note "Standard library modules" By default, Julia includes several standard library modules. They function like regular Julia packages, except that they do not need to be explicitly installed. For example, if you need to perform unit testing, you can download the standard library `Test' as follows:

    using Test

Submodules and relative paths

Modules can contain modules in which the same syntax module …​ end+ is nested. They can be used to introduce separate namespaces, which can be useful for organizing complex code bases. Note that each module introduces its own scope, so submodules do not automatically inherit names from their parent module.

It is recommended that submodules refer to other modules within the enclosing parent module (including the last one) using the _ relative module qualifiers in the using and import statements. The relative module qualifier starts with a dot (.), which corresponds to the current module, and each subsequent character is . leads to the parent module of the current module. It should be followed by modules, if necessary, and eventually the actual name to be accessed. All elements are separated by the . symbol.

Consider the following example, where the submodule SubA defines a function, which is then expanded in its single-level module:

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;

You can see the code in the packages, which in a similar situation uses

julia> import .ParentModule.SubA: add_D

However, it works through code download and therefore only works if the `ParentModule' module is in the package. It is recommended to use relative paths.

Note that when calculating values, the order of definitions is also important. Consider

module TestPackage

export x, y

x = 0

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

y = 1

end

where the Sub module tries to use the TestPackage' variable.y before it was defined, so it has no value.

For similar reasons, it is not possible to use cyclic ordering.

module A

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

module C
using ..B
end

end

Initialization and pre-compilation of the module

It can take several seconds to load large modules, as it often requires a significant amount of code to compile in order to execute all the statements in a module. To reduce this time, Julia creates precompiled module caches.

When loading a module using the import or using operator, precompiled module files (sometimes called "cache files") are automatically created and used. If there are no cache files yet, the module will be compiled and saved for future reuse. To create these files without loading the module, you can also call Base.compilecache(Base.identify_package("modulename")) manually. The resulting cache files will be saved in the compiled subfolder in the DEPOT_PATH[1] folder. If nothing has changed in your system, these cache files will be used when loading the module using the import or using operator.

Precompiled files contain definitions of modules, types, methods, and constants. They can also contain method specializations and code created for them, but usually the developer must explicitly add directives for this. precompile or perform workloads that force compilation during package build.

However, if you update the module’s dependencies or change its source code, the module will be automatically recompiled when using the using or import operator. Dependencies are imported modules, the Julia build, included files, or explicit dependencies declared using a function. include_dependency(path) in the module files.

For file dependencies uploaded using include, the change is determined by analyzing whether the file size (fsize) or its contents (compressed into a hash) are unchanged. For file dependencies loaded using include_dependency, the change is determined by analyzing whether the modification time (mtime) is unchanged or equal to the modification time truncated to the nearest second (for systems that cannot copy mtime with precision in fractions of a second). It also takes into account whether the file path selected by the search logic in require matches the path used to create the pre-compilation file. In addition, a set of dependencies already loaded into the current process is taken into account. These modules will not be recompiled, even if their files change or disappear, to avoid incompatibilities between the running system and the pre-compilation cache. Finally, changes in any compile-time settings.

If it is known that the precompilation of a module is unsafe (for example, for one of the reasons described below), you must place the function __precompile__(false) in the module file (usually at the top). As a result, the Base.compilecache function will return an error, and the using or import operator will load the module directly into the current process and skip the pre-compilation and caching procedures. It also prevents the module from being imported by any other precompiled module.

You may need to be aware of some of the behavioral features inherent in creating additional shared libraries that require attention when writing a module. For example, the external state is not saved. To take this point into account, it is necessary to explicitly separate the initialization steps that must be performed during execution from the steps that may occur during compilation. To do this, Julia allows you to define the function __init__() in the module, which performs any initialization steps that should occur during execution. This function will not be called during compilation (--output-*). In fact, we can assume that it will be executed exactly once during the entire lifetime of the code. Of course, you can call it manually if necessary, but by default it is assumed that this function works with the computing state for the local computer, which does not need to be fixed in the compiled image. The function will be called after the module is loaded into the process, including if it is loaded into the incremental compilation process (--output-incremental=yes), but will not be called if the module is loaded into the full compilation process.

In particular, if you define the function function __init__() in the module, Julia will call the function __init__()`immediately _ after loading the module (for example, using the `import, using, or require statements) during execution for the first time (i.e., the function __init__ is called only once and only after executing all the statements in the module). Since the function is called after the module is fully imported, the functions of the __init__ submodules or other imported modules are called _ before the '__init__' function of the enclosing module.

Two typical uses of the __init__ function are calling the runtime initialization functions of external C libraries and initializing global constants, which use pointers returned by external libraries. For example, suppose you need to call the C library libfoo', for which you need to call the initialization function `foo_init() at runtime. Let’s assume that we should also preempt the global constant foo_data_ptr, which will store the return value of the void *foo_data() function defined by the libfoo library. This constant must be initialized at runtime (not at compile time), because the address of the pointer will change with each execution. This can be done by defining the following function in the module `__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

Note that it is quite possible to define a global object inside a function of type __init__'. This is one of the advantages of using a dynamic language. By making an object a constant in the global scope, you can ensure that the type is known to the compiler and allow it to generate more optimized code. Obviously, any other global objects in the module that depends on `foo_data_ptr must also be initialized in the `__init__' function.

Constants associated with most Julia objects that are not created using a keyword 'ccall`, does not need to be placed in the function __init__': their definitions can be precompiled and loaded from the cached module image. These include complex heap-allocated objects such as arrays. However, any subroutine that returns the raw pointer value should be called at runtime, otherwise no precompilation will be performed (objects `Ptr will turn into null pointers unless they are hidden inside the object. isbits). This includes the return values of Julia functions. @cfunction and pointer.

The situation with dictionary types and set types, or in general with everything that depends on the output of the hash(key) method, is much more complicated. In general, when the keys are numbers, strings, symbols, ranges, Expr expressions, or compositions of these types (via arrays, tuples, sets, pairs, etc.), pre`compilation is performed without problems. However, for some other key types, such as Function or DataType, and universal user-defined types for which no hash method is defined, the backup hash method depends on the object’s memory address (via its objectid identifier) and, therefore, may change with each execution. If you have one of these types of keys or you are not sure, for security reasons, you can initialize this dictionary in the function __init__'. Alternatively, you can use a dictionary type. `IdDict, which is specially processed as part of pre-compilation so that it can be safely initialized during compilation.

When using precompilation, it is important to clearly understand the difference between the compilation stage and the execution stage. In this mode, it is often much more obvious that Julia is a compiler that allows arbitrary Julia code to be executed, rather than a separate interpreter that also generates compiled code.

Other known potential failure scenarios are listed below.:

  1. Global counters (for example, to attempt unique identification of objects). Consider the following code snippet.

     mutable struct UniquedById
     myid::Int let counter = 0 UniquedById() = new(counter += 1) end end
while the intent of this code was to give every instance a unique id, the counter value is recorded
at the end of compilation. All subsequent usages of this incrementally compiled module will start
from that same counter value.

Note that `objectid` (which works by hashing the memory pointer) has similar issues (see notes
on `Dict` usage below).

One alternative is to use a macro to capture [`@+++__MODULE__+++`](@ref) and store it alone with the current `counter` value,
however, it may be better to redesign the code to not depend on this global state.
2. Associative collections (such as `Dict` and `Set`) need to be re-hashed in `+++__init__+++`. (In the
future, a mechanism may be provided to register an initializer function.)
3. Depending on compile-time side-effects persisting through load-time. Example include: modifying
arrays or other variables in other Julia modules; maintaining handles to open files or devices;
storing pointers to other system resources (including memory);
4. Creating accidental "copies" of global state from another module, by referencing it directly instead
of via its lookup path. For example, (in global scope):

julia mystdout = Base.stdout #= will not work correctly because Base.stdout will be copied to this module =

Instead, use the access method functions.:

getstdout() = Base.stdout = best option =

Or move the assignment to the runtime.:

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

There are several additional restrictions for operations that can be performed during pre-compilation of the code, designed to help the user avoid other situations with incorrect behavior.

  1. Function call eval to cause a side effect in another module. This will also trigger a warning if the incremental precompilation flag is set.

  2. global const statements from the local scope after running the function __init__() (see the description of issue # 12010 with plans to add the corresponding error).

  3. Module replacement is a runtime error when performing incremental precompilation.

Below are a few more points that you should pay attention to.

  1. Code reloading or cache cancellation is not performed after making changes to the source files themselves (including using Pkg.update), and cleanup is not performed after Pkg.rm.

  2. During pre-compilation, the memory sharing behavior of an array with a modified shape is ignored (each representation gets its own copy).

  3. Waiting for file system immutability between compile time and run time, for example '@__FILE__' or source_path() to search for resources at runtime, or the BinDeps macro `@checked_lib'. Sometimes it’s unavoidable. However, whenever possible, it is recommended to copy resources to the module at compile time so that they do not need to be searched at runtime.

  4. At the moment, the serializer does not properly handle WeakRef objects and finalizers (this will be fixed in the next release).

  5. It is usually better to avoid writing references to instances of internal metadata objects such as Method, MethodInstance, MethodTable, TypeMapLevel, TypeMapEntry and fields of these objects, as this may confuse the serializer and not lead to the desired result. The recording won’t necessarily be considered an error, but you just have to be prepared for the system to try to copy some of them and create a single unique instance of the others.

Sometimes it is advisable to disable incremental pre-compilation during module development. You can enable or disable pre-compilation of a module using the command-line flag --compiled-modules={yes|no|existing}. When Julia is started with the --compiled-modules=no flag set, the serialized modules in the compilation cache are ignored when loading modules and module dependencies. In some cases, you may need to download existing precompiled modules, but not create new ones. This can be done by running Julia with --compiled-modules=existing'. More detailed control is provided by the `--pkgimages={yes|no|existing}, which only acts to store the machine code during pre-compilation. The Base.compilecache function can still be called manually. The status of this command line flag is passed to the 'Pkg' script.build` to disable the automatic start of pre-compilation when installing, updating, and explicitly building packages.

Some pre-compilation failures can also be debugged using environment variables. If you set `JULIA_VERBOSE_LINKING=true', it can help eliminate failures when linking shared libraries of compiled machine code. In the developer documentation in the Julia manual, there is a section on the features of the Julia internal device, in the chapter "Package Images", where you can find more detailed information.