1  Julia as a calculator


A notebook for this material: ipynb

1.1 Introduction

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:

  • the use of the basic math operators
  • an understanding of the order of operations, or knowing when parentheses are needed
  • how to use the buttons that compute functions such as the sine function
  • and how to use the memory feature to store intermediate computations.

Parallels of each of these will be discussed in the following.

1.2 Expressions

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

1.2.1 Practice

Question

Compute \(22/7\) in Julia.


Question

Compute \(\sqrt{220}\) in Julia.


Question

Compute \(2^8\) in Julia.


1.3 Precedence

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.

1.3.1 Basics of PEMDAS

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 parentheses
  • (E) then exponents (or powers)
  • (MD) then multiplication or division
  • (AS) 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)
Alert

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.

1.3.2 Same precedence – what to do

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.

Alert

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

1.3.3 Practice

Question

Which of the following is a valid Julia expression for

\[ \frac{3 - 2}{4 - 1} \]

that uses the least number of parentheses?

Select an item

Question

Which of the following is a valid Julia expression for

\[ \frac{3\cdot2}{4} \]

that uses the least number of parentheses?

Select an item

Question

Which of the following is a valid Julia expression for

\[ 2^{4 - 2} \]

that uses the least number of parentheses?

Select an item

Question

One of these three expressions will produce a different answer, select that one:

Select an item

Question

One of these three expressions will produce a different answer, select that one:

Select an item

Question: the minus sign as unary operator

One of these three expressions will produce a different answer, select that one:

Select an item

Question

Compute the value of

\[ \frac{9 - 5 \cdot (3-4)}{6 - 2}. \]


Question

Compute the following using Julia:

\[ \frac{(.25 - .2)^2}{(1/4)^2 + (1/3)^2} \]


Question

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} \]


Question

Compute the following using Julia:

\[ \frac{3 - 2^2}{4 - 2\cdot3} \]


Question

Compute the following using Julia:

\[ (1/2) \cdot 32 \cdot 3^2 + 100 \cdot 3 - 20 \]


1.4 Using functions

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.)

1.4.1 trigonometric functions

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

1.4.2 Inverse trigonometric function

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.

1.4.3 Exponential and logs

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

1.4.4 Some useful functions

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).

1.4.5 Practice

Question

What is the value of \(\sin(\pi/10)\)?


Question

What is the value of \(\sin(52^\circ)\)?


Question

Is \(\sin^{-1}(\sin(3\pi/2))\) equal to \(3\pi/2\)?

Select an item

Question

What is the value of round(3.5000)


Question

What is the value of sqrt(32 - 12)


Question

Which is greater \(e^\pi\) or \(\pi^e\)?

Select an item

Question

What is the value of \(\pi - (x - \sin(x)/\cos(x))\) when \(x=3\)?


1.5 Variables

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\):

x = 2
2

So, when we evaluate

x^2
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:

x = sqrt(2) ## a Float64 now
1.4142135623730951

In Julia one can have single letter names, or much longer ones, such as

some_ridiculously_long_name = 3
3
some_ridiculously_long_name^2
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.

Alert

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,

x = 2 + 3
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.)

1.5.1 Multiple assignments

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:

a=0
b=1
1

Multiple expressions can be more tersely written by separating each expression using a semicolon:

a=0; b=1;

Note that Julia makes this task even easier, as one can do multiple assignments via “tuple destructuring:”

a, b  = 0, 1        ## printed output is a "tuple"
a + b
1

1.5.2 Practice

Question

Let \(a=10\), \(b=2.3\), and \(c=8\). Find the value of \((a-b)/(a-c)\).


Question

What is the answer to this computation?

a = 3.2; b=2.3
a^b - b^a

Question

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?


1.6 Numbers

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.

1.6.1 Integers and floating point numbers

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:

x = -3
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:

x = -3
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!

Gotchas

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.

Comparing values

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

1.6.2 Scientific notation

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:

x = 1e-17
x^2/(1-cos(x))          ## should be about 2
Inf

1.6.3 Rational numbers

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

1.6.4 Complex numbers

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:

z = Complex(1,2)
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:

z = 1 + 2im
1 + 2im

Here we see that the usual operations can be done:

z^2, 1/z, conj(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.

1.6.5 Practice

Question

Compute the value of \(2^{1023}(2 -2^{-52})\) using 2.0 – not the integer 2:

Select an item

Question

The result of sqrt(16) is

Select an item

Question

The result of 16^2` is

Select an item

Question

The result of 1/2 is

Select an item

Question

The result of 2/1 is

Select an item

Question

Which number is 1.23e4?

Select an item

Question

Which number is -43e-2?

Select an item

Question

What is the answer to the following:

val = round(3.4999999999999999);

 🎁

Question

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?

Select an item
Alert

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