Setting up your Julia session

By: Blog by Bogumił Kamiński

Re-posted from: https://bkamins.github.io/julialang/2023/12/08/plot.html

Introduction

A nice feature of Julia is that supports startup file that contains the code
that is executed every time you start a new Julia session.

This functionality is described in the Julia Manual.
In this post I want to discuss this functionality from the user’s perspective.

The basics of startup file

The startup file is typically located at ~/.julia/config/startup.jl folder
(I am using Linux defaults here). However, this default location can be modified.
You can change it by manipulating environment variables. These more advanced
configuration options are described here in the Julia Manual.

The startup.jl file contains the commands that are executed when Julia is run.

What are typical entries of the startup file?

Usually people load utility packages that they routinely need when working with
Julia (to avoid having to manually load them each time). Some popular examples are:

  • Revise.jl: allowing to modify source code without having to restarting Julia;
  • OhMyREPL.jl: advanced highlighting support in Julia terminal;
  • JET.jl: code analyzer for Julia;
  • BenchmarkTools.jl: performance tracking of Julia code.

Another use programmatic setting of preferences, for example:

  • setting default code editor via ENV["EDITOR"] variable;
  • automatic activation of project environment; I discussed it some time ago in this post.

Selective execution of code in startup file

Julia can be started in two modes:

  • interactive (called REPL);
  • script execution.

Some of the features are useful only in REPL mode. For example, loading OhMyREPL.jl
probably does not have much value added when executing a script.

Julia allows you to add code that should run only when it is activated in REPL mode
by registering a function that is run only in this mode. This registration is achieved
via the atreplinit function that should be defined in your startup.jl file.
It is done in the following way (the code inside the function is just an example):

atreplinit() do repl
    println("This is printed only in REPL, but not when executing a script")
end

Disabling loading of a startup file

In some cases, especially if you run some third party Julia code, you might want
to disable loading the startup.jl file. This can be achieved by
passing the --startup-file=no command line argument.

Why could you want to do it? In this way you make sure that the code you have in
your startup.jl does not conflict with the code you want to run (this situation
is rare, but is possible).

Conclusions

Some Julia users prefer not to define startup.jl at all and always be explicit
about what is loaded when Julia is started. This scenario is probably most common
when someone mostly runs Julia scripts as it ensures a clean environment and fastest
load times.

However, in many users, especially if they work interactively a lot, like to initialize
their Julia session each time it is started with some standard code.
For example, if you are a data scientist you might want to always load for example
CSV.jl, DataFrames.jl, Statistics.jl, and StatsBase.jl when you are working in REPL mode
to avoid having to load these packages manually every time you start a new session.

When working with startup.jl files there are two things worth remembering:

  1. You can selectively decide which code is loaded only if you work in REPL mode
    via the atreplinit function.
  2. Even if you have startup.jl file you can fully disable loading it by passing
    the --startup-file=no command line option.

Happy hacking!

Exploring Modules and Variable Scope in Julia Programming

By: Great Lakes Consulting

Re-posted from: https://blog.glcs.io/modules-variable-scope

Julia is a relatively new,
free, and open-source programming language.
It has a syntax
similar to that of other popular programming languages
such as MATLAB and Python,
but it boasts being able to achieve C-like speeds.

One way to organize Julia code
is to split functionality
into individual functions.
When enough functions exist,
it may become useful
to group the functions together,
along with any relevant global variables,
constants,
and type definitions.
Julia provides modules for this purpose.

Modules form the backbone of Julia packages,
helping to organize code
and minimize namespace collisions.

In this post,
we will learn about
modules in Julia,
and we will discuss how to create and use them.
Because modules each have their own global scope,
we will also learn about
scoping rules for variables.

This post assumes you already have
a basic understanding of variables and functions
in Julia.
You should also understand the difference
between functions and methods.
If you haven’t yet,
check out our earlier
post on variables and functions
as well as our post on multiple dispatch,
which explains the difference
between functions and methods.

Modules

The syntax for creating a module is

module ModuleName

# Code goes here.

end

Here’s an example module:

module MyModule

using Statistics

const A = "A global constant"
const B = [1, 2, 3]

