Functions in Julia

We see in this project how to easily create functions in julia. In the following project we learn how to graph functions.

For basic things creating a new function and plotting it is as familiar as this:

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 (in particular, trying this at home won't actually draw the plot unless you've loaded one of the graphing packages like Winston or Gadfly), 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 type of argument and the number of arguments is 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.

One of the most basic families of functions are the polynomial functions, which include as special cases the very familiar constant functions, linear functions and quadratic functions.

Let's look at the familiar linear function to convert Fahrenheit into Celsius:

\[ f(x) = 5/9 \cdot (x - 32). \]

That is we subtract \(32\) then multiply by \(5/9\)

We can easily turn this into a function, simply by replacing the mathematical notation \(\cdot\) with *:

f(x) = (5/9) * (x - 32)

This defines a function object f. To evaluate f for a given value of \(x\) we simply use the familiar mathematical notation. Here we see what Celsius is for "normal" body temperature:

f(98.6)
## 37.0

And the Celsius equivalent to the standard boiling point:

f(212)
## 100.0

Calling a function Evaluating a function for a given value is also known in programming languages as "calling" f. When we work with functions in julia, the distinction between the function object and calling the function is important to keep straight -- though not too hard to do. In mathematical notation, it is sort of like the difference between writing \(f\) -- as opposed to \(f(x)\) -- to describe the function, the latter implying there is some \(x\) to be evaluated, so we think of \(f(x)\) as a \(y\)-value and \(f\) as a function.

Numeric literals

Julia provides the familiar notation above for simple functions. In fact, for polynomials with numeric coefficients, one can even skip using * for multiplication:

g(x) = 2x^2 - 3x + 2
g(2)
## 4

The convention is that when variables are immediately preceded by a numeric literal (and by immediately it is meant no spaces are in between) julia will assume multiplication is intended. (This can bypass the usual order of operations, e.g. 5/9(x-32) is not 5/9*(x-32).)

This can also used when simple parentheses are involved:

k(x) = 2(x - 1)^2 + 3(x+1)

But won't work with expressions such as (x+1)(x-1), as julia will then think the first parenthetical pair is a function and try to call it with a value of x-1.

Equations versus functions

Mathematically we tend to blur the distinction between the equation

\[ y = 5/9 \cdot (x - 32) \]

and the function

\[ f(x) = 5/9 \cdot (x - 32) \]

As the graph of the function \(f(x)\) is the same as the graph of the equation \(y=f(x)\). There is a distinction in julia as

x = -40
## -40
y = 5/9 * (x - 32)
## -40.0

will evaluate the righthand side with the value of x bound at the time of assignment to y, whereas

f(x) = 5/9 * (x - 32)
f(72)               ## room temperature
## 22.22222222222222

will create a function which is called with a value of x at a later time. So the value of x defined when the function is created is not important here (as x is passed in as an argument).


Rational functions

Rational functions are simply ratios of polynomial functions. Defining them is straightforward. E.g.,

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

becomes

f(x) = (x^2 - 2)/(x-2)

Here, as with your calculator, it is important to remember to use parentheses around the top and bottom expressions, as otherwise the division operator will have higher precedence than the subtraction operators.

Other powers

While polynomials use non-negative integer coefficients for the powers, functions with other powers are, of course, possible. The ^ operator is used in general for powers, and the sqrt and cbrt offer an alternative.

So, this

\[ f(x) = \sqrt{x} + x^{1/3} + x^{1/4} \]

can become:

f(x) = sqrt(x) + cbrt(x) + x^(1/4)

For the fractional power, this shows the required parentheses around (1/4) to ensure that division occurs before the higher-precedence power operation (compare 2^1/4 to 2^(1/4)).

Practice

Question

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

Is \(f(4)\) positive?

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

Question

Which of these functions will compute

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

Mathematical functions

Of course julia has readily available all the usual built-in functions found on a scientific calculator, and many more. See section 1.4.3 in julia's manual (Mathematical Functions) and http://docs.julialang.org/en/latest/stdlib/base/#mathematical-functions of the official julia documentation for lists. Here we show how to translate some basic math functions into julia functions:

trigonometric functions

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

becomes

f(x) = cos(x) - sin(x)^2

