Modules and files
In this document we will learn how VPL code can be organized into files and modules.
Files
Julia code can be organized into files, which are plain text files with a .jl
extension. A file can contain multiple functions, types, and variables. We can load the code from a file using the include
function. For example, if we have a file named my_functions.jl
that contains the following code, we can load this file into the current Julia session using:
include("my_functions.jl")
This will simply execute the code in the file, so it is equivalent to copying and pasting the code into the current session.
When developing a VPL model you want to be able to makes changes to the code and run it without having to restart the Julia session. For functions this is not really an issue but redefinition of types (struct
or mutable struct
) is not allowed in Julia within the same session. To bypass this we need to use the following strategy:
- Make sure that the package
Revise.jl
is installed and loaded. - Use
includet
instead ofinclude
to load files. - Place all type definitions and global variables inside one or more modules.
This means that, when building a typical VPL model, you should include at least one module to host the type definitions for the different organs in your model. Of course, for large models you may want to consider splitting your code into multiple files and modules to keep it organized and manageable.
Note that there are alternative ways to bypass the type redefinition issue, such as using the ProtoStruct.jl
package.
Modules
In Julia, a module is a collection of related functions, types, and variables that can be grouped together to form a namespace. Modules help in organizing code and avoiding name clashes. A module can be defined using the module
keyword, and it can export specific functions or types to make them available outside the module.
For example, the following code defines a simple module named MyModule
that exports a function my_function
:
module MyModule
export my_function
function my_function(x)
return x + 1
end
end
We can refer to the name of the module as .MyModule
where the .
indicates that it is a local module defined in the current scope (Julia packages also defined modules, but they are not prefixed with a .
).
using .MyModule
result = my_function(5) # This will return 6
Modules can also be nested, allowing for better organization of code. For example, we can define a module OuterModule
that contains another module InnerModule
:
module OuterModule
module InnerModule
export inner_function
function inner_function(x)
return x * 2
end
end # End of InnerModule
export outer_function
function outer_function(x)
return x + 2
end
end # End of OuterModule
We can use the nested module as follows (not that .
is used to separated nested modules):
using .OuterModule.InnerModule
result_inner = inner_function(3) # This will return 6
using .OuterModule
result_outer = outer_function(3) # This will return 5
The keyword using
will make available all exported functions and types from the module, while import
will only make the module available without importing its exported functions or types. This allows for more control over what is imported into the current namespace but we need to prefix each function or type with the module name to use it:
import .OuterModule.InnerModule
result_inner = OuterModule.InnerModule.inner_function(3) # This will return 6
result_outer = OuterModule.outer_function(3) # This will return 5
If the name of the module is too long or cumbersome, we can use the as
keyword to create an alias for the module:
using .OuterModule.InnerModule as Inner # Now Inner refers to OuterModule.InnerModule
result_inner = Inner.inner_function(3) # This will return 6
Also, we can import specific functions or types from a module using the import
keyword:
import .OuterModule.InnerModule: inner_function
result_inner = inner_function(3) # This will return 6
A Julia source file can contain multiple modules, but a module can only be defined within a single source file.
Defining methods for existing functions
If you want to add a method to an existing function so that it works with a new type (e.g., the feed!
methods that are used in VPL to generate geometry), you need to define the method by prefixing the function name with the module name where the function is defined. As you will see in the tutorials, this means we define feed!
methods as follows:
function VirtualPlantLab.feed!(turtle::Turtle,, ...)
# Implementation of the method
end
It is important to do this to make sure that we are creating a method for the feed!
function even if you used using VirtualPlantLab
.
How to organize your code
VPL models can become quite complex, depending on how much functionality is added. There is no correct way to organize your code into multiples files and modules but it helps to think for a bit why do we even want to do this (i.e., as said before, you probably want to have at least one modules for your organ types but you could write an entire model in a single file, as in the tutorials in this website).
We generally want to have multiple files to make sure that, when we are editing a particular aspect of the code, we visit as few files as possible. The reason for this is that it is always easier to search within a single file and to jump up and down in the code than to switch files (though searching across files is also possible in most editors).
We also want to have multiple files if we expect to reuse some of the code for other models or simpler version of our current model. This touches on the issue of modularity, whereby we want to organized different parts of the code that can be reused in different contexts.
Regarding modules, the main reason for their use (besides what was described earlier about types) is to avoid clashes between function names and global variables. This is very much related to the issue of modularity, as we want to be able to reuse code without worrying about having to rename functions or variables inside the code (i.e., a module helps build the abstraction of a black box that can be used without knowing its internal details). This may never be required in small models (or even in large models that are not meant to be reused), so just use your own judgement to decide how to organize your code based on your specific needs.