2  Functions in Julia


A notebook for this material: ipynb

2.1 Introduction

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.

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

2.4 Common functions

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:

2.4.1 Trigonometric functions

\[ f(x) = \cos(x) - \sin^2(x) \]

becomes

f(x) = cos(x) - sin(x)^2
f (generic function with 1 method)
About exponents and functions…

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.

Degrees, not radians

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

2.4.2 Inverse trigonometric functions

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.

2.4.3 Exponential function

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

2.4.4 Logarithms

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.

2.5 Algebra of functions

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.

2.5.1 Practice

Question

Which of these functions will compute \(\sin^3(x^2)\)?

Select an item

Question

Which of these functions will compute

\[ \frac{1}{\sqrt{2\pi}} e^{-\frac{1}{2}x^2}? \]

Select an item

Question

Define the function \(f(x) = -16x^2 + 100\).

Is \(f(4)\) positive?

Select an item

Question

Define the function \(f(x) = x^3 - 3x + 2\)

What is the value of \(f(10)\)?


Question

Define the function \(f(x) = x^5 + x^4 + x^3\)

What is the value of \(f(2)\)?


Question

Which of these functions will compute \(f(x) = x^2 -2x + 1\)?

Select an item

Question

Which of these functions will compute

\[ f(x) = \frac{x^2 - 2x}{x^2 - 3x}? \]

Select an item

Which of these functions will compute

\[ f(x) = e^{-x} \sin(x)? \]

Select an item

2.6 Multi-step functions

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.

Example: Many parts

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)
     theta = pi/4
     a = 200*cos(theta)
     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.

2.7 Functions defined by cases

Example Hockey-stick functions

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
A subtlety

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

2.7.1 The “ternary operator”, a simple alternative to if-else-end

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.

Example: Nesting the ternary operator

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 :
               x == 0.0 ? 0.0 :
               -1.0
heaviside (generic function with 1 method)

2.7.2 Practice

Question

Which of these definitions will be the equivalent of \(f(x) = |x|\)? (The abs function is already one):

Select an item

Question

The sign function returns \(-1\) for negative numbers \(1\) for positive numbers and \(0\) for 0. Which of these functions could do the same?

Select an item

Question

T-Mobile has a pay as you go cell phone plan with the following terms:

  • You pay 30 per month and this includes the first 1500 minutes or text messages combined.
  • Each additional minute or message costs 13 cents.

Which of these functions will model this?

Select an item

2.8 Anonymous functions

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:

x -> -16x^2 + 32x
#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:

motion_of_particle = x -> -16x^2 + 32x
#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.

2.9 Functions of multiple variables

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.

2.9.1 Positional arguments

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.

Question

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

Select an item

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.

2.9.2 Keyword arguments

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)
     a = 200*cos(theta)
     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.

2.9.3 Generic functions

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.

Anonymous and generic functions

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:

h1 = 4
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)
h2 = 4
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) = ....)

Example: composition and multiple dispatch

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.

2.10 Functions with parameters

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.

2.10.1 Example: lines

The line is an essential object in calculus and other subjects. Their ubiquity leads to different mathematical descriptions:

  • The general equation of a line: \(Ax + By = C\)
  • the slope-intercept equation of a line \(y = m\cdot x + b\), as above
  • the point-slope equation of a line \(y = y_1 + m \cdot (x - x_1)\)

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:

  • Using different positional arguments for the variable and the parameters:
three_arguments(x, m, b) = m * x + b
three_arguments (generic function with 1 method)
  • Using the 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)
  • Using keyword arguments for the parameters:
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.

2.10.2 Operators

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
a, b = 0, 1
(0, 1)

then we can create the secant line for this data as:

m = (f(b) - f(a)) / (b-a)
x1, y1 = a, f(a)  # or b, f(b) it doesn't matter
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)
    m = (f(b) - f(a)) / (b -a)
    x1, y1 = a, f(a)
    x -> point_slope(x, m, x1, y1)
end
Secant (generic function with 1 method)

The same “secant_line” function would be generated through:

sl = Secant(f, a, b)
#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)

Note: “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
a, b = 0, 1
sl = secant(f, a, b)
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:

b = sl(0)
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:

a, b = 2, 3
x0, y0 = a, sl(a)
x1, y1 = b, sl(b)
m = (y1 - y0) / (x1 - x0)
1.0

Or more directly, the two intermediate lines can be skipped:

m = (sl(b) - sl(a)) / (b - a)
1.0

Question

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
c = 1
tl = tangent(f, c)
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:

  • the \(y\)-intercept:

  • the slope:

  • the \(x\)-intercept:

Partial function application

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.

2.11 Practice

Question: transformations

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

Select an item

Question: transformations

Which of these definitions will lengthen the period of a periodic function \(f\) by a factor of \(c\), with a default of \(1\)?

Select an item

Question: wavelet transform

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


Question

Let \(g\) be defined by:

function g(x; theta=pi/4)
     a = 200*cos(theta)
     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)?


Question

What anonymous function will compute \(\sin(x^2)\)?

Select an item

Question

What anonymous function of \(x\) will return the polynomial \(x^2 - 2x\):

Select an item

Question

What does this function do?

function mystery(f)
   x -> -f(x)
end
mystery (generic function with 1 method)
Select an item

Question

What does this operator do?

function trim(f, c)
  x -> abs(f(x)) <= c ? f(x) : NaN
end
trim (generic function with 1 method)
Select an item

2.12 Additional details

This section presents some additional details on writing functions in Julia that are here for informational purposes only.

2.12.1 Return values, tuples

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:

a, msg = f(-1)          # alternatively: (a, b) = f(-1)
(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.

2.12.2 Specializing functions by argument type

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.

2.12.3 Expressions as functions

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:

  • expressions are immediately evaluated
  • functions have their evaluation delayed until they are called

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:

x = 2
p = 100 + 20x - 16x^2
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
p = 100 + 20x - 16x^2
(100 + (20 * x)) - (16 * (x ^ 2))

This symbolic expression can be added and multiplied, as is typically done by hand:

q = (1 + 2p)*p^2
(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:

eqn = sin(x^2) ~ x/2

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
eqn1 = sin(x^2) ~ x/p
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 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

  1. 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.↩︎

  2. 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.↩︎

  3. Later, when SymPy is used for symbolic computations, some basic simplification happens automatically.↩︎

  4. 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.↩︎

  5. 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.↩︎