2 + 2
4
A notebook for this material: ipynb
The programming language Julia
(www.julialang.org) is a new language that builds on a long history of so-called dynamic scripting languages (DSLs). DSLs are widely used for exploratory work and are part of the toolbox of most all data scientists, a rapidly growing area of employment. The basic Julia
language is reminiscent of is MATLAB
, though it offers many improvements over that language. For these notes, the focus will be on its more transparent syntax, but in general a major selling point of Julia
is that it is much faster that MATLAB
at many tasks. (Well it should be, MATLAB
was started back in the 70s.) Even better, Julia
is an open-source project which means it is free to download and install, unlike the commercial package MATLAB
.
These notes will use Julia
to explore calculus concepts. Unlike some other programs used with calculus (e.g., Mathematica, Maple, and Sage) Julia
is not a symbolic math language. Rather, we will use a numeric approach. This gives a different viewpoint on the calculus material supplementing that of the text book. Though there are a few idiosyncrasies we will see along the way, for the most part the Julia
language will be both powerful and easy to learn.
In this project we start with baby steps – how to use Julia
to perform operations we can do on the calculator. The use of a calculator requires knowledge about several things we take for granted here:
Parallels of each of these will be discussed in the following.
Julia
can replicate the basics of a calculator with the standard notations. The familiar binary operators are +
, -
, *
, /
, and ^
. With a calculator you “punch” in numbers and operators and to get an answer push the =
key. Using Julia
is not much different: you type in an expression then send to Julia
to compute. The “equals key” on a calculator is replaced by the enter key on the keyboard (or shift-enter if using IJulia
). Beyond that there isn’t much difference.
For example, to add two and two we simply execute:
2 + 2
4
Or to convert \(70\) degrees to Celsius with the standard formula \(C=5/9(F-32)\):
5/9)*(70 - 32) (
21.11111111111111
Of to find a value of \(32 - 16x^2\) when \(x=1.5\):
32 - 16*(1.5)^2
-4.0
To find the value of \(\sqrt{15}\) we can use power notation:
15^(1/2)
3.872983346207417
Compute \(22/7\) in Julia
.
Compute \(\sqrt{220}\) in Julia
.
Compute \(2^8\) in Julia
.
One drawback about using a calculator is the expression gets evaluated as it gets entered. For simple computations this is a convenience, but for more complicated ones it requires some thinking about the order of how an expression will be computed. In Julia
you still need to be mindful of the order that operations are carried out, but as the entire expression is typed – and can be edited – before evaluation, you can more closely control what you want.
Order of operations refers to conventions used to decide which operation will happen first, when there is a choice of more than one. A simple example, is what is the value of \(3 \cdot 2 + 1\)?
There are two binary operations in this expressions: a multiplication and an addition. Which is done first and which second?
In some instances the order used doesn’t matter. A case of this would be \(3 + 2 + 1\). This is because addition is associative. (Well, not really on the computer, but that is another lesson.) In the case of \(3 \cdot 2 + 1\) the order certainly matters.
For \(3 \cdot 2 + 1\) if we did the addition first, the expression would be \(9 = 3\cdot 3\). If we did the multiplication first, the value would be \(7=6+1\). In this case, we all know that the correct answer is \(7\), as we perform multiplication before addition, or in more precise terms the precedence of multiplication is higher than that of addition.
The standard order of the basic mathematical operations is remembered by many students through the mnemonic PEMDAS, which can be misleading, so we spell it out here:
P
) First parenthesesE
) then exponents (or powers)MD
) then multiplication or divisionAS
) then addition or subtraction.This has the precedence of multiplication (part of MD
) higher than that of subtraction (part of AS
), as just mentioned.
Applying this, if we have the mathematical expression
\[ \frac{12 - 10}{2} \]
We know that the subtraction needs to be done before the division, as this is how we interpret this form of division. How to make this happen? The precedence of parentheses is used to force the subtraction before the division, as in (12-10)/2
. Without parentheses you get a different answer:
(1.0, 7.0)
Using a comma to separate expressions will cause both to print out – not just the last one evaluated. This trick is also a useful trick within an IJulia
notebook.
Parentheses are used to force lower precedence operations to happen before higher precedence ones.
There is a little more to the story, as we need to understand what happens when we have more then one operation with the same level. For instance, what is \(2 - 3- 4\)? Is it \((2 - 3) - 4\) or \(2 - (3 - 4)\).
Unlike addition, subtraction is not associative so this really matters. The subtraction operator is left associative meaning the evaluation of \(2 - 3 - 4\) is done by \((2-3) - 4\). The operations are performed in a left-to-right manner. Most – but not all operations – are left associative, some are right associative and performed in a right-to-left manner.
right to left It is the order of which operation is done first, not reading from right to left, as one might read Arabic.
To see that Julia
has left associative subtraction, we can just check.
2 - 3 - 4, (2 - 3) - 4, 2 - (3 - 4)
(-5, -5, 3)
Not all operations are processed left-to-right. The power operation, ^
, is right associative, as this matches the mathematical usage. For example:
4^3^2, (4^3)^2, 4^(3^2)
(262144, 4096, 262144)
What about the case where we have different operations with the same precedence? What happens then? A simple example would be \(2 / 3 * 4\)? Is this done in a left to right manner as in:
2 / 3) * 4 (
2.6666666666666665
Or a right-to-left manner, as in:
2 / (3 * 4)
0.16666666666666666
And the answer is left-to-right:
2 / 3 * 4
2.6666666666666665
Which of the following is a valid Julia
expression for
\[ \frac{3 - 2}{4 - 1} \]
that uses the least number of parentheses?
Which of the following is a valid Julia
expression for
\[ \frac{3\cdot2}{4} \]
that uses the least number of parentheses?
Which of the following is a valid Julia
expression for
\[ 2^{4 - 2} \]
that uses the least number of parentheses?
One of these three expressions will produce a different answer, select that one:
One of these three expressions will produce a different answer, select that one:
One of these three expressions will produce a different answer, select that one:
Compute the value of
\[ \frac{9 - 5 \cdot (3-4)}{6 - 2}. \]
Compute the following using Julia
:
\[ \frac{(.25 - .2)^2}{(1/4)^2 + (1/3)^2} \]
Compute the decimal representation of the following using Julia
:
\[ 1 + \frac{1}{2} + \frac{1}{2^2} + \frac{1}{2^3} + \frac{1}{2^4} \]
Compute the following using Julia
:
\[ \frac{3 - 2^2}{4 - 2\cdot3} \]
Compute the following using Julia
:
\[ (1/2) \cdot 32 \cdot 3^2 + 100 \cdot 3 - 20 \]
Most all calculators used are not limited to these basic arithmetic operations. So-called scientific calculators provide buttons for many of the common mathematical functions, such as exponential, logs, and trigonometric functions. Julia
provides these too, of course.
There are special functions to perform common powers. For example, the square-root function is used as:
sqrt(15)
3.872983346207417
This shows how to evaluate a function – using its name and parentheses, as in function_name(arguments)
. Parentheses are also used to group expressions, as would be done to do this using the power notation:
15^(1/2)
3.872983346207417
Additionally, parentheses are also used to make “tuples”, a concept we don’t pursue here but that is important for programming with Julia
. The point here is the context of how parentheses are used is important, though for the most part the usage is the same as their dual use in your calculus text.
Like sqrt
, there is also a cube-root function:
cbrt(27)
3.0
The cbrt
and sqrt
functions are not exactly the same as using ^
, as they differ when the inputs are not in their domain: For cube roots, we can see that there is a difference with negative bases:
cbrt(-8) ## correct
-2.0
-8)^(1/3) ## need first parentheses, why? (
LoadError: DomainError with -8.0:
Exponentiation yielding a complex result requires a complex argument.
Replace x^y with (x+0im)^y, Complex(x)^y, or similar.
(The latter is an error as the power function has an output type that depends on the power being real, not a specific value of a real. For 1/2 the above would clearly be an error, so then for 1/3 Julia
makes this an error.)
The basic trigonometric functions in Julia
work with radians:
sin(pi/4)
cos(pi/3)
0.5000000000000001
But students think in degrees. What to do? Well, you can always convert via the ratio \(\pi/180\):
sin(45 * pi/180)
cos(60 * pi/180)
0.5000000000000001
However, Julia
provides the student-friendly functions sind
, cosd
, and tand
to work directly with degrees:
sind(45)
cosd(45)
0.7071067811865476
Be careful, an expression like \(\cos^2(\pi/4)\) is a shorthand for squaring the output of the cosine of \(\pi/4\), hence is expressed with
cos(pi/4)^2 # not cos^2(pi/4) !!!
0.5000000000000001
The math notation \(\sin^{-1}(x)\) is also a source of confusion. This is not a power, rather it indicates an inverse function, in this case the arcsine. The arcsine function is written asin
in Julia
.
For certain values, the arcsine and sine function are inverses:
asin(sin(0.1))
0.1
However, this isn’t true for all values of \(x\), as \(\sin(x)\) is not monotonic everywhere. In particular, the above won’t work for \(x\) values outside \((-\pi/2, \pi/2)\):
asin(sin(100))
-0.5309649148733837
Other inverse trigonometric functions are acos
, atan
and for completeness asec
, acsc
, and acot
are available for use.
The values \(e^x\) can be computed through the function exp(x)
:
exp(2)
7.38905609893065
The constant value of e
is only available on demand:
using Base.MathConstants
e
ℯ = 2.7182818284590...
The logarithm function, log
(and not ln
) does log base \(e\):
log(exp(2))
2.0
To do base 10, there is a log10
function:
log10(exp(2))
0.8685889638065036
There is also a log2
function for base 2. However, there are many more possible choices for a base. Rather than create functions for each possible one of interest the log
function has an alternative form taking two argument. The first is interpreted as the base, the second the \(x\) value. So the above, is also done through:
log(10, exp(2))
0.8685889638065035
There are some other useful functions For example, abs
for the absolute value, round
for rounding, floor
for rounding down and ceil
for rounding up. Here are some examples
round(3.14)
3.0
floor(3.14)
3.0
ceil(3.14)
4.0
The observant eye will notice the answers above are not integers. (We discuss how to tell later.) What to do if you want an integer? These functions take a “type” argument, as in round(Int, 3.14)
.
What is the value of \(\sin(\pi/10)\)?
What is the value of \(\sin(52^\circ)\)?
Is \(\sin^{-1}(\sin(3\pi/2))\) equal to \(3\pi/2\)?
What is the value of round(3.5000)
What is the value of sqrt(32 - 12)
Which is greater \(e^\pi\) or \(\pi^e\)?
What is the value of \(\pi - (x - \sin(x)/\cos(x))\) when \(x=3\)?
With a calculator, one can store values into a memory for later usage. This useful feature with calculators is greatly enhanced with computer languages, where one can bind, or assign, a variable to a value. For example the command x=2
will bind x
to the value \(2\):
= 2 x
2
So, when we evaluate
^2 x
4
The value assigned to x
is looked up and used to return \(4\).
The word “dynamic” to describe the Julia
language refers to the fact that variables can be reassigned and retyped. For example:
= sqrt(2) ## a Float64 now x
1.4142135623730951
In Julia
one can have single letter names, or much longer ones, such as
= 3 some_ridiculously_long_name
3
^2 some_ridiculously_long_name
9
The basic tradeoff being: longer names are usually more expressive and easier to remember, whereas short names are simpler to type. To get a list of the currently bound names, the whos
function may be called. Not all names are syntactically valid, for example names can’t begin with a number or include spaces.
In fact, only most objects bound to a name can be arbitrarily redefined. When we discuss functions, we will see that redefining functions can be an issue and new names will need to be used. As such, it often works to stick to come convention for naming: numbers use values like i
, j
, x
, y
; functions like f
, g
, h
, etc.
To work with computer languages, it is important to appreciate that the equals sign in the variable assignment is unlike that of mathematics, where often it is used to indicate an equation which may be solved for a value. With the following computer command the right hand expression is evaluated and that value is assigned to the variable. So,
= 2 + 3 x
5
does not assign the expression 2 + 3
to x
, but rather the evaluation of that expression, which yields 5. (This also shows that the precedence of the assignment operator is lower than addition, as addition is performed first in the absence of parentheses.)
At the prompt, a simple expression is entered and, when the return key is pressed, evaluated. At times we may want to work with multiple subexpressions. A particular case might be setting different parameters:
=0
a=1 b
1
Multiple expressions can be more tersely written by separating each expression using a semicolon:
=0; b=1; a
Note that Julia
makes this task even easier, as one can do multiple assignments via “tuple destructuring:”
= 0, 1 ## printed output is a "tuple"
a, b + b a
1
Let \(a=10\), \(b=2.3\), and \(c=8\). Find the value of \((a-b)/(a-c)\).
What is the answer to this computation?
a = 3.2; b=2.3
a^b - b^a
For longer computations, it can be convenient to do them in parts, as this makes it easier to check for mistakes. (You likely do this with your calculator.)
For example, to compute
\[ \frac{p - q}{\sqrt{p(1-p)}} \]
for \(p=0.25\) and \(q=0.2\) we might do:
p, q = 0.25, 0.2
top = p - q
bottom = sqrt(p*(1-p))
answer = top/bottom
What is the result of the above?
In mathematics, there a many different types of numbers. Familiar ones are integers, rational numbers, and the real numbers. In addition, complex numbers are needed to fully discuss polynomial functions. This is not the case with calculators.
Most calculators treat all numbers as floating point numbers – an approximation to the real numbers. Not so with Julia
. Julia
has types for many different numbers: Integer
, Real
, Rational
, Complex
, and specializations depending on the number of bits that are used, e.g., Int64
and Float64
. For the most part there is no need to think about the details, as values are promoted to a common type when used together. However, there are times where one needs to be aware of the distinctions.
In the real number system of mathematics, there are the familiar real numbers and integers. The integers are viewed as a subset of the real numbers.
Julia provides types Integer
and Real
to represent these values. (Actually, the Integer
type represents more than one actual storage type, either Int32
or Int64
.) These are separate types. The type of an object is returned by typeof()
.
For example, the integer \(1\) is simply created by the value 1
:
1
1
The floating point value \(1\) is specified by using a decimal point:
1.0
1.0
The two values are printed differently – integers never have a decimal point, floating point values always have a decimal point. This emphasizes the fact that the two values 1
and 1.0
are not the same – they are stored differently, they print differently, and can give different answers. In most cases – but not all – uses they can be used interchangeably.
For example, we can add the two:
1 + 1.0
2.0
This gives back the floating point value 2.0
. First the integer and floating point value are promoted to a common type (floating point in this case) and then added.
Sometimes there can be an issue. The value 2^(-3)
we know should be \(1/2^3 = 1/8\) or 0.125
and Julia
agrees:
2^(-3)
0.125
However, this is special cased. If -3
is replaced with a variable name, there will be a failure:
= -3
x 2^x
LoadError: DomainError with -3:
Cannot raise an integer x to a negative power -3.
Make x or -3 a float by adding a zero decimal (e.g., 2.0^-3 or 2^-3.0 instead of 2^-3)or write 1/x^3, float(x)^-3, x^float(-3) or (x//1)^-3.
This gotcha has an explanation: in Julia
, most functions are “type-stable” meaning, the type of the input (integer/integer in this case) should determine the type of the output (in this case floating point). But for this operation to be fast, Julia
insists (in general) it be an integer, as what happens when the base is non-negative. It is not a “gotcha” when either the exponent or the base is a floating point number:
= -3
x 2.0^x
0.125
When a computer is used to represent numeric values there are limitations: a computer only assigns a finite number of bits for a value. This works great in most cases, but since there are infinitely many numbers, not all possible numbers can be represented on the computer.
The first limitation is numbers can not be arbitrarily large.
Take for instance a 64-bit integer. A bit is just a place in computer memory to hold a \(0\) or a \(1\). Basically one bit is used to record the sign of the number and the remaining 63 to represent the numbers. This leaves the following range for such integers \(-2^{63}\) to \(2^{63} - 1\).
Julia
is said to not provide training wheels. This means it doesn’t put in checks for integer overflow, as these can slow things down. To see what happens, let just peek:
2^62 ## about 4.6 * 10^18
2^63 ## negative!!!
-9223372036854775808
So if working with really large values, one must be mindful of the difference – or your bike might crash!
Look at the output of
2^3^4
0
Why is it 0? The value of \(3^4=81\) is bigger than 63, so \(2^{81}\) will overflow.
The following works though:
2.0 ^ 3 ^ 4
2.4178516392292583e24
This is because the value 2.0
will use floating point arithmetic which has a much wider range of values. (The Julia
documentation sends you to this interesting blog post johndcook, which indicates the largest floating point value is \(2^{1023}(2 - 2^{-52})\) which is roughly 1.8e308
.
Scientific notation is used to represent many numbers
A number in Julia
may be represented in scientific notation. The basic canonical form is \(a\cdot 10^b\), with \(1 \leq |a| < 10\) and \(b\) is an integer. This is written in Julia
as aeb
where e
is used to separate the value from the exponent. The value 1.8e308
means \(1.8 \cdot 10^{308}\). Scientific notation makes it very easy to focus on the gross size of a number, as the exponent is set off.
The second limitation is numbers are often only an approximation.
This means expressions which are mathematically true, need not be true once approximated on the computer. For example, \(\sqrt{2}\) is an irrational number, that is, its decimal representation does not repeat the way a rational number does. Hence it is impossible to store on the computer an exact representation, at some level there is a truncation or round off. This will show up when you try something like:
2 - sqrt(2) * sqrt(2)
-4.440892098500626e-16
That difference of basically \(10^{-16}\) is roughly the machine tolerance when representing a number. (One way to imagine this is mathematically, we have two ways to write the number \(1\):
\[ 0.\bar{9} = 0.9999... = 1 \]
but on the computer, you can’t have the “…” in a decimal expansion – it must truncate – so instead values round to something like \(0.9999999999999999\) or \(1\), with nothing in between.
A typical expression in computer languages is to use ==
to compare the values on the left- and right-hand sides. This is not assignment, rather a question. For example:
2 == 2
2 == 3
sqrt(2) * sqrt(2) == 2 ## surprising?
false
The last one would be surprising were you not paying attention to the last paragraph. Comparisons with ==
work well for integers and strings, but not with floating point numbers. (For these the isapprox
function can be used.)
Comparisons do a promotion prior to comparing, so even though these numbers are of different types, the ==
operation treats them as equal:
1 == 1.0
true
The ===
operator has an even more precise notion of equality:
1 === 1.0
false
As mentioned, one can write 3e8
for \(3 \cdot 10^8\), but in fact to Julia
the two values 3e8
and 3*10^8
are not quite the same, as one is stored in floating point, and one as an integer. One can use 3.0 * 10.0^8
to get a floating point equivalent to 3e8
.
Floating point includes the special values:
NaN
,Inf
. (Not so with integers.)
Floating point contains two special values: NaN
and Inf
to represent “not a number” and “infinity.” These arise in some natural cases:
1/0 ## infinity. Also -1/0.
Inf
0/0 ## indeterminate
NaN
These values can come up in unexpected circumstances. For example division by \(0\) can occur due to round off errors:
= 1e-17
x ^2/(1-cos(x)) ## should be about 2 x
Inf
In addition to special classes for integer and floating point values, Julia
has a special class for rational numbers, or ratios of integers. To distinguish between regular division and rational numbers, Julia
has the //
symbol to define rational numbers:
1//2
1//2
typeof(1//2)
Rational{Int64}
As you know, a rational number \(m/n\) can be reduced to lowest terms by factoring out common factors. Julia
does this to store its rational numbers:
2//4
1//2
Rational numbers are used typically to avoid round off error when using floating point values. This is easy to do, as Julia
will convert them when needed:
1//2 - 5//2 ## still a rational
1//2 - sqrt(5)/2 ## now a floating point
-0.6180339887498949
However, we can’t do the following, as the numerator would be non-integer when trying to make the rational number:
1 - sqrt(5)) // 2 (
MethodError: no method matching //(::Float64, ::Int64) Closest candidates are: //(::SymPyCore.Sym, ::Int64) @ SymPyCore ~/.julia/packages/SymPyCore/tOpxk/src/mathops.jl:26 //(::Rational, ::Integer) @ Base rational.jl:64 //(::AbstractArray, ::Number) @ Base rational.jl:82 ...
Complex numbers are an extension of the real numbers when the values \(i = \sqrt{-1}\) is added. Complex numbers have two terms: a real and imaginary part. They are typically written as \(a + bi\), though the polar form \(r\cdot e^{i\theta}\) is also used. The complex numbers have the usual arithmetic operations defined for them.
In Julia
a complex number may be constructed by the Complex
function:
= Complex(1,2) z
1 + 2im
We see that Julia
uses im
(and not i
) for the \(i\). It can be more direct to just use this value in constructing complex numbers:
= 1 + 2im z
1 + 2im
Here we see that the usual operations can be done:
^2, 1/z, conj(z) z
(-3 + 4im, 0.2 - 0.4im, 1 - 2im)
The value of \(i\) comes from taking the square root of \(-1\). This is almost true in Julia
, but not quite. As the sqrt
function will return a real value for any real input, directly trying sqrt(-1.0)
will give a DomainError
, as in \(-1.0\) is not in the domain of the function. However, the sqrt
function will return complex numbers for complex inputs. So we have:
sqrt(-1.0 + 0im)
0.0 + 1.0im
Complex numbers have a big role to play in higher level mathematics. In calculus, they primarily occur as roots of polynomial equation.
Compute the value of \(2^{1023}(2 -2^{-52})\) using 2.0
– not the integer 2
:
The result of sqrt(16)
is
The result of 16^2` is
The result of 1/2
is
The result of 2/1
is
Which number is 1.23e4
?
Which number is -43e-2
?
What is the answer to the following:
val = round(3.4999999999999999);
Recall that with 64-bit integers, we can only store numbers up to \(2^{63} - 1\) without overflowing. If you need more bits, Julia
provides the BigInt
class which allow for what is known as arbitrary precision arithmetic, where the number of bits used to store the number grows as needed. Using this allows one to compute \(2^{3^4}\) precisely as an integer:
x = BigInt(2)
answer = x^3^4
What is the answer?
Like the BigInt
class, Julia
also has a BigFloat
class for storing decimal numbers with arbitrary precision. Due to some technical details of how floating point numbers are stored, the number of bits must be fixed in advance of any computation—unlike BigInt
s whose precision can grow on-the-fly. Julia
provides the setprecision
command to set this number of bits (e.g. setprecision(1024)
). The default precision is \(256\).
Julia
when generated
Julia
version:
VERSION
v"1.10.4"
Packages and versions:
using Pkg
Pkg.status()
Status `~/work/mth229.github.io/mth229.github.io/Project.toml`
[a2e0e22d] CalculusWithJulia v0.2.6
[7073ff75] IJulia v1.25.0
[b964fa9f] LaTeXStrings v1.3.1
[ebaf19f5] MTH229 v0.3.2
[91a5bcdd] Plots v1.40.4
[f27b6e38] Polynomials v4.0.11
[438e738f] PyCall v1.96.4
[612c44de] QuizQuestions v0.3.23
[295af30f] Revise v3.5.14
[f2b01f46] Roots v2.1.5
[24249f21] SymPy v2.1.1
[56ddb016] Logging