func(x) = println("A: ", A, "\nmean(B): ", mean(B), "\nx: ", x)

export A, func

end

Let’s walk through this code.

  • First,
    the module loads another package:

    using Statistics
    

    Modules can load packages,
    just like we can do in the REPL.
    When a package is loaded in a module,
    the package is brought into the module’s namespace,
    meaning the loaded symbols
    (i.e., names referring to functions, types, constants, etc.)
    are not visible outside of the module.
    For example:

    julia> module StatsModule
           using Statistics
           end
    Main.StatsModule
    
    julia> mean([1, 2, 3])
    ERROR: UndefVarError: `mean` not defined
    
  • Next,
    the module defines its own data and functionality:

    const A = "A global constant"
    const B = [1, 2, 3]
    
    func(x) = println("A: ", A, "\nmean(B): ", mean(B), "\nx: ", x)
    

    This is the code
    we want to organize into a module.
    Typically,
    there are more lines of code,
    and they are saved in one or more separate files
    that are just included by the module via

    include("mycode.jl")
    
  • Finally,
    the module specifies some exports:

    export A, func
    

    Modules can export symbols
    that are then made available
    when the module is loaded with using.

Using Modules

When a module is created,
it can be referred to
by its name,
and any symbols in its namespace
can be accessed
by prepending the module name,
e.g., MyModule.func.
(This is called a qualified name.)

julia> MyModule
Main.MyModule

julia> MyModule.func(1)
A: A global constant
mean(B): 2.0
x: 1

If we want to make exported symbols
available without using a qualified name,
we can load the module with using:

julia> using .MyModule

julia> A
"A global constant"

(Here we note one difference
between packages and modules:
packages can be loaded with using PackageName,
whereas modules need their name
to be prepended with a period,
as seen above.)

After loading a module with using,
unexported symbols
are not made directly available,
but they can still be accessed
via a qualified name:

julia> B
ERROR: UndefVarError: `B` not defined

julia> MyModule.B
3-element Vector{Int64}:
 1
 2
 3

If we want to make an unexported symbol
available without using a qualified name,
we can explicitly load it:

julia> using .MyModule: B

julia> B
3-element Vector{Int64}:
 1
 2
 3

import Statements

import is another keyword
that can be used to load modules and packages.

import .MyModule will make available
just the name MyModule,
not any exported symbols:

julia> import .MyModule

julia> func(false) # Error, even though `func` is exported
ERROR: UndefVarError: `func` not defined

julia> MyModule.func(false) # Qualified names still work
A: A global constant
mean(B): 2.0
x: false

Using vs. import

import also allows methods
to be added to a module’s functions
without using a qualified name:

julia> import .MyModule: func

julia> func() = println("Method 2")
func (generic function with 2 methods)

julia> func()
Method 2

julia> func("MyModule.func")
A: A global constant
mean(B): 2.0
x: MyModule.func

For comparison,
below are two similar examples that use using:

  1. julia> using .MyModule: func
    
    julia> func() = println("Method 2")
    ERROR: error in method definition: function MyModule.func
    must be explicitly imported to be extended
    

    Here,
    we learn that we cannot add a method
    to a function from another module
    without importing the function,
    as we did earlier,
    or referring to the function
    with its qualified name,
    as shown below:

    julia> using .MyModule
    
    julia> MyModule.func() = println("Method 2")
    
    julia> func()
    Method 2
    
    julia> func("MyModule.func")
    A: A global constant
    mean(B): 2.0
    x: MyModule.func
    
  2. julia> using .MyModule
    
    julia> func() = println("Method 2")
    func (generic function with 1 method)
    
    julia> func()
    Method 2
    
    julia> func("MyModule.func")
    ERROR: MethodError: no method matching func(::String)
    
    julia> MyModule.func("MyModule.func")
    A: A global constant
    mean(B): 2.0
    x: MyModule.func
    

    Here,
    we see that,
    even though func is exported from MyModule,
    we created a different func function
    in the REPL
    because we did not import func
    or use a qualified name.
    As a result,
    future uses of func from MyModule
    must use its qualified name.

Adding methods to a module's function

Finally,
import enables renaming symbols:

