using Pkg
17 Overview of Julia commands
The Julia
programming language is well suited as a computer accompaniment while learning the concepts of calculus. The following overview covers the language-specific aspects of the pre-calculus part of the Calculus with Julia notes.
17.1 Installing Julia
Julia
is an open source project which allows anyone with a supported computer to use it free of charge.
To install locally, the downloads page has directions to use the Juliaup
utility for managing an installation. There are also links to several different binaries for manual installation. Additionally, the downloads page contains a link to a docker image. Julia
can also be compiled from source.
Julia
can also be run through the web.
The https://mybinder.org/ service in particular allows free access, though limited in terms of allotted memory and with a relatively short timeout for inactivity.
Google colab offers a free service with more computing power than binder
, though setup is a bit more fussy. To use colab
along with these notes, you need to execute a command that downloads Julia
and installs the CalculusWithJulia
package and a plotting package. (Modify the pkg"add ..."
command to add other desired packages; update the julia version as necessary):
# Installation cell
%%capture
%%shell
if ! command -v julia 3>&1 > /dev/null
then
wget -q 'https://julialang-s3.julialang.org/bin/linux/x64/1.10/julia-1.10.2-linux-x86_64.tar.gz' \
-O /tmp/julia.tar.gz
tar -x -f /tmp/julia.tar.gz -C /usr/local --strip-components 1
rm /tmp/julia.tar.gz
fi
julia -e 'using Pkg; pkg"add IJulia CalculusWithJulia; precompile;"'
julia -e 'using Pkg; Pkg.add(url="https://github.com/mth229/BinderPlots.jl")'
echo 'Now change the runtime type'
(The `BinderPlots` is a light-weight, barebones, plotting package that uses `PlotlyLight` to render graphics with commands mostly following those of the `Plots` package. Though suitable for most examples herein, the `Plots` package could instead be installed)
After this executes (which can take quite some time, as in a few minutes) under the Runtime
menu select Change runtime type
and then select Julia
.
After that, in a cell execute these commands to load the two installed packages:
using CalculusWithJulia
using BinderPlots
As mentioned, other packages can be chosen for installation.
17.2 Interacting with Julia
At a basic level, Julia
provides an interactive means to read commands or instructions, evaluate those commands, and then print or return those commands. At a user level, there are many different ways to interact with the reading and printing. For example:
- The REPL. The
Julia
terminal is the built-in means to interact withJulia
. AJulia
Terminal has a command prompt, after which commands are typed and then sent to be evaluated by theenter
key. The terminal may look something like the following where2+2
is evaluated:
$ julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.11.1 (2024-10-16)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia> 2 + 2
4
- An IDE. For programmers, an integrated development environment is often used to manage bigger projects.
Julia
hasJuno
andVSCode
. - A notebook. The Project Juptyer provides a notebook interface for interacting with
Julia
and a moreIDE
stylejupyterlab
interface. A jupyter notebook has cells where commands are typed and immediately following is the printed output returned byJulia
. The output of a cell depends on the state of the kernel when the cell is computed, not the order of the cells in the notebook. Cells have a number attached, showing the execution order. TheJuypter
notebook is used bybinder
and can be used locally through theIJulia
package. This notebook has the ability to display many different types of outputs in addition to plain text, such as images, marked up math text, etc. - The Pluto package provides a reactive notebook interface. Reactive means when one “cell” is modified and executed, the new values cascade to all other dependent cells which in turn are updated. This is very useful for exploring a parameter space, say. Pluto notebooks can be exported as HTML files which make them easy to read online and – by clever design – embed the
.jl
file that can run throughPluto
if it is downloaded.
The Pluto
interface has some idiosyncrasies that need explanation:
- Cells can only have one command within them. Multiple-command cells must be contained in a
begin
block or alet
block. - By default, the cells are reactive. This means when a variable in one cell is changed, then any references to that variable are also updated – like a spreadsheet. This is fantastic for updating several computations at once. However it means variable names can not be repeated within a page. Pedagogically, it is convenient to use variable names and function names (e.g.,
x
andf
) repeatedly, but this is only possible if they are within alet
block or a function body. - To not repeat names, but to be able to reference a value from cell-to-cell, some Unicode variants may be used within a page. Visually these look familiar, but typing the names requires some understanding of Unicode input. The primary usages is bold italic (e.g.,
\bix[tab]
or\bif[tab]
) or bold face (e.g.\bfx[tab]
or\bff[tab]
). - The notebooks snapshot the packages they depend on, which is great for reproducibility, but may lead to older versions of the packages being silently used.
17.3 Augmenting base Julia
The base Julia
installation has many features, but leaves many others to Julia
’s package ecosystem. These notes use packages to provide plotting, symbolic math, access to special functions, numeric routines, and more.
Within Pluto
, using add-on packages is very simple, as Pluto
downloads and installs packages when they are requested through a using
or import
directive.
For other interfaces to Julia
some more detail is needed.
The Julia
package manager makes add-on packages very easy to install.
Julia comes with just a few built-in packages, one being Pkg
which manages subsequent package installation. To add more packages, we first must load the Pkg
package. This is done by issuing the following command:
The using
command loads the specified package and makes all its exported values available for direct use. There is also the import
command which allows the user to select which values should be imported from the package, if any, and otherwise gives access to the new functionality through the dot syntax.
Packages need to be loaded just once per session.
To use Pkg
to “add” another package, we would have a command like:
Pkg.add("CalculusWithJulia")
This command instructs Julia
to look at its general registry for the CalculusWithJulia.jl
package, download it, then install it. Once installed, a package only needs to be brought into play with the using
or import
commands.
In a terminal setting, there is a package mode, entered by typing ]
as the leading character and exited by entering <backspace>
at a blank line. This mode allows direct access to Pkg
with a simpler syntax. The command above would be just add CalculusWithJulia
. As well, when a package is not installed, calling using SomePackage
will prompt the user if they wish to install the package in the current environment.)
Packages can be updated through the command Pkg.update()
, and removed with Pkg.rm(pkgname)
.
By default packages are installed in a common area. It may be desirable to keep packages for projects isolated. For this the Pkg.activate
command can be used. This feature allows a means to have reproducible environments even if Julia
or the packages used are upgraded, possibly introducing incompatibilities.
For these notes, the following packages, among others, are used:
Pkg.add("CalculusWithJulia") # for some convenience functions and a few packages (SpecialFunctions, ForwardDiff)
Pkg.add("Plots") # for basic plotting
Pkg.add("SymPy") # for symbolic math
Pkg.add("Roots") # for numerically solving `f(x)=0` and `f(x)=g(x)`
Pkg.add("QuadGk") # for 1-dimensional numeric integration
Pkg.add("HQuadrature") # for higher-dimensional integration
17.4 Julia
commands
In a Jupyter
notebook or Pluto
notebook, commands are typed into a notebook cell:
2 + 2 # use shift-enter to evaluate
4
Commands are executed by using shift-enter
or a run button near the cell.
In Jupyter
multiple commands per cell are allowed. In Pluto
, a begin
or let
block is used to collect multiple commands into a single cell. Commands may be separated by new lines or semicolons.
On a given line, anything after a #
is a comment and is not processed.
The results of the last command executed will be displayed in an output area. Separating values by commas allows more than one value to be displayed. Plots are displayed when the plot object is returned by the last executed command.
In Jupyter
, the state of the notebook is determined by the cells executed along with their order. The state of a Pluto
notebook is a result of all the cells in the notebook being executed. The cell order does not impact this and can be rearranged by the user.
17.5 Numbers, variable types
Julia
has many different number types beyond the floating point type employed by most calculators. These include
- Floating point numbers:
0.5
- Integers:
2
- Rational numbers:
1//2
- Complex numbers
2 + 0im
Julia
’s parser finds the appropriate type for the value, when read in. The following all create the number \(1\) first as an integer, then a rational, then a floating point number, again as floating point number, and finally as a complex number:
1, 1//1, 1.0, 1e0, 1 + 0im
(1, 1//1, 1.0, 1.0, 1 + 0im)
As much as possible, operations involving certain types of numbers will produce output of a given type. For example, both of these divisions produce a floating point answer, even though mathematically, they need not:
2/1, 1/2
(2.0, 0.5)
Some powers with negative bases, like (-3.0)^(1/3)
, are not defined. However, Julia
provides the special-case function cbrt
(and sqrt
) for handling these.
Integer operations may silently overflow, producing odd answers, at first glance:
2^64
0
(Though the output is predictable, knowing why requires understanding of how the hardware implements these operations.)
When different types of numbers are mixed, Julia
will usually promote the values to a common type before the operation:
2 + 1//2) + 0.5 (
3.0
Julia
will first add 2
and 1//2
promoting 2
to rational before doing so. Then add the result, 5//2
to 0.5
by promoting 5//2
to the floating point number 2.5
before proceeding.
Julia
uses a special type to store a handful of irrational constants such as pi
. The special type allows these constants to be treated without round off, until they mix with other floating point numbers. An irrational value for e
is not exported; the CalculusWithJulia
exports a floating point value e=exp(1)
.
There are some functions that require these be explicitly promoted to floating point. This can be done by calling float
.
The standard mathematical operations are implemented by +
, -
, *
, /
, ^
. Parentheses are used for grouping.
17.5.1 Vectors
A vector is an indexed collection of similarly typed values. Vectors can be constructed with square brackets (syntax for concatenation):
1, 1, 2, 3, 5, 8] [
6-element Vector{Int64}:
1
1
2
3
5
8
Values will be promoted to a common type (or type Any
if none exists). For example, this vector will have type Float64
due to the 1/3
computation:
1, 1//2, 1/3] [
3-element Vector{Float64}:
1.0
0.5
0.3333333333333333
(Vectors are used as a return type from some functions, as such, some familiarity is needed.)
Other common container types are variables of vectors (higher-dimensional arrarys, offset arrays, etc.) tuples (for heterogeneous, immutable, indexed values); named tuples (which add a name to each value in a tuple); and dictionaries (for associative relationships between a key and a value).
Regular arithmetic sequences can be defined by either:
- Range operations:
a:h:b
ora:b
which produces a generator of values starting ata
separated byh
(h
is1
in the last form) until they reachb
. - The
range
function:range(a, b, length=n)
which produces a generator ofn
values betweena
andb
;
These constructs return range objects. A range object compactly stores the values it references. To see all the values, they can be collected with the collect
function, though this is rarely needed in practice.
Random sequences are formed by rand
, among others:
rand(3)
3-element Vector{Float64}:
0.5983076145551056
0.3542812336692216
0.31318936185122903
The call rand()
returns a single random number (in \([0,1)\).)
17.6 Variables
Values can be assigned variable names, with =
. There are some variants
= 2
u = 3
a_really_long_name = 1, 2 # multiple assignment
a0, b0 = a2 = 0 # chained assignment, sets a2 and a1 to 0 a1
0
The names can be short, as above, or more verbose. Variable names can’t start with a number, but can include numbers. Variables can also include Unicode or even be an emoji.
= π/3, π/4 α, β
(1.0471975511965976, 0.7853981633974483)
We can then use the variables to reference the values:
+ a_really_long_name + a0 - b0 + α u
5.047197551196597
Within Pluto
, names are idiosyncratic: within the global scope, only a single usage is possible per notebook; functions and variables can be freely renamed; structures can be redefined or renamed; …
Outside of Pluto
, names may be repurposed, even with values of different types (Julia
is a dynamic language), save for (generic) function names, which have some special rules and can only be redefined as another method for the function. Generic functions are central to Julia
’s design. Generic functions use a method table to dispatch on, so once a name is assigned to a generic function, it can not be used as a variable name; the reverse is also true.
17.7 Functions
Functions in Julia
are first-class objects. In these notes, we often pass them as arguments to other functions. There are many built-in functions and it is easy to define new functions.
We “call” a function by passing argument(s) to it, grouped by parentheses:
sqrt(10)
sin(pi/3)
log(5, 100) # log base 5 of 100
2.8613531161467867
Without parentheses, the name (usually) refers to a generic name and the output lists the number of available implementations (methods).
log
log (generic function with 42 methods)
17.7.1 Built-in functions
Julia
has numerous built-in mathematical functions, we review a few here:
Powers logs and roots
Besides ^
, there are sqrt
and cbrt
for powers. In addition basic functions for exponential and logarithmic functions:
sqrt, cbrt
exp
log # base e
log10, log2, # also log(b, x)
Trigonometric functions
The 6
standard trig functions are implemented; their implementation for degree arguments; their inverse functions; and the hyperbolic analogs.
sin, cos, tan, csc, sec, cot
asin, acos, atan, acsc, asec, acot
sinh, cosh, tanh, csch, sech, coth
asinh, acosh, atanh, acsch, asech, acoth
If degrees are preferred, the following are defined to work with arguments in degrees:
sind, cosd, tand, cscd, secd, cotd
Useful functions
Other useful and familiar functions are defined:
abs
: absolute valuesign
: is \(\lvert x \rvert/x\) except at \(x=0\), where it is \(0\).floor
,ceil
: greatest integer less or least integer greatermax(a,b)
,min(a,b)
: larger (or smaller) ofa
orb
maximum(xs)
,minimum(xs)
: largest or smallest of the collection referred to byxs
In a Pluto session, the “Live docs” area shows inline documentation for the current object.
For other uses of Julia
, the built-in documentation for an object is accessible through a leading ?
, say, ?sign
. There is also the @doc
macro, for example:
@doc sign
17.7.2 User-defined functions
Simple mathematical functions can be defined using standard mathematical notation:
f(x) = -16x^2 + 100x + 2
f (generic function with 1 method)
The argument x
is passed into the body of function.
Other values are found from the environment where defined:
= 1
a f(x) = 2*a + x
f(3) # 2 * 1 + 3
= 4
a f(3) # now 2 * 4 + 3
11
User-defined functions can have \(0\), \(1\) or more positional arguments:
area(w, h) = w*h
area (generic function with 1 method)
Julia makes different methods for generic function names, so function definitions whose argument specification is different are for different uses, even if the name is the same. This is polymorphism. The practical use is that it means users need only remember a much smaller set of function names, as attempts are made to give common expectations to the same name. (That is, +
should be used only for “add” ing objects, however defined.)
Functions can also be defined with keyword arguments that may have defaults specified:
f(x; m=1, b=0) = m*x + b # note ";"
f(1) # uses m=1, b=0 -> 1 * 1 + 0
f(1, m=10) # uses m=10, b=0 -> 10 * 1 + 0
f(1, m=10, b=5) # uses m=10, b=5 -> 10 * 1 + 5
15
Keyword arguments are not considered for dispatch.
Longer functions can be defined using the function
keyword, the last command executed is returned:
function f(x)
= x^2
y = y - 3
z
zend
f (generic function with 1 method)
Functions without names, anonymous functions, are made with the ->
syntax as in:
-> cos(x)^2 - cos(2x) x
#20 (generic function with 1 method)
These are useful when passing a function to another function or when writing a function that returns a function.
17.8 Conditional statements
Julia
provides the traditional if-else-end
statements, but more conveniently has a ternary
operator for the simplest case:
our_abs(x) = (x < 0) ? -x : x
our_abs (generic function with 1 method)
17.9 Looping
Iterating over a collection can be done with the traditional for
loop. However, there are list comprehensions to mimic the definition of a set:
^2 for x in 1:10] [x
10-element Vector{Int64}:
1
4
9
16
25
36
49
64
81
100
Comprehensions can be filtered through the if
keyword
^2 for x in 1:10 if iseven(x)] [x
5-element Vector{Int64}:
4
16
36
64
100
This is more efficient than creating the collection then filtering, as is done with:
filter(iseven, [x^2 for x in 1:10])
5-element Vector{Int64}:
4
16
36
64
100
17.10 Broadcasting, mapping
A function can be applied to each element of a vector through mapping or broadcasting. The latter is implemented in a succinct notation. Calling a function with a “.” before its opening “(” will apply the function to each individual value in the argument:
= [1,2,3,4,5]
xs sin.(xs) # gives back [sin(1), sin(2), sin(3), sin(4), sin(5)]
5-element Vector{Float64}:
0.8414709848078965
0.9092974268256817
0.1411200080598672
-0.7568024953079282
-0.9589242746631385
For “infix” operators, the dot precedes the operator, as in this example instructing pointwise multiplication of each element in xs
:
.* xs xs
5-element Vector{Int64}:
1
4
9
16
25
Alternatively, the more traditional map
can be used:
map(sin, xs)
5-element Vector{Float64}:
0.8414709848078965
0.9092974268256817
0.1411200080598672
-0.7568024953079282
-0.9589242746631385
17.11 Plotting
Plotting is not built-in to Julia
, rather added through add-on packages. Julia
’s Plots
package is an interface to several plotting packages. We mention plotly
(built-in) for web based graphics, pyplot
, and gr
(also built into Plots
) for other graphics.
We must load Plots
before we can plot (and it must be installed before we can load it):
using Plots
plotly() # optionally change the backend from the default
Plots.PlotlyBackend()
With Plots
loaded, we can plot a function by passing the function object by name to plot
, specifying the range of x
values to show, as follows:
plot(sin, 0, 2pi) # plot a function - by name - over an interval [a,b]
This is in the form of the basic pattern employed: verb(function_object, arguments...)
. The verb in this example is plot
, the object sin
, the arguments 0, 2pi
to specify [a,b]
domain to plot over.
Plotting more than one function over [a,b]
is achieved through the plot!
function, which modifies the existing plot (plot
creates a new one) by adding a new layer:
plot(sin, 0, 2pi)
plot!(cos, 0, 2pi)
plot!(zero, 0, 2pi) # add the line y=0
(There are alternatives to plot functions or other traces all at once.)
Individual points are added with scatter
or scatter!
:
plot(sin, 0, 2pi, legend=false)
plot!(cos, 0, 2pi)
scatter!([pi/4, pi+pi/4], [sin(pi/4), sin(pi + pi/4)])
(The extra argument legend=false
suppresses the automatic legend drawing. There are many other useful keyword arguments to adjust attributes of a trace of a graphic. For example, passing markersize=10
to the scatter!
command would draw the points larger than the default.)
Plotting an anonymous function is a bit more immediate than the two-step approach of defining a named function then calling plot
with this as an argument:
plot( x -> exp(-x/pi) * sin(x), 0, 2pi)
The scatter!
function used above takes two vectors of values to describe the points to plot, one for the \(x\) values and one for the matching \(y\) values. The plot
function can also produce plots with this interface. For example, here we use a comprehension to produce y
values from the specified x
values:
= range(0, 2pi, length=251)
xs = [sin(2x) + sin(3x) + sin(4x) for x in xs]
ys plot(xs, ys)
There are different plotting interfaces. Though not shown, all of these plot
commands produce a plot of f
, though with minor differences:
= range(a, b, length=251)
xs = f.(xs)
ys plot(f, a, b) # recipe for a function
plot(xs, f) # alternate recipe
plot(xs, ys) # plot coordinates as two vectors
plot([(x,f(x)) for x in xs]) # plot a vector o points
The choice should depend on convenience.
17.12 Equations
Notation for Julia
and math is similar for functions - but not for equations. In math, an equation might look like:
\[ x^2 + y^2 = 3 \]
In Julia
the equals sign is only for assignment and mutation. The left-hand side of an equals sign in Julia
is reserved for a) variable assignment; b) function definition (via f(x) = ...
); c) indexed mutation of a vector or array; d) mutation of fields in a structure. (Vectors are indexed by a number allowing retrieval and mutation of the stored value in the container. The notation mentioned here would be xs[2] = 3
to mutate the 2nd element of xs
to the value 3
.
17.13 Symbolic math
Symbolic math is available through an add-on package SymPy
(among others). Once loaded, symbolic variables in SymPy
are created with the macro @syms
:
using SymPy
@syms x a b c
(x, a, b, c)
(A macro rewrites values into other commands before they are interpreted. Macros are prefixed with the @
sign. In this use, the “macro” @syms
translates x a b c
into a command involving SymPy
s symbols
function.)
Symbolic expressions - unlike numeric expressions - are not immediately evaluated, though they may be simplified:
= a*x^2 + b*x + c p
\(a x^{2} + b x + c\)
To substitute a value, we can use Julia
’s pair
notation (variable=>value
):
p(x=>2), p(x=>2, a=>3, b=>4, c=>1)
(4*a + 2*b + c, 21)
This is convenient notation for calling the subs
function for SymPy
.
SymPy expressions of a single free variable can be plotted directly:
plot(64 - (1/2)*32 * x^2, 0, 2)
- SymPy has functions for manipulating expressions:
simplify
,expand
,together
,factor
,cancel
,apart
, \(...\) - SymPy has functions for basic math:
factor
,roots
,solve
,solveset
, \(\dots\) - SymPy has functions for calculus:
limit
,diff
,integrate
, \(\dots\)