The mathematical notation \(\sin^2(x)\) is really just a shorthand for the more cumbersome \((\sin(x))^2\), which is what julia must do, as notation such as sin^2(x) to julia is not what is wanted here.

If you want to work in degrees you can do so with the degree-based trigonometric functions:

fd(x) = cosd(x) - sind(x)^2

inverse trigonometric functions

A mathematical definition like

\[ f(x) = 2\tan^{-1}(\frac{\sqrt{1 - x^2}}{1 + x}) \]

becomes

f(x) = 2atan( sqrt(1-x^2) / (1 + x) )

This particular function is just an alternative expression for \(\cos^{-1}\) 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.

Exponential function

The math function

\[ f(x) = e^{-\frac{1}{2}x^2} \]

Can be expressed as

f(x) = e^(-(1/2)*x^2)

The value of \(e\) is built-in to julia, but can be inadvertently redefined. As such, it would be safer practice to use the exp function, as in:

f(x) = exp(-(1/2)*x^2)

There isn't much difference in use, but don't try to do exp^(-(1/2)*x^2)!

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)

Where as, the base 10 log:

\[ f(x) = \log_{10}(1 + x) \]

can be done through:

f(x) = log(10, 1 + x)

where the first argument expresses the base. (Of course log(1+x)/log(10) works too!) For convenience, julia also gives the functions log10 and log2 for base 10 and 2 respectively.

Practice

Question

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

Question

Which of these functions will compute

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

Question

Which of these functions will compute

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


More complicated 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

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/k}{200 \cos\theta} x - \frac{32}{k} \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)\) and \(32/k\) in the defintion of \(g(x)\), so we give them the intermediate names of a and b:

function g(x)
     theta = pi/4
     k = 1/2
     a = 200*cos(theta)
     b = 32/k
     tan(theta)*x + (b/a)*x - b*log(a/(a-x))
end

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.

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.

A cell phone plan might cost one $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

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. This can be important when using the function later on. In general it is a good programming practice to have functions return only one type of variable. (This is why sqrt(-1) is an error and not the imaginary number im. If you wanted complex outputs, you need to specify complex inputs, e.g, sqrt(-1 + 0im).)

To see why we call the above function a "hockey stick" function we show a quick plot:

plot(cell_phone, 0, 1000)

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. So this example could have been a one-liner:

cell_phone(x) = x < 500 ? 30.0 : 30.0 + 0.25*(x - 500)

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.

Practice

Question

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

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?

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?

Multiple arguments, "functions with parameters

In julia it is very common to have more than one argument for a function. For example, we saw the log function can use a second argument to express the base.

Parameters and function arguments are easily confused. We will use keywords for our parameters which allows us to specify a default value. Using a keyword is as simple as specifying the argument with key=value.

A simple case is a function which computes the \(y\) value on a line \(y=mx+b\) from a given \(x\) value. Here \(m\) and \(b\) are parameters. We will give them a default of \(1\) and \(0\):

mxplusb(x; m=1, b=0) = m*x + b

The syntax is to use a semicolon to separate regular arguments from those with keywords. This is not needed when calling the function:

mxplusb(2)              ## 1*2 + 0, using defaults
## 2
mxplusb(2, m=2, b=3)                ## 2*2 + 3, using passed in values
## 7

For a more complicated example, we revisit this function

\[ g(x) = \tan(\theta) x + \frac{32/k}{200 \cos\theta} x - \frac{32}{k} \log\left(\frac{200 \cos\theta}{200\cos\theta - x}\right). \]

Rather than define the values of \(k\) and \(\theta\) outside the function, we can pass in values for these parameters with this definition

function g(x; theta=pi/4, k=1/2)
     a = 200*cos(theta)
     b = 32/k
     tan(theta)*x + (b/a)*x - b*log(a/(a-x))
end

The defaults mean g(50, theta=pi/4, k=1/2) would be the same as

g(50)
## 44.70647823072916

The parameters make it easy to look at other types of problems. For example, if the angle were less, would the value of \(f\) be smaller or larger?

g(50, theta=pi/8)       ## smaller in this case.
## 17.835012377340664

Passing in parameters has the big advantage of explicitly showing how julia will find variables used within a function, as otherwise you need to have an understanding of the scoping rules in place. (Scoping rules determine where variables that are not passed in as arguments are found when referred to within a function.)

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

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

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 )