julia> import .MyModule as MM

julia> MM.A
"A global constant"

julia> import .MyModule: B as NEWNAME

julia> NEWNAME
3-element Vector{Int64}:
 1
 2
 3

Common Modules

Now that we know how modules work,
let’s learn about a few modules
that every Julia programmer will come across:
Main, Base and Core.

  • Main:
    It turns out that all Julia code
    executes within a module.
    When Julia starts,
    a module named Main is created,
    and code that runs
    that isn’t explicitly contained in a module
    (e.g., code in the REPL)
    is executed within Main.
  • Base:
    Much of Julia’s basic functionality,
    including functions like
    +, print, and getindex,
    is defined in a module named Base.
    This module is automatically loaded
    into all modules
    (with few exceptions).
  • Core:
    Code that is considered built-in to Julia,
    i.e., code Julia needs to be able to function,
    lives in a module named Core.
    This module also is automatically loaded
    into all modules
    (with even fewer exceptions).

Variable Scope

Variable scope refers to where in code
a variable is accessible.
It therefore has implications
for when two pieces of code
can use the same variable name
without referring to the same thing.

There are three types of scopes in Julia:
global scope, hard local scope, and soft local scope.
We will discuss each of these in turn.

Global Scope

Symbols defined within a global scope
can be accessed within the global scope
and any local scopes
contained in the global scope.

Each module defines its own global scope.
Importantly,
there is no universal global scope,
meaning there is nowhere
we can define, e.g., x
and have x refer to the same thing everywhere,
even across modules.

Also note that global scopes do not nest,
in the sense that a nested module
cannot refer to a containing module’s global variable:

julia> module A
           a = 1
           module B
               b = a # `a` is undefined here, even though `B` is nested within `A`
           end
       end
ERROR: UndefVarError: `a` not defined

Hard Local Scope

Symbols defined within a hard local scope
can be accessed within the local scope
and any contained local scopes.

Functions, let blocks, and comprehensions
each introduce a hard local scope.

In a hard local scope,
variable assignment always assigns
to a local variable
unless the variable is explicitly declared as global
using the global keyword:

julia> let
           x = 1 # Assigns to a local variable `x`
       end;

julia> x
ERROR: UndefVarError: `x` not defined

julia> let
           global x
           x = 1 # Assigns to a global variable `x`
       end;

julia> x
1

Furthermore,
if there is a local variable, e.g., x,
in an outer local scope,
assignment to x in an inner local scope
will assign to the same x
in the outer local scope:

julia> let
           x = 0
           for i = 1:10
               s = x + i
               x = s # `x` is the existing local variable, not a new one
           end
           x # 55, not 0
       end
55

In particular,
it will not create a new local named x
unless x is explicitly declared local
in the inner scope.
This is called shadowing,
where names can be reused
to refer to different things:

julia> x = 1; # A global `x`

julia> let
           x = 2 # A local `x` shadowing the global `x`
           let
               local x
               x = 3 # An inner local `x` shadowing the outer local `x`
               @show x # Shows `x = 3`
           end
           @show x # Shows `x = 2`, not `x = 3`
       end;
x = 3
x = 2

julia> @show x; # Shows `x = 1`, not `x = 2` or `x = 3`
x = 1

Another example of shadowing:

julia> x = 1; # A global `x`

julia> function f(x)
           x + 1 # This `x` refers to the local `x`, not the global one
       end;

julia> f(3) # Computes `3 + 1`, not `1 + 1`
4

Soft Local Scope

for, while, and try blocks
each introduce a soft local scope.

Soft local scope is the same as hard local scope
except in interactive contexts
(e.g., when running code in the REPL)
and when assigning to a variable
(let’s call it x)
while the following conditions are met:

  1. x is not already a local variable.
  2. All enclosing local scopes are soft.
    (To illustrate,
    nested for loops within the REPL
    would satisfy this condition,
    while a for loop in a function
    would not.)
  3. A global variable x is defined.

In this case,
the global variable x is assigned
(as opposed to creating a new local variable x,
as would be done in a hard local scope).
(See the Julia documentation
for the rationale.)

Variable scope

Summary

