Using Modules

Last updated on 2023-09-15 | Edit this page

Estimated time: 15 minutes

Overview

Questions

  • “What’s the purpose of modules?”

Objectives

  • “Structure your code using modules”
  • “Use Revise.jl to track changes”

Modules


Melissa now has a bunch of definitions in her running Julia session and using the REPL for interactive exploration is great, but it is more and more taxing to keep in mind what is defined, and all the definitions are lost once she closes the REPL.

That is why she decides to put her code in a file. She opens up her text editor and creates a file called aim_trebuchet.jl in the current working directory and pastes the code she got so far in there. This is what it looks like:

JULIA

using Pkg
Pkg.activate("projects/trebuchet")
import Trebuchet as Trebuchets
using ForwardDiff: gradient

mutable struct Trebuchet <: AbstractVector{Float64}
  counterweight::Float64
  release_angle::Float64
end

Base.size(trebuchet::Trebuchet) = tuple(2)

Base.getindex(trebuchet::Trebuchet, i::Int) = getfield(trebuchet, i)

function Base.setindex!(trebuchet::Trebuchet, v, i::Int)
    if i === 1
        trebuchet.counterweight = v
    elseif i === 2
        trebuchet.release_angle = v
    else
        error("Trebuchet only accepts indices 1 and 2, yours is $i")
    end
end

struct Environment
  wind::Float64
  target_distance::Float64
end

function shoot_distance(args...)
    Trebuchets.shoot(args...)[2]
end

function shoot_distance(trebuchet::Trebuchet, env::Environment)
    shoot_distance(env.wind, trebuchet.release_angle, trebuchet.counterweight)
end

function aim(trebuchet::Trebuchet, environment::Environment; ε = 1e-1, η = 0.05)
    better_trebuchet = copy(trebuchet)
    hit = x -> (shoot_distance([environment.wind, x[2], x[1]]) - environment.target_distance)
    while abs(hit(better_trebuchet)) > ε
        grad = gradient(hit, better_trebuchet)
        better_trebuchet -= η * grad
    end
    return Trebuchet(better_trebuchet[1], better_trebuchet[2])
end

imprecise_trebuchet = Trebuchet(500.0, 0.25pi)

environment = Environment(5, 100)

precise_trebuchet = aim(imprecise_trebuchet, environment)

shoot_distance(precise_trebuchet, environment)

Now Melissa can run include("aim_trebuchet.jl") in the REPL to execute her code.

She also recognizes that she has a bunch of definitions at the beginning that she doesn’t need to execute more than once in a session and some lines at the end that use these definitions which she might run more often. She will split these in two separate files and put the definitions into a module. The module will put the definitions into their own namespace which is the module name. This means Melissa would need to put the module name before each definition if she uses it outside of the module. But she remembers from the Using the package manager Episode that she can export names that don’t need to be prefixed.

She names her module MelissasModule and accordingly the file MelissasModule.jl. From this module she exports the names aim, shoot_distance, Trebuchet and Environment. This way she can leave her other code unchanged.

JULIA

module MelissasModule

using Pkg
Pkg.activate("projects/trebuchet")
import Trebuchet as Trebuchets
using ForwardDiff: gradient

export aim, shoot_distance, Trebuchet, Environment

mutable struct Trebuchet <: AbstractVector{Float64}
  counterweight::Float64
  release_angle::Float64
end

Base.size(trebuchet::Trebuchet) = tuple(2)

Base.getindex(trebuchet::Trebuchet, i::Int) = getfield(trebuchet, i)

function Base.setindex!(trebuchet::Trebuchet, v, i::Int)
    if i === 1
        trebuchet.counterweight = v
    elseif i === 2
        trebuchet.release_angle = v
    else
        error("Trebuchet only accepts indices 1 and 2, yours is $i")
    end
end

struct Environment
  wind::Float64
  target_distance::Float64
end

function shoot_distance(args...)
    Trebuchets.shoot(args...)[2]
end

function shoot_distance(trebuchet::Trebuchet, env::Environment)
    shoot_distance(env.wind, trebuchet.release_angle, trebuchet.counterweight)
end

function aim(trebuchet::Trebuchet, environment::Environment; ε = 1e-1, η = 0.05)
    better_trebuchet = copy(trebuchet)
    hit = x -> (shoot_distance([environment.wind, x[2], x[1]]) - environment.target_distance)
    while abs(hit(better_trebuchet)) > ε
        grad = gradient(hit, better_trebuchet)
        better_trebuchet -= η * grad
    end
    return Trebuchet(better_trebuchet[1], better_trebuchet[2])
end
end # MelissasModule

The rest of the code goes to a file she calls MelissasCode.jl.

JULIA

using .MelissasModule

imprecise_trebuchet = Trebuchet(500.0, 0.25pi)
environment = Environment(5, 100)
precise_trebuchet = aim(imprecise_trebuchet, environment)
shoot_distance(precise_trebuchet, environment)

Now she can include MelissasModule.jl once, and change and include MelissasCode.jl as often as she wants. But what if she wants to make changes to the module? If she changes the code in the module, re-includes the module and runs her code again, she only gets a bunch of warnings, but her changes are not applied.

Revise.jl


Revise.jl is a package that can keep track of changes in your files and load these in a running Julia session.

Melissa needs to take two things into account:

  • using Revise must come before using any Package that she wants to be tracked
  • she should use includet instead of include for included files (t for “tracking”)

Thus she now runs

JULIA

using Revise


includet(joinpath(path,"MelissasModule.jl"))
include(joinpath(path,"MelissasCode.jl"))

OUTPUT

100.05601729579894

where path is the path to her files.

and any change she makes in MelissasModule.jl will be visible in the next run of her code.

Did I say any changes?

Well, almost any. Revise can’t track changes to structures.

Key Points

  • “Modules introduce namespaces”
  • “Public API has to be documented and can be exported”