If \(f(x) = \sin(x)/x\) and \(a=2\) and \(b=1\) compute \(g(0, a=2, b=1)\).

Question what angle?

Let \(g\) be defined by:

function g(x; theta=pi/4, k=1/2)
     a = 200*cos(theta)
     b = 32/k
     tan(theta)*x + (b/a)*x - b*log(a/(a-x))
end

For x in 20, 25, 30, 35, 40, 45 degrees, what value will maximize g(125, theta=x*pi/180)?

Anonymous functions

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

This expression creates a function object, but since we didn't bind it to a variable it will be immediately forgotten. Such functions without a name are known as anonymous functions.

Operators

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 the integral which will come later.

Julia allows one to use function objects as arguments to other functions and to return functions from within a function call. In computer science language, functions are first class objects. We can exploit this when useful.

Let's look at a concrete example. In a precalculus course, we learn about transformations of functions where we relate the function \(g(x) = d + af(c(x-b))\) to the function \(f(x)\) in terms of the parameters \(a\), \(b\), \(c\), and \(d\). Here we focus on \(d\) and \(b\) which shift up and right.

Let's make a function that takes \(f\), some specifications and returns \(g\):

function transform_f(f; shift_up=0, shift_right=0)
  x -> shift_up + f(x - shift_right)
end

This basically is \(g(x) = d + f(x-b)\), but with longer names. This function takes as its main argument a function (f) and returns an anonymous function (look for the arrow in the function body).

Here we look at \(1/(x-2)\) evaluated at \(3\):

f(x) = 1/x
tf = transform_f(f, shift_right=2) ## returns a function
tf(3)
## 1.0

Here is another example, which is used to replace large or small values with NaN. This can be useful when plotting.

function trim_large(f; val=10)
  x -> abs(f(x)) > val ? NaN : f(x)
end

The ternary operator checks if the function is large in absolute value (bigger than val) and if so, use NaN for a value, otherwise it returns \(f(x)\). This is used within an anonymous function, so that trim_large returns a function object.

To see it in use we have:

f(x) = 1/x
tf = trim_large(f, val=10)
tf(1)               ## 1 is fine, leave as is
## 1.0
tf(.001)            ## 1000 is too large
## NaN

Practice

Question

What anonymous function of \(x\) will always return a value of \(\pi\)?

Question

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

Question

What does this function do?

function mystery(f)
   x -> -f(x)
end

Details

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

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 negative numbers")

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 negative 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)
## (NaN,"You can't use negative numbers")

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

The extra bit ::Integer specializes n and k so that this function only is called with both n and k are of this type.

Then we can call our binom function as:

binom(10,4)
## 210.0

But not as follows, as \(\pi\) is not an integer:

binom(10, pi)
## "MethodError(binom,(10,π = 3.1415926535897...))"

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

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.

Generic and anonymous functions

In julia there are really two types of functions: generic functions and anonymous functions. A generic function is created when we use this form to create a function:

g1(x) = sin(3x^2 - 2x^3)

or when we use this longer form:

function longer_form(x)
     cos(3x^2 - 2x^3)
end

Generic functions allow for polymorphic behavior, a computer science term for allowing different function definitions to be called based on the number of and type of the arguments passed in. This allows us to have two or more different functions with the same name. Some commonly used functions in the base language of julia have many different defintions. For example, sqrt has at least 15 and show (to display an object) has over 50. (cf. methods(show) to count.)

An anonymous function is made when we do something like

g2 = x -> sin(3x^2 - 2x^3)

For the most part the end user can't tell the difference:

(g1(1), g2(1))
## (0.8414709848078965,0.8414709848078965)

But, there are times when there can be a conflict, in particular when you try to redefine a generic function as an anonymous function:

g1 = x -> sin(2x^2)
## "ErrorException(\"invalid redefinition of constant g1\")"

Or vice versa. Basically, julia has assigned a certain function type to that name and you can't change that type though you can change the function's definition. (This is one exception to the "dynamic" aspect of julia.)

We use the generic function approach in these notes to define our named functions, as the basic notation so closely mirrors the standard math notation. Though in the examples we make use of anonymous functions when we don't want to store the value, and we return anonymous functions when our functions return other functions, such as the composition example.