Overview of Julia commands

Julia The Julia programming language is well suited as an 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.

Installing Julia

Julia is an open source project which allows anyone with a supported computer to use it. To install locally, the downloads page has several different binaries for installation. Additionally, the downloads page contains a link to a docker image. For Microsoft Windows, the new juilaup installer may be of interest; it is available from the Windows Store. 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.

Launch Binder

Interacting with Julia

The Calculus With Julia notes are formatted as though the commands were run in the notebook interface of Jupyter (below), though there are many other interfaces for interacting with Julia.

At a basic level, Julia provides a 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:


$ julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.6.0 (2021-03-24)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> 2 + 2
4

Each interface has some means to send commands to Julia's interpreter. For Jupyter the commands in a cell are sent to be executed by the keyboard command shift-enter or through the "play" icon (➡) in the toolbar.

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.

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 package, we first must load the Pkg package. This is done by issuing the following command:

using Pkg

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.

Packages can be updated through the command Pkg.up(), 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 incompatabilities.

For these notes, the following packages, among others are used:

Pkg.add("CalculusWithJulia") # for some simplifying functions and a few packages (SpecialFunctions, ForwardDiff)
Pkg.add("Plots")  # for basic plotting
Pkg.add("SymPy")  # for symbolic math
Pkg.add("Roots")  # for solving  `f(x)=0`
Pkg.add("QuadGk") # for integration
Pkg.add("HQuadrature") # for higher-dimensional integration

Julia commands

In Jupyter, commands are typed into a notebook cell:

2 + 2   # use shift-enter to evaluate
4

Commands are executed by using shift-enter or the play button in Jupyter.

Commands may be separated by new lines or semicolons, allowing multiple commands per Jupyter cell.

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. Explicit printing can be done to output intermediate values. Plots are displayed when the plot object is returned by the last executed command.

The state of the notebook follows the execution order, not the order with the notebook. (That is there is no reason to assume the state comes by just looking at the cells above the current one. This is not the case with Pluto notebooks due to their reactive nature.)

Numbers, variable types

Julia has many different number types beyond the floating point type employed by most calculators. These include

Julia's parser finds the appropriate type for the value, when read in. These 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, if overflow is taken into consideration appropriately.)

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 converting 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, we use pi. The special type allows these constants to be treated without round off, until they mix with other floating point numbers. 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.

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

(Vectors are used as a return type from some functions, as such, some familiarity is needed.)

Regular arithmetic sequences can be defined by either:

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.5207430790662317
 0.601707078457371
 0.5718261081035458

Random sequences are formed by rand, among others:

rand(3)
3-element Vector{Float64}:
 0.9433876444534524
 0.19588987108786848
 0.07754529218007877

Variables

Values can be assigned variable names, with =. There are some variants

x = 2
a_really_long_name = 3
a, b = 1, 2    # multiple assignment
a1 = a2 = 0    # chained assignment, sets a2 and a1 to 0
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:

x + a_really_long_name + a - b + α
5.047197551196597

Names may be repurposed, even with values of different types (Julia is a dynamic language), save for function names, which have some special rules and can only be redefined as another 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.

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

With out parentheses, the name (usually) refers to a generic name and the output lists the number of available implementations.

log
log (generic function with 42 methods)

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:


The built-in documentation for an object is accessible through, say, ?sign. There is also the @doc macro:

@doc sign
sign(x)

Return zero if x==0 and $x/|x|$ otherwise (i.e., ±1 for real x).

sign(x::AbstractQuantity)

Returns the sign of x.


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:

a = 1
f(x) = 2*a + x
f(3)   # 2 * 1 + 3
a = 4
f(3)  # now 2 * 4 + 3
11

User defined functions can have 0, 1 or more 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 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

Longer functions can be defined using the function keyword, the last command executed is returned:

function f(x)
  y = x^2
  z = y - 3
  z
end
f (generic function with 1 method)

Functions without names, anonymous functions, are made with the -> syntax as in:

x -> cos(x)^2 - cos(2x)
#2 (generic function with 1 method)

These are useful when passing a function to another function or when writing a function that returns a function.

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)

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:

[x^2 for x in 1:10]
10-element Vector{Int64}:
   1
   4
   9
  16
  25
  36
  49
  64
  81
 100

Comprehensions can be filtered through the if keyword

[x^2 for x in 1:10 if iseven(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

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:

xs = [1,2,3,4,5]
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

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

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, 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

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]

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

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 arguments to adjust 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:

xs = range(0, 2pi, length=251)
ys = [sin(2x) + sin(3x) + sin(4x) for x in xs]
plot(xs, ys)

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. The left-hand side of an equals sign in Julia is reserved for a) variable assignment; b) function definition (via f(x) = ...); and c) indexed assignment to a vector or array. (Vectors are indexed by a number allow retrieval and setting of the stored value in the container. The notation mentioned here would be xs[2] = 3 to assign to the 2nd element of xs a value 3.

Symbolic math

Symbolic math is available through an add-on package SymPy. Once loaded, symbolic variables 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 SymPys symbols function.)

Symbolic expressions - unlike numeric expressions - are not immediately evaluated, though they may be simplified:

p = a*x^2 + b*x + c
$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$