using MTH229
using Plots
plotly()
Plots.PlotlyBackend()
A notebook for this material: ipynb
We see in this section how to easily create functions in Julia
. In the following sections we begin to do things with function, such as learning how to graph functions with Julia
.
For basic things creating a new function and plotting it is as familiar as this:
We begin by loading some packages:1
using MTH229
using Plots
plotly()
Plots.PlotlyBackend()
Then we define a function and plot it over \([0, \pi]\):
f(x) = sin(3x^2 - 2x^3)
plot(f, 0, pi)
Really, you’d be hard pressed to make this any shorter or more familiar. Of course, not everything is this easy so there are still things to learn, but keep in mind that 90% of what we want to do in these projects is really this straightforward.
Mathematically, a function can be viewed in many different ways. An abstract means is to think of a function as a mapping, assigning to each \(x\) value in the function’s domain, a corresponding \(y\) value in the function’s range. With computer languages, such as Julia
, the same holds, though there may be more than one argument to the function and with Julia
the number of arguments and type of each argument are consulted to see exactly which function is to be called.
Here we don’t work abstractly though. For a mathematical function (real-valued function of a single variable, \(f: \mathbb{R} \rightarrow \mathbb{R}\)), we typically just have some rule for what the function will do to \(x\) to produce \(y\), such as
\[ f(x) = \sin(x) - \cos(x). \]
In Julia
there are a few different ways to define a function, we start with the most natural one which makes it very simple to work with such functions.
Real-valued functions (\(f: \mathbb{R} \rightarrow \mathbb{R}\)) are often described in terms of elementary types of functions such as polynomial, trigonometric, or exponential. We give examples of each in the following.
Of course Julia
has readily available all the usual built-in functions found on a scientific calculator, and many more. See the section on mathematical operations and functions of the official Julia
documentation. In the following, we show how to translate some basic math functions into Julia
functions:
\[ f(x) = \cos(x) - \sin^2(x) \]
becomes
f(x) = cos(x) - sin(x)^2
f (generic function with 1 method)
The conversion from the commonly written form (\(\sin^2(x)\)) to the far less ambiguous \(\sin(x)^2\) is very important. This is necessary with Julia
– as it is with calculators – as there is no function sin^2
. In Julia, squaring is done on values – not functions, like sin
. (And most likely squaring of a function is more likely to be composition, which is not the usage here.) So, to have success, learn to drop the notations \(\sin^2(x)\) or for the arc sine function \(\sin^{-1}(x)\). These shortcuts are best left in the age when mathematics was done just on paper.
If you want to work in degrees you can do so with the degree-based trigonometric functions, which follow the same naming pattern save a trailing “d”:
fd(x) = cosd(x) - sind(x)^2
fd (generic function with 1 method)
This is not used in the following, rather, when needed converting from degrees to radians through multiplication by pi/180
is. (There is also deg2rad
.)
A mathematical definition like
\[ f(x) = 2\tan^{-1}\left(\frac{\sqrt{1 - x^2}}{1 + x}\right) \]
becomes
f(x) = 2atan( sqrt(1-x^2) / (1 + x) )
f (generic function with 1 method)
This particular function is just an alternative expression for the arc cosine (mathematically \(\cos^{-1}\) but in Julia
acos
) using the arctan function, as seen here:
f(.5) - acos(.5) ## nearly 0
-2.220446049250313e-16
The exponent in the inverse trigonometric functions is just mathematical notation for the longer expression “arctan” or “arccos”. (It definitely is not a reciprocal.) The Julia
functions – like most all computer languages – abbreviate these names to atan
, acos
or asin
.
The math function
\[ f(x) = e^{-\frac{1}{2}x^2} \]
Can be expressed as
f(x) = e^(-(1/2)*x^2)
f (generic function with 1 method)
The value of \(e\) is built-in to Julia
, but not immediately available. It is s exposed by the MTH229
package. But \(e\) can be inadvertently redefined. As such, it is a safer practice to use the exp
function, as in:
f(x) = exp(-(1/2)*x^2)
f (generic function with 1 method)
There isn’t much difference in use, but don’t try to do both at once, as in exp^(-(1/2)*x^2)
!
The mathematical notations for logarithms often include \(\ln\) and \(\log\) for natural log and log base 10. With computers, there is typically just log
for natural log, or with an extra argument the logarithm to other bases.
\[ f(x) = \ln(1 - x) \]
becomes just
f(x) = log(1 - x)
f (generic function with 1 method)
Whereas, the base 10 log:
\[ f(x) = \log_{10}(1 + x) \]
can be done through:
f(x) = log(10, 1 + x)
f (generic function with 1 method)
where the first argument expresses the base. For convenience, Julia
also gives the functions log10
and log2
for base \(10\) and \(2\) respectively.
In mathematics a typical observation is to recognize some object as a combination of simpler objects. For functions, we think of combining simpler functions into more complicated ones. For example, we can think of the sum of functions, \(h(x) = f(x) + g(x)\). The rule for each \(x\) is simply to add the results of the two rules for \(f\) and \(g\) applied to \(x\). Notationally, we might write this as either:
\[ h = f + g \]
or
\[ h(x) = f(x) + g(x). \]
The former treats \(f\) and \(g\) as function objects, the latter ties more closely to the concept of a function as a rule that operates on \(x\).
With Julia
the latter representation is more useful for defining combinations of functions. For example, if \(f(x) = \sin(x)\) and \(g(x) = x^2\), then we can combine these in several ways. The following illustrates several ways to combine the two functions \(f\) and \(g\):
f(x) = sin(x)
g(x) = x^2
h(x) = f(x) + g(x) # f + g
h(x) = f(x) - g(x) # f - g
h(x) = f(x) * g(x) # f * g
h(x) = f(x) / g(x) # f / g
h(x) = f(x)^g(x) # f^g
h (generic function with 1 method)
All these are based on underlying mathematical operators. In addition, for functions there is the operation of composition, where the output of one function is the input to another. For example:
h(x) = f(g(x)) # f ∘ g or sin(x^2)
h(x) = g(f(x)) # g ∘ f or (sin(x))^2
h (generic function with 1 method)
This operation is fundamentally non-commutative, as the above example illustrates.
Which of these functions will compute \(\sin^3(x^2)\)?
Which of these functions will compute
\[ \frac{1}{\sqrt{2\pi}} e^{-\frac{1}{2}x^2}? \]
Define the function \(f(x) = -16x^2 + 100\).
Is \(f(4)\) positive?
Define the function \(f(x) = x^3 - 3x + 2\)
What is the value of \(f(10)\)?
Define the function \(f(x) = x^5 + x^4 + x^3\)
What is the value of \(f(2)\)?
Which of these functions will compute \(f(x) = x^2 -2x + 1\)?
Which of these functions will compute
\[ f(x) = \frac{x^2 - 2x}{x^2 - 3x}? \]
Which of these functions will compute
\[ f(x) = e^{-x} \sin(x)? \]
If you want to define a more complicated function, say one with a few steps to compute, an alternate form for defining a function can be used:
function function_name(function_arguments)
...function_body...
end
The last value computed is returned unless the function_body
contains a return
call.
For example, the following is a more verbose way to define \(f(x) = x^2\):
function f(x)
return(x^2)
end
f (generic function with 1 method)
The line return(x^2)
, could have just been x^2
as it is the last (and) only line evaluated.
Imagine we have a complicated function, such as:
\[ g(x) = \tan(\theta) x + \frac{32}{200 \cos\theta} x - 32 \log\left(\frac{200 \cos\theta}{200\cos\theta - x}\right). \]
where \(k\) is the constant 1/2 and \(\theta=\pi/4\). To avoid errors in transcribing, it can be be useful to break such definitions up into steps. Here we note the repeated use of \(200\cos(\theta)\) in the definition of \(g(x)\), so we give that value the intermediate name of a
function g(x)
= pi/4
theta = 200*cos(theta)
a tan(theta)*x + (32/a)*x - 32*log(a/(a-x))
end
g (generic function with 1 method)
From this, we can easily see that we would need to be concerned as \(x\) approaches the value of a
, as when \(x \geq a\) the logarithm won’t be defined.
Here is a different example, where we define a “hockey stick” function, a name for functions that are flat then increase linearly after some threshold.
An old-school cell-phone plan might cost $30 for the first 500 minutes of calling and 25 cents per minute thereafter. Represent this as a function of the number of minutes used.
Here we need to do one of two things depending if \(x\) is greater or less than \(500\). There are different ways to do this, here we use an if-else-end
statement, which takes the following form:
function cell_phone(x)
if x < 500
return(30.0)
else
return(30.0 + 0.25*(x-500))
end
end
cell_phone (generic function with 1 method)
To see what it would cost to talk for 720 minutes in a month, we have:
cell_phone(720)
85.0
We return 30.0
above – and not the integer 30
– when \(x<500\) so that the function always returns a floating point value and not an integer if less than 0 and a floating point value if bigger. In general it is a good programming practice to have functions return only one type of variable for a given type of input. In this case, as the answer could be real-valued – and not just integer-valued, we want to return floating point values.
A quick plot will show why the above function is called a “hockey stick” function:
plot(cell_phone, 0, 1000)
When functions that have different rules based on the specific value of \(x\) that is input, the use of “cases” notation is common. For example,
\[ f(x) = \begin{cases} \cos(x) & x \geq 0\\ 1 - e^{-1/x^2} & \text{otherwise}. \end{cases} \]
Translating this notation to Julia
can also be done with the if-else-end
construct:
function f(x)
if x >= 0
cos(x)
else
1 - exp(-1/x^2)
end
end
f (generic function with 1 method)
The expression after if
is a Boolean value (a true
or false
value). In these examples they are generated through the Boolean operators, which include the familiar comparison symbols <
, <=
, ==
, >=
, and >
. (Only ==
takes learning, as double equal signs are used for comparison, a single one is for assignment.)
One can use the so-called ternary operator a ? b : c
for simple if-else-end
statements as above.
Basically, a ? b : c
is the same as the more verbose
if a
b
else
c
end
So the cell-phone example could have been a one-liner:
cell_phone(x) = x < 500 ? 30.0 : 30.0 + 0.25*(x - 500)
cell_phone (generic function with 1 method)
When x < 500
the expression right after ?
is evaluated, and if not, the expression after :
is.
For mathematical functions, the directness of the ternary operator usually makes it a preferred choice over if-else-end
.
It can be convenient to nest ternary operators. In particular, when the cases involve have more than 2 possibilities. The following does something depending on whether x
is positive, negative or zero:
heaviside(x) = x > 0 ? 1.0 : x == 0.0 ? 0.0 : -1.0
heaviside (generic function with 1 method)
That is a mess to read, but easy to write. It can be made a bit clearer by using parentheses around the case where x
is not greater than 0:
heaviside(x) = x > 0 ? 1.0 : (x == 0.0 ? 0.0 : -1.0)
heaviside (generic function with 1 method)
Similarly, new lines can clear up the flow:
heaviside(x) = x > 0 ? 1.0 :
== 0.0 ? 0.0 :
x -1.0
heaviside (generic function with 1 method)
Which of these definitions will be the equivalent of \(f(x) = |x|\)? (The abs
function is already one):
The sign
function returns \(-1\) for negative numbers \(1\) for positive numbers and \(0\) for 0. Which of these functions could do the same?
T-Mobile has a pay as you go cell phone plan with the following terms:
Which of these functions will model this?
A alternate mathematical notation for a function that emphasizes the fact that \(f\) maps \(x\) to some value \(y\) involving the rule of \(f\) is to use an arrow as:
\[ x \rightarrow -16x^2 + 32x \]
You can do the exact thing in Julia
to create a function:
-> -16x^2 + 32x x
#11 (generic function with 1 method)
This expression creates a function object, but since we didn’t bind it to a variable (that is, we didn’t give the function a name) it was immediately forgotten. Such functions without a name are known as anonymous functions.
Anonymous functions can be named. For example, we might have:
= x -> -16x^2 + 32x motion_of_particle
#13 (generic function with 1 method)
This names a function object; it does not create a method for a generic function (to be described later). One source of possible confusion is the name given to an anonymous function can not be used as a name for a function defined through the style f(x) = ...
, as that creates a method for a generic function. Vice versa.
The concept of a function is of much more general use than its restriction to mathematical functions of single real variable. A natural application comes from describing basic properties of geometric objects. The following function definitions likely will cause no great concern when skimmed over:
Area(w, h) = w * h # of a rectangle
Volume(r, h) = pi * r^2 * h # of a cylinder
SurfaceArea(r, h) = pi * r * (r + sqrt(h^2 + r^2)) # of a right circular cone
SurfaceArea (generic function with 1 method)
The right-hand sides may or may not be familiar, but it should be reasonable to believe that if push came to shove, they could be looked up. However, the left-hand sides are subtly different – they have two arguments, not one. In Julia
it is trivial to define functions with multiple arguments – we just did. However, Julia
has two means to specify additional arguments, as described briefly in the following.
Functions like Area
above use position to match the values used when calling the function with the variables in the function definition. This is quite natural. We would expect Area(5,6)
to use w=5
and h=6
when the evaluation is performed.
The function call log(x)
finds \(\log_e(x) = \ln(x)\); the function call log2(x)
finds \(\log_2(x)\); the function call log10(x)
finds \(\log_{10}(x)\). More generally we have log(a, b)
to find the log for a specified base.
Does log(a, b)
find \(\log_a(b)\) or \(\log_b(a)\)?
In a function definition, there can be \(0, 1\), or more positional arguments, even an unspecified variable number of arguments. The values may be restricted to having different types. Default values may be given. Here we stick to the simplest cases, as illustrated above, where the variables are all named and separated by commas in the function definition.
Positional arguments work well for many cases, but are not always the most covenient approach:
if there are numerous arguments, the user must know what position matches to what variable which can mean consulting the documentation or some other means. For example, in Volume
above is the radius the first or second argument? It is hard to guess without peeking.
if there are numerous arguments but the user only wants to change a few from the default values the user would need to still use all the arguments. (Well, not technically, but practically.)
The latter issue is well solved with keyword arguments.
Keyword arguments are widely used when plotting with Julia
as they conveniently help customize specific attributes of a plot on a case-by-case basis. The Plots
package we will illustrate utilizes positional arguments for the data to be plotted and keyword arguments to adjust attributes of how the data is shown.
For an example using keyword arguments, we revisit this function
\[ g(x) = \tan(\theta) x + \frac{32}{200 \cos\theta} x - 32 \log\left(\frac{200 \cos\theta}{200\cos\theta - x}\right). \]
The value \(\theta\) may have a default value of \(\pi/4\) for most uses, but rather than hard code that value, we set it as a default and allow it to be adjusted by the user by passing the argument theta=...
.
Keyword arguments are separated from any positional arguments by a semicolon and are given a default value:
For example:
function g(x; theta=pi/4)
= 200*cos(theta)
a tan(theta)*x + (32/a)*x - 32*log(a/(a-x))
end
g (generic function with 1 method)
If keyword arguments are not specified when called, the defaults are used:
g(50)
47.35323911536457
Is the same as g(50, theta=pi/4)
.
To modify a keyword argument it is specified as key=value
after any positional arguments are specified in the function call.
For example, if the angle were less than the default of \(\pi/4\) would the value of \(f\) be smaller or larger?
g(50, theta=pi/8) ## smaller in this case.
19.272845247997708
The pattern g(theta=pi/8, 50)
would error.
We used a comma to separate the positional argument from the keyword argument. Commas are used to separate different positional arguments; commas are used to separate different keyword arguments, so this is natural. However, a semicolon can also be used, to mirror their mandated use when defining a function with keyword arguments.
Earlier we saw the log
function can use a second argument to express the base. This function is defined by log(b, x) = log(x) / log(b)
. The log(x)
value is the natural log, and this definition just uses the change-of-base formula for logarithms.
But not so fast, on the left side is a function with two arguments and on the right side the functions have one argument – yet they share the same name. How does Julia
know which to use? Julia
uses the number, order, and type of the positional arguments passed to a function to determine which method for the generic function to use.
This is technically known as multiple dispatch or polymorphism. As a feature of the language, it can be used to greatly simplify the number of functions the user must learn. The basic idea is that many functions are “generic” in that they will work for many different scenarios.
A good example is addition. In experience, different algorithms are used for adding integers (using carrying); adding decimal numbers (align the decimal points); adding complex numbers (add real and imaginary parts separately); adding polynomials (add coefficients of like terms); etc. Each definition may be different, but the concept the same. For the end user, only the operator +
need be used. A similar thing occurs with Julia
.
Some upcoming example usage of generic functions will include:
find_zero(f, c)
and find_zero(f, a, b)
will both find a zero of f
, but the first form will look for one near c
; the second for a zero between a
and b
– while using a totally different algorithm.
plot(f, a, b)
will plot the function f
over the interval [a,b]
. However, plot(xs, ys)
will plot the points \((x_1, y_1), (x_2, y_2), \dots (x_n, y_n)\) specified with two contains.
For each example, the same general idea is being asked (finding a zero or rendering a plot) but different implementations, or methods, are eventually used.
Julia
has two types of functions: anonymous functions and generic functions. (There are also “callable” structs which act like functions.) Anonymous functions, despite their name, can be named in the way a name can be given to a value, just like naming any other value.
Generic functions are more complicated. In order to have one name and many methods, a method table is used behind the scenes.
Generic function names in the method table (e.g., plot
, log
, etc.) can not be repurposed as variable names or vice versa. Julia
is a dynamic language otherwise, where the type of values attached to a variable name can be changed, but not in this usage.
Hence the following will error, as h1
is used a variable and then a function:
= 4 h1
4
h1(x) = 4
LoadError: cannot define function h1; it already has a value
Similarly, this will error when the attempt to redefine h2
is made:
h2(x) = 4
h2 (generic function with 1 method)
= 4 h2
LoadError: invalid redefinition of constant Main.h2
The suggestion is to use familiar names for variables (e.g., x
, y
, a
, …) and either descriptive function names or the usual common names (e.g. f
, g
, …) for generic functions (those defined by f(x) = ...
.)
Julia
’s multiple dispatch allows multiple functions with the same name. The function that gets selected depends on the arguments given to the function. We can exploit this to simplify our tasks. For example, consider this optimization problem:
For all rectangles of perimeter \(20\), what is the one with largest area?
The start of this problem is to represent the area in terms of one variable. We see next that composition can simplify this task, which when done by hand requires a certain amount of algebra.
Representing the area of a rectangle in terms of two variables is easy:
Area(w, h) = w * h
Area (generic function with 1 method)
But the other fact about this problem – the constraint that the perimeter is \(20\) – means that height depends on width. For this question, we can see that \(P=2w + 2h\) so that
h(w) = (20 - 2 * w)/2
h (generic function with 1 method)
By hand we would substitute this last expression into that for the area and simplify (to get \(A=w\cdot (20-2 \cdot w)/2 = -w^2 + 10\)). However, within Julia
we can let composition do the substitution and leave algebraic simplification for Julia
to do:
Area(w) = Area(w, h(w))
Area (generic function with 2 methods)
This might seem odd, as now we have two different but related functions named Area
. Julia will decide which to use based on the number of arguments when the function is called. This allows both to be used on the same line, as above. This usage is not common with computer languages, but is a feature of Julia
which is built around the concept of generic functions with multiple dispatch rules to decide which rule to call.
For example, the plot
function, when called as below, expects functions of a single numeric variable. Behind the scenes, then the function A(w)
will be used in this graph:
plot(Area, 0, 10)
From this, we can see that that the width yielding the maximum area is \(w=5\), and so \(h=5\) as well.
Many descriptions involve both a variable (or variables) and different parameters. In calculus many functions are part of wider family of functions (exponential, logaorithmic, etc.) that use parameters to fit the family to a problem at hand. Function transformations are an example.
A even more familiar example is the equation for a line \(y = m\cdot x + b,\) where \(x\) is a variable and \(m\) and \(b\) are parameters for a given application.
The equation written as a function might be implemented through:
f(x) = m * x + b
f (generic function with 1 method)
The value of x
is passed into the function when f
is called, but where would the values for m
and b
be found? In math, these parameters would come from the context of the problem being discussed. On the computer, there are scoping rules used to resolve the task of identifying a value assigned to a variable in a given scope.
Were f
defined as above, the values of m
and b
would likely come from the global scope. This uses the current values assigned to the symbols m
and b
and not the values when f
is defined. This means if those values are changed, the values f
computes will be changed.
For the most part this is just fine, but it is fragile and can lead to subtle mistakes.
In general, using global variables is frowned upon for the reason above and for performance reasons. Rather is it suggested that parameters are passed in the functions that use them. We illustrate three possible means to specify parameters when calling a function in the following example.
The line is an essential object in calculus and other subjects. Their ubiquity leads to different mathematical descriptions:
The general equation is useful, as it can describe vertical lines, where the slope is undefined. However, the use of the slope-intercept or point-slope forms is more commonly employed in calculus, especially the point-slope form as the data describing a line is usually presented in terms of these two quantities.
Both of these latter forms can be written in function form, e.g. \(f(x) = m\cdot x + b\).
Consider the task mentioned above of making a function for the slope-intercept form of a line, \(f(x) = m\cdot x + b\), where \(x\) is the variable; \(m\) and \(b\) the parameters.
Some different forms that can be used:
three_arguments(x, m, b) = m * x + b
three_arguments (generic function with 1 method)
f(x, p)
form, with two positional arguments, and where the parameters are passed through in a container:fxp(x, p) = p[1] * x + p[2] # p = (m, b) is passed
fxp (generic function with 1 method)
keyword_arguments(x; m=0, b=1) = m * x + b
keyword_arguments (generic function with 1 method)
For the point-slope form of a line, these three styles might become:
four_arguments(x, m, x1, y1) = y1 + m * (x - x1)
fxp(x, p) = p[3] + p[1] * (x - p[2]) # p = (m, x1, y1) is passed
keyword_arguments(x; m=0, x1 = 0, y1 = 0) = y1 + m * (x - x1)
keyword_arguments (generic function with 1 method)
All three forms have their merits:
The multiple arguments form is the easiest to understand. The style is used in most calculus books to describe multi-variable functions.
The f(x, p)
style is an extremely common design pattern in the Julia
ecosystem. It provides a consistent interface for scalar values and multivariate values just by passing in containers of values for x
and p
. (We would pass in a container of (m,b)
or (m, x1, y1)
to use the above; more sophisticated containers and unpacking schemes are used in practice. Named tuples in particular are convenient.)
The keyword arguments form is utilized in many statistics book, as most probability distributions are parameterized. This style is very explicit for the user; the default values and calling style allows for the easiest customization from the defaults. Keyword arguments do not participate in method dispatch.
In computer science terminology, Julia
is a language which treats functions as first class objects. In the plot
function, illustrated above, the basic syntax for the call is plot(f, a, b)
, with the function object, f
, being passed as an argument to plot
.
This particular usage fits into a more general template: verb(function_object, arguments....)
that will be seen in other contexts later in these notes.
In calculus an operator is some operation that takes a function and produces a different, but related function. Calculus has two main operators: the derivative and, in some scenarios, the integral, as will be discussed elsewhere.
In Julia
it is natural to use functions which mirror mathematical operators: functions which accept other functions as inputs and output function objects. We call such functions operators here, though that term isn’t so standard. In the example below, we give an illustration. However, the main example – the derivative – will wait until later.
One difference we highlight is that since operators return functions we simply assign them names such as f = ...
, when that is desirable, rather than use the function definition notation of f(x) = ...
. Though we are mindful that the same name can’t be used for both styles, as the former style assigns a variable to a function object, the latter creates a method for a function name.
Returning to our discussion of a line, suppose we have the function using multiple arguments to represent parameters:
point_slope(x, m, x1, y1) = y1 + m * (x - x1)
point_slope (generic function with 1 method)
In calculus, a secant line is related to a function, \(f(x)\), and two points \((a,f(a))\) and \((b, f(b)).\) As two points determine a line, this is well defined. Suppose f
, a
, and b
are specified, say by:
f(x) = x^2
= 0, 1 a, b
(0, 1)
then we can create the secant line for this data as:
= (f(b) - f(a)) / (b-a)
m = a, f(a) # or b, f(b) it doesn't matter
x1, y1 secant_line(x) = point_slope(x, m, x1, y1)
secant_line (generic function with 1 method)
This takes the values of f
, a
, and b
(or the derived parameters m
, x1
, and y1
) and creates a function of just x
. This function can be called like any other function:
secant_line(3)
3.0
While the above construction is easy to understand, it is also a bit of a fragile construction, as the values of the parameters, m
, x1
, and y1
are global variables derived from the basic inputs f
, a
, and b
. As mentioned earlier, when secant_line
is called the current values assigned to the global variables are used – not necessarily the same values as when secant_line
is defined.
To be explicit about the values of the parameters being fixed, a different approach is typically used.
Below, we write a function to return an anonymous function with the values for the parameters fixed. In computer science language, we are creating a closure:
function Secant(f, a, b)
= (f(b) - f(a)) / (b -a)
m = a, f(a)
x1, y1 -> point_slope(x, m, x1, y1)
x end
Secant (generic function with 1 method)
The same “secant_line
” function would be generated through:
= Secant(f, a, b) sl
#18 (generic function with 1 method)
To see they are the same, let’s just call them at an arbitrary value of x
and compare:
sl(3), secant_line(3)
(3.0, 3.0)
sl
” – not “sl(x)
”The Secant
function above constructs and returns a function, so sl
is a function object. We don’t need to write sl(x)
on the left-hand side, as we do when we define a generic function or method through an expression of its arguments. This can be done, were the following usage employed:
sec_line(x) = Secant(f, a, b)(x)
sec_line (generic function with 1 method)
but this is not recommended: it is a bit awkward to type, is inefficient, and—most importantly—uses global values for a
and b
each time it is called.
The secant(f, a, b)
function in the MTH229
package returns a function like Secant
that has a reminder of what it does when used:
f(x) = x^2
= 0, 1
a, b = secant(f, a, b) sl
Function of `x` to compute the secant line of `f` between `a` and `b`:
f(a) + ((f(b)-f(a)) / (b-a) * (x-a)
The sl
object is a black-box function, but it can be used to identify the slope and the intercept of the secant line being represented.
For example, the intercept of a line (the \(b\) in \(y = m\cdot x + b\)) is the value of the function when \(x=0\) or:
= sl(0) b
0.0
The slope can be found by taking any two points, forming \((x_0, y_0)\) and \((x_1, y_1)\) and using the slope formula:
= 2, 3
a, b = a, sl(a)
x0, y0 = b, sl(b)
x1, y1 = (y1 - y0) / (x1 - x0) m
1.0
Or more directly, the two intermediate lines can be skipped:
= (sl(b) - sl(a)) / (b - a) m
1.0
The package defines a related function tangent(f, c)
to return a function which computes the tangent line to f
at c
:
f(x) = x^2
= 1
c = tangent(f, c) tl
Function of `x` to compute the tangent line of `f` at `c`:
f(c) + f'(c) * (x-c)
For the line described by tl
, compute:
A partial function application is related to the closure above. Consider our first example for a function representing the slope-intercept form using three arguments:
slope_intercept(x, m, b) = m * x + b
slope_intercept (generic function with 1 method)
For a fixed m
and b
, we can use the following style to create a function of x
as follows:
SlopeIntercept(m, b) = x -> slope_intercept(x, m, b)
SlopeIntercept (generic function with 1 method)
When the function returned by SlopeIntercept
is called, it uses the fixed values for m
and b
.
The pattern of fixing a value of a function is common enough that there are also functions Base.Fix1
and Base.Fix2
. Both are for functions of two variables, f(x,y)
. The first is used to create a function of y
for a fixed x
; the second to create a function of x
for a fixed y
. While the style of SlopeIntercept
above, where an anonymous function is returned, is natural, the implementation of these two “fix” functions is more performant and their use appears often behind the scenes.
Which of these function definitions corresponds to shifting the function f
to the right by c
units and up by d
units with a default of \(0\) and \(0\):
Which of these definitions will lengthen the period of a periodic function \(f\) by a factor of \(c\), with a default of \(1\)?
The following transform of a function is at the core of wavelet theory:
g(t; a=1, b=0) = (1/sqrt(a)) * f((t - b)/a)
g (generic function with 1 method)
If \(f(x) = \sin(x)/x\) and \(a=2\) and \(b=1\) compute \(g(0, a=2, b=1)\).
Let \(g\) be defined by:
function g(x; theta=pi/4)
= 200*cos(theta)
a tan(theta)*x + (32/a)*x + 32*log((a-x)/a)
end
g (generic function with 1 method)
For x
in 20, 25, 30, 35, 40, 45 degrees, what value will maximize g(125, theta=x*pi/180)
?
What anonymous function will compute \(\sin(x^2)\)?
What anonymous function of \(x\) will return the polynomial \(x^2 - 2x\):
What does this function do?
function mystery(f)
-> -f(x)
x end
mystery (generic function with 1 method)
What does this operator do?
function trim(f, c)
-> abs(f(x)) <= c ? f(x) : NaN
x end
trim (generic function with 1 method)
This section presents some additional details on writing functions in Julia
that are here for informational purposes only.
As mentioned, the value returned by a function is either the last value executed or any value returned by return
. For a typical real valued function \(f\) this is usually just a number. Sometimes it is convenient to return more than one value. For this a tuple proves useful:
A tuple is a container for holding different objects at once. They are made quite simply by enclosing the values in parentheses:
1, "one") (
(1, "one")
Tuples have many uses, but here we want to focus on their use as return values. Here is a somewhat contrived example. Imagine you write a function to compute the value of \(f(x) = x^x\), but you want to ensure \(x\) is positive, as otherwise there will be an error. You can do this, where we return a value of NaN
and a message when the user tries to use a negative number:
f(x) = x > 0 ? (x^x, "") : (NaN, "You can't use non-positive numbers")
f (generic function with 1 method)
We include a message even when the value of \(x\) is okay, as it is good practice –though not a requirement of Julia
– to always return the same type of object, regardless the input.
A simple call would be:
f(-1)
(NaN, "You can't use non-positive numbers")
We get a tuple back. Julia
makes working with tuple return values very easy. We can destructure them by simply placing two variable names on the left-hand side:
= f(-1) # alternatively: (a, b) = f(-1) a, msg
(NaN, "You can't use non-positive numbers")
A less artificial example will be discussed later: the quadgk
function which estimates the value of an integral. For this computation both the value and an estimated maximum error are of interest, so both are returned using a tuple.
Typical functions here are real-valued functions of a single variable. The easiest way to use these is to just mimic the regular mathematical notation as much as possible. However, there are times where we want to be specific about what possible values a user can place into a function. For example, a naive function to compute the binomial coefficients,
\[ { n \choose k } = \frac{n!}{(n-k)! k!}, \]
can be specialized to just integer values with:
binom(n::Integer, k::Integer) = factorial(n)/(factorial(n-k) * factorial(k))
binom (generic function with 1 method)
The extra bit ::Integer
is called a type annotation and specializes n
and k
so that this function only is called with both n
and k
are of this type.
As defined, we can call our binom
function as:
binom(10, 4)
210.0
But not as follows (\(\pi\) is not an integer):
binom(10, pi)
MethodError: no method matching binom(::Int64, ::Irrational{:π}) Closest candidates are: binom(::Integer, ::Integer) @ Main In[100]:1
(The actual binomial
function is much better than this, as it doesn’t divide a big number by a big number, which can cause real issues with loss of precision, though it does specialize to integers, and any sub-type. It also always returns an integer, whereas ours returns a floating-point value.)
Types in Julia
are a more complicated matter than we want to get into here, but we do want to list the common types useful for basic calculus: Function
, Real
, Integer
, Rational
, Complex
, and Number
(real or complex).
Clearly the latter ones should nest, in that an object of type Integer
should also be of type Real
. This means when we specialize a mathematical function, it is enough to specify values of Real
.
The MTH229
package loads a small package, SimpleExpressions
, which can be used to blur the distinction between an expression and a function. Later on, the SymPy
package will be illustrated which allows this blurring and much more.
Consider the polynomial \(p = 100 + 20x - 16x^2\). Polynomials can be viewed either as a mathematical expression consisting of indeterminates or as a polynomial function.
As an expression of indeterminates, polynomials can be algebraically combined, eg. added, multiplied, etc. They can be factored; in calculus integrated and differentiated.
As functions they can be evaluated; used as examples of continuous and differentiable functions; integrated and differentiated, etc.
Within Julia
the distinction is important:
Unlike expressions, function’s can’t be added or subtracted without the effort of creating a new function, as previously illustrated.
To illustrate, without first setting a value for x
, the expression below would error:
= 2
x = 100 + 20x - 16x^2 p
76
As 2
is assigned to x
, the value 2
is substituted in for the symbol x
and the expression is immediately evaluated and assigned to p
.
Contrast this with the definition as a polynomial function:
f(x) = 100 + 20x - 16x^2
f (generic function with 1 method)
The variable x
need not be defined when f
is defined, a value for the variable is assigned when the function is called. For example:
f(2)
76
The SimpleExpressions
package allows the creation of one symbolic variable and optionally one symbolic parameter. These can be used in an expression as above. However, rather than being evaluated immediately in the expression, the evaluation steps involving the variable are queued up and deferred until a value is assigned.
The @symbolic
macro is used to create the variable, which is then used within other expressions:2
@symbolic x
= 100 + 20x - 16x^2 p
(100 + (20 * x)) - (16 * (x ^ 2))
This symbolic expression can be added and multiplied, as is typically done by hand:
= (1 + 2p)*p^2 q
(1 + (2 * ((100 + (20 * x)) - (16 * (x ^ 2))))) * (((100 + (20 * x)) - (16 * (x ^ 2))) ^ 2)
A new expression is returned and assigned to q
above. These symbolic expressions are not simplified.3
The expressions in SimpleExpressions
are also functions4 and can be used where functions would be.
For example, we can easily call them:
p(2)
76
or
q(3)
8448
Importantly, these expressions can be passed in as functions to other functions. Skipping ahead a bit, we see that a simple expression can be used in place of a function when plotting:
plot(x^5 - x - 1, -1, 1.5) # Plots is loaded
One final feature of simple expressions is they can also represent equations. This is done by separating the left and right hand sides with a ~
.
A plot recipe is defined, so plotting of these equations is straightforward and highlights the two sides as separate functions:
= sin(x^2) ~ x/2
eqn
plot(eqn, 0, pi)
Again, skipping ahead, we see the three intersection points in \([0,\pi]\) can be identified with:5
solve(eqn, 0, pi)
3-element Vector{Float64}:
0.0
0.505482272339305
1.511542718555805
Finally, to show that having a parameter might be useful, what if the intersection points in terms of the slope of the line (\(1/2\) above) was of interest? In the following we parameterize it and show how to substitute in a value for the parameter:
@symbolic x p
= sin(x^2) ~ x/p
eqn1 solve(eqn1(:, 3), 0, pi)
5-element Vector{Float64}:
0.0
0.3340259282913851
1.6052940219459464
2.7243212874268083
2.857225258605647
As can be imagined, the number of intersection points depends on how flat the line is. This shows for p=2
three intersection points, but for p=3
five such.
The @symbolic
macro treats the first symbol as a symbolic variable, the second (when given) as a symbolic parameter. The notation eqn(:, 3)
substitutes 3
in for the symbolic parameter p
, returning an equation in x
that can be passed to the zero-finding function.
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
Packages are a means to extend base Julia
. There are thousands of add-on packages for Julia
, but these notes basically use just the two here, which rely on a handful of other add-on packages.↩︎
The SymPy
package will be used later on in these notes. It too has symbolic variables, but those variables have much more flexibility and the SymPy
package has many, many more features. SimpleExpressions
sole purpose is very modest, just to make basic expressions of a single variable and optional parameter act like functions.↩︎
Later, when SymPy
is used for symbolic computations, some basic simplification happens automatically.↩︎
The type holding a simple expression subtypes Function
and has a “call” method defined to allow the expressions to have the same calling style as a function.↩︎
The solve
generic finds solutions to a problem. It is widely used within the Julia
ecosystem, but not so much in these notes. The call solve(f::Function, ...)
has too many different possible interpretations to privilege just one; to call a desired one, the function is typically wrapped into a distinguishing type. So, the use of solve
here would be solve(ZeroProblem(f,...),...)
which gets a bit verbose for this level, where a different name will suffice, as seen later. However, in this example, the eqn
expression has a specific type, so eqn
need not be wrapped into another special type.↩︎