In this post,
we learned how to create and use modules in Julia
for organizing code.
We also learned about Julia’s scoping rules
for global, hard local, and soft local scopes.

How do you use modules in your code?
Let us know in the comments below!

Additional Links

Is Makie.jl up to speed?

By: Blog by Bogumił Kamiński

Re-posted from: https://bkamins.github.io/julialang/2023/12/01/plot.html

Introduction

Makie is a plotting ecosystem for the Julia language that is extremely feature-packed and actively developed.
Recently its core package Makie.jl reached version 0.20. Its core developer Simon told me that
the package now loads much faster than it was the case in the past.

The “time to first plot” issue is often raised by new users of the Julia ecosystem as important.
Therefore a lot of work was put both by Julia core developers and by package maintainers to reduce it.

In this post I thought that it would be interesting to check how CairoMakie.jl compares to Plots.jl.
The Plots.jl package is another great plotting ecosystem for Julia. It is more lightweight, so in the past it was seen
as faster, but less feature rich. Let us see how the situation stands currently.
From the Makie ecosystem I have chosen CairoMakie.jl as I typically need 2-D production-quality plots.

The code in this post was tested under Julia Version 1.10.0-rc1, CairoMakie.jl 0.11.2 and Plots.jl 1.39.0.

Installing the package

This, and the following tests, are done in separate Julia sessions in separate project environments
for both plotting ecosystems.

The first timing we make is package installation. The results are the following:

  • CairoMakie.jl 0.11.2: 241 dependencies successfully precompiled in 227 seconds
  • Plots.jl v1.39.0: 62 dependencies successfully precompiled in 78 seconds

CairoMakie.jl takes three times more time to install and has four times more dependencies.
Installation is one-time cost, however, there are two considerations to keep in mind:

  • New users are first faced with installation time so it is noticeable (this is especially relevant with Pluto.jl).
  • Since CairoMakie.jl has many more dependencies it is more likely that it will require recompilation when any of them gets updated.

Loading the package

After installing packages we can check how long it takes to load them:

julia> @time using CairoMakie
  3.580779 seconds (2.75 M allocations: 181.590 MiB, 4.39% gc time, 1.71% compilation time: 49% of which was recompilation)

vs

julia> @time using Plots
  1.296026 seconds (1.05 M allocations: 73.541 MiB, 6.00% gc time, 2.19% compilation time)

CairoMakie.jl takes around three times more time to load. This difference is noticeable, but I think it is not a show-stopper in most cases.
Having to wait 3.5 seconds for a package to load should be acceptable unless someone expects to run a really short-lived Julia session.

Simple plotting

Now comes the time to compare plotting time. Start with CairoMakie.jl:

julia> x = range(0, 10, length=100);

julia> y = sin.(x);

julia> @time lines(x, y)
  0.559800 seconds (476.77 k allocations: 32.349 MiB, 3.13% gc time, 94.66% compilation time)

julia> @time lines(x, y)
  0.012473 seconds (32.40 k allocations: 2.128 MiB)

vs Plots.jl:

julia> x = range(0, 10, length=100);

julia> y = sin.(x);

julia> @time plot(x, y)
  0.082866 seconds (9.16 k allocations: 648.188 KiB, 97.32% compilation time)

julia> @time plot(x, y)
  0.000508 seconds (484 allocations: 45.992 KiB)

The situation repeats. CairoMakie.jl is visibly slower, but having to wait 0.5 second for a first plot to be sent to the plotting engine is I think acceptable.
Note that the consecutive plots are much faster as they do not require compilation.

Conclusions

Given the timings I have gotten my judgment is as follows:

  • CairoMakie.jl is still visibly slower than Plots.jl.
  • Yet, CairoMakie.jl is in my opinion currently fast enough not to annoy users by requiring them to wait excessively long for a plot.

I think Makie maintainers, in combination with core Julia developers,
have done a fantastic job with improving time-to-first-plot in this ecosystem.

I can say that I decided to switch to Makie as my default plotting tool for larger projects.
However, I will probably for now still use Plots.jl in scenarios when I just want to start Julia and do a single quick plot
(especially on a machine where it has to be yet installed).