What a function is

A mathematical function is a rule that takes an input and gives back exactly one output. Think of a vending machine: press B4 and you always get the same snack, never two different ones. Different buttons may give the same snack, but one button never gives two.

The usual notation is f(x) = 2x + 3, read "f of x equals two x plus three". Here f is the name of the function, x the input and 2x + 3 the rule. To evaluate it, replace every x by the input: f(5) = 2 \cdot 5 + 3 = 13. Two more notations you will meet: f : \mathbb{R} \to \mathbb{R} says that f takes real numbers to real numbers, and x \mapsto 2x + 3 ("x maps to 2x + 3") describes the rule without naming the function.

Some examples from Kestrel Energy's systems:

  • Meter ID → customer is a function: every meter belongs to exactly one customer.
  • Customer → meter ID is not a function: a customer with an electricity and a gas meter has two.
  • kWh used → monthly bill is a function: the same usage always gives the same bill.

A function in a programming language is a mathematical function only when it is pure: it returns the same result for the same arguments and changes nothing else (see side effects and pure functions). How to write functions in Python is on Functions.

Domain and range

The domain of a function is the set of inputs it accepts. The range is the set of outputs it actually produces. When no domain is given, take every real number for which the rule makes sense. Three things commonly rule numbers out:

  • Division by zero: f(x) = 1/x has domain "every real number except 0".
  • Square roots of negative numbers: f(x) = \sqrt{x} has domain [0, \infty), and its range is [0, \infty) too.
  • Logarithms of zero or negative numbers: f(x) = \log_2 x has domain (0, \infty), while its range is every real number.

In code, a value outside the domain is an error. Python reports it as a "math domain error":

import math

for x in (16, 0, -4):
    for name, fn in (("sqrt", math.sqrt), ("log2", math.log2)):
        try:
            result = fn(x)
        except ValueError as error:
            result = f"ValueError: {error}"
        print(f"{name}({x}) = {result}")
sqrt(16) = 4.0
log2(16) = 4.0
sqrt(0) = 0.0
log2(0) = ValueError: math domain error
sqrt(-4) = ValueError: math domain error
log2(-4) = ValueError: math domain error

Reading a graph

The graph of a function is the set of all points (x, f(x)): for every input on the horizontal x-axis, the height of the curve is the output. Reading a graph means answering questions such as "what is the output at x = 3?" (go up from 3 to the curve, then across to the y-axis), "where is the output 0?" (where the curve crosses the x-axis) and "where does it rise or fall?".

Because a function has exactly one output per input, a vertical line crosses its graph at most once. A circle fails this vertical line test: it is a curve, but not the graph of a function.

Linear functions

A linear function has the form

f(x) = mx + c

and its graph is a straight line. The number c is the intercept, the output at x = 0, where the line crosses the y-axis. The number m is the slope: how much the output changes when the input goes up by 1. A positive slope rises from left to right, a negative slope falls, and slope 0 is a horizontal line. Between two points on the line, the slope is m = \Delta y / \Delta x, "the change in y divided by the change in x".

The defining property: equal steps in x always give equal steps in y. Anything with a constant rate is linear: a meter that registers 11 kWh every day, a cost of €0.27 per kWh, a fixed download speed.

Worked example: a meter trend line

A smart meter reads 1,200 kWh on day 0 and 1,530 kWh on day 30. Assume the usage is steady. What will it read on day 45?

  1. Slope: m = (1530 - 1200)/(30 - 0) = 330/30 = 11 kWh per day.
  2. Intercept: the reading at day 0 is 1,200, so c = 1200.
  3. The function: f(x) = 11x + 1200.
  4. Forecast: f(45) = 11 \cdot 45 + 1200 = 495 + 1200 = 1695 kWh.
# Meter readings: day 0 -> 1200 kWh, day 30 -> 1530 kWh
(x1, y1), (x2, y2) = (0, 1200), (30, 1530)
slope = (y2 - y1) / (x2 - x1)
intercept = y1 - slope * x1
print(f"y = {slope}x + {intercept}")
print(f"Forecast for day 45: {slope * 45 + intercept} kWh")
y = 11.0x + 1200.0
Forecast for day 45: 1695.0 kWh

With many noisy readings instead of two exact ones, you look for the line that fits best: that is linear regression, the simplest machine-learning model (see Linear regression).

Quadratic and polynomial functions

A quadratic function f(x) = ax^2 + bx + c (with a \ne 0) has a U-shaped graph called a parabola. If a \gt 0 it opens upward and has a lowest point; if a \lt 0 it opens downward and has a highest point. That turning point is the vertex, at

x = -\frac{b}{2a}

For f(x) = x^2 - 4x + 5: the vertex is at x = -(-4)/(2 \cdot 1) = 2, and f(2) = 4 - 8 + 5 = 1. So the lowest point is (2, 1), and the parabola is symmetric around the vertical line x = 2:

# f(x) = x^2 - 4x + 5: a parabola that opens upward (a = 1 > 0)
def f(x):
    return x * x - 4 * x + 5

a, b = 1, -4
vertex_x = -b / (2 * a)
print("vertex:", (vertex_x, f(vertex_x)))
print([f(x) for x in range(0, 5)])
vertex: (2.0, 1.0)
[5, 2, 1, 2, 5]

Where the parabola crosses the x-axis, f(x) = 0: that is a quadratic equation, solved on Equations and Inequalities. This parabola has no crossing, because its lowest point is above the axis.

A polynomial function adds up powers of x with constant coefficients, such as x^3 - 4x. The highest power is the degree: degree 1 is linear, degree 2 quadratic, degree 3 cubic. For large x the highest power dominates everything else. That is the idea behind Big O notation: a program doing n^2 + 100n steps behaves like n^2 for large inputs (see common complexity classes).

Other basic functions

A handful of function families come back again and again. Knowing their shapes lets you sketch a graph, or recognize one in a monitoring dashboard, without plotting points.

Function Formula Domain Range Shape
Linear mx + c all reals all reals (if m \ne 0) straight line
Quadratic x^2 all reals [0, \infty) parabola
Square root \sqrt{x} [0, \infty) [0, \infty) rises ever more slowly
Absolute value \lvert x \rvert all reals [0, \infty) V shape
Reciprocal 1/x x \ne 0 y \ne 0 two branches, approaching the axes
Exponential 2^x all reals (0, \infty) rises ever faster
Logarithmic \log_2 x (0, \infty) all reals rises ever more slowly
Sine \sin x all reals [-1, 1] wave, repeats every 2\pi

The absolute value \lvert x \rvert is the distance from 0, so it is never negative: \lvert -3 \rvert = 3. It measures how far off something is regardless of direction, such as the error between a forecast and a real meter reading.

The reciprocal family describes everything that is "a fixed amount divided by a rate". Copying 600 GB at b GB per hour takes t(b) = 600/b hours. Doubling the speed halves the time, and as b grows the time approaches 0 without ever reaching it. A line that a graph approaches but never reaches is an asymptote.

# Transfer time t(b) = 600 / b: 600 GB at b GB per hour
def transfer_hours(gb_per_hour):
    return 600 / gb_per_hour

for b in (50, 100, 200, 400):
    print(f"{b:>3} GB/h -> {transfer_hours(b):g} h")
 50 GB/h -> 12 h
100 GB/h -> 6 h
200 GB/h -> 3 h
400 GB/h -> 1.5 h

Sine and cosine are explained with the unit circle on Geometry and Trigonometry; as functions they describe anything that repeats, such as a daily load pattern or an alternating current.

Exponential and logarithmic functions

An exponential function has the variable in the exponent: f(x) = a^x with a fixed base a \gt 0, a \ne 1, or more generally f(x) = c \cdot a^x, where c is the starting value. Where a linear function adds the same amount at every step, an exponential function multiplies by the same factor. With a \gt 1 it grows, ever faster; with 0 \lt a \lt 1 it shrinks toward 0 (decay), as in a half-life.

The difference with a polynomial is dramatic. Compare \log_2 n, n^2 and 2^n:

import math

print(f"{'n':>6}{'log2 n':>8}{'n^2':>12}{'2^n':>22}")
for n in (1, 10, 20, 40, 64):
    print(f"{n:>6}{math.log2(n):>8.2f}{n * n:>12}{2 ** n:>22}")
     n  log2 n         n^2                   2^n
     1    0.00           1                     2
    10    3.32         100                  1024
    20    4.32         400               1048576
    40    5.32        1600         1099511627776
    64    6.00        4096  18446744073709551616

That table is why an algorithm whose running time grows like 2^n is hopeless for large inputs, why each extra bit of a key doubles the work of guessing it, and why a binary search that takes \log_2 n steps stays fast on a billion records. Retry timers that double after each failure (1, 2, 4, 8 … seconds) are exponential too; they are covered on Sequences, Sums and Growth Rates.

The logarithmic function \log_2 x answers the opposite question: "2 to which power gives x?". It undoes 2^x: it is its inverse function (see below), and its graph is the graph of 2^x mirrored in the diagonal line y = x. The rules for calculating with logarithms are on Powers, Roots and Logarithms.

A grid from minus 3 to 6 on both axes. The curve y = 2 to the power x passes through (0, 1), (1, 2) and (2, 4) and climbs ever faster. The curve y = log base 2 of x passes through (1, 0), (2, 1) and (4, 2) and climbs ever slower. The two curves are mirror images in the diagonal line y = x. x y −3 −2 −1 1 2 3 4 5 6 −3 −2 −1 1 2 3 4 5 6 0 y = 2^x y = log₂ x y = x (2, 4) (4, 2)
y = 2^x (accent) and y = \log_2 x are mirror images in the line y = x: the point (2, 4) on one matches (4, 2) on the other.

Shifting, stretching and mirroring

Once you know the shape of y = f(x), small changes to the formula move or reshape that graph in predictable ways. Here h, k and a are positive numbers.

Formula Effect on the graph
f(x) + k shift up by k
f(x) - k shift down by k
f(x - h) shift right by h
f(x + h) shift left by h
a \cdot f(x) stretch vertically by factor a (squeeze if 0 \lt a \lt 1)
-f(x) mirror in the x-axis (upside down)
f(-x) mirror in the y-axis (left and right swap)

Why does f(x - 2) move the graph to the right? Because the new function reaches each value 2 steps later. The vertex of y = x^2 is at input 0; the new function has to be fed x = 2 before the input of f becomes 0. Worked example: start with f(x) = x^2, vertex (0, 0).

  1. Replace x by x - 2: (x - 2)^2 is the same parabola, 2 to the right, vertex (2, 0).
  2. Add 1: (x - 2)^2 + 1 moves it up 1, vertex (2, 1).
  3. Expanded, that is x^2 - 4x + 4 + 1 = x^2 - 4x + 5: exactly the parabola from the vertex example above.
Two parabolas on a grid. The parabola y = x squared has its lowest point at (0, 0). The parabola y = (x minus 2) squared plus 1 has exactly the same shape, moved 2 to the right and 1 up, with its lowest point at (2, 1). x y −4 −3 −2 −1 1 2 3 4 5 6 −1 1 2 3 4 5 6 7 8 9 0 y = x² y = (x − 2)² + 1 (2, 1)
y = (x - 2)^2 + 1 (accent) is y = x^2 moved 2 to the right and 1 up; the vertex moves from (0, 0) to (2, 1).
def f(x):
    return x * x

def shifted(x):          # f(x - 2) + 1: right 2, up 1
    return f(x - 2) + 1

for x in (0, 1, 2, 3, 4):
    print(x, f(x), shifted(x))
0 0 5
1 1 2
2 4 1
3 9 2
4 16 5

Composing functions

Function composition feeds the output of one function into the next. The composition f \circ g, read "f after g", is

(f \circ g)(x) = f(g(x))

Work from the inside out: first apply g, then f to the result. The order matters. With g(x) = x + 3 and f(x) = 2x:

  • f(g(5)) = f(8) = 16: first add 3, then double;
  • g(f(5)) = g(10) = 13: first double, then add 3.
def compose(f, g):
    return lambda x: f(g(x))

double = lambda x: 2 * x
plus_three = lambda x: x + 3
print(compose(double, plus_three)(5))   # double(plus_three(5))
print(compose(plus_three, double)(5))   # plus_three(double(5))
16
13

Composition is how you chain unit conversions without mistakes. To find how long a file takes on a 100 Mbit/s link, compose "bytes to bits" (g(x) = 8x) with "bits to seconds" (f(b) = b / 10^8):

def to_bits(size_bytes):            # g: bytes -> bits
    return 8 * size_bytes

def seconds_at_100_mbit(bits):      # f: bits -> seconds on a 100 Mbit/s link
    return bits / 100_000_000

def transfer_seconds(size_bytes):   # f(g(x))
    return seconds_at_100_mbit(to_bits(size_bytes))

print(transfer_seconds(250_000_000))   # a 250 MB file
20.0

A shell pipeline such as sort | uniq -c and a chain of data transformations in a program are compositions too: each step's output is the next step's input. The domain of f \circ g is every x in the domain of g whose output g(x) lies in the domain of f.

Inverse functions

The inverse function f^{-1} undoes f: if f takes x to y, then f^{-1} takes y back to x. So f^{-1}(f(x)) = x for every x in the domain.

Worked example: Fahrenheit from Celsius is F = \tfrac{9}{5} C + 32. To find the inverse, solve for C:

  1. Subtract 32 from both sides: F - 32 = \tfrac{9}{5} C.
  2. Multiply both sides by \tfrac{5}{9}: C = \tfrac{5}{9}(F - 32).
  3. Check with the boiling point: 100 °C gives 9/5 × 100 + 32 = 212 °F, and (212 − 32) × 5/9 = 100. Correct.
def c_to_f(c):
    return 9 / 5 * c + 32

def f_to_c(f):
    return (f - 32) * 5 / 9

print(c_to_f(100), f_to_c(212))
print(f_to_c(c_to_f(37.5)))
212.0 100.0
37.5

Only functions in which no two inputs share an output (called one-to-one) have an inverse: otherwise you would not know which input to go back to. x^2 is not one-to-one, because 3 and −3 both give 9. Restrict its domain to [0, \infty) and the inverse is \sqrt{x}. Likewise \log_2 is the inverse of 2^x, which is why its graph is the mirror image in y = x (see the figure above): swapping input and output swaps the x and y coordinates.

import math

print(math.log2(2 ** 10), 2 ** math.log2(1024))
print(math.sqrt(9 ** 2), math.sqrt((-9) ** 2))   # sqrt undoes squaring only for x >= 0
10.0 1024.0
9.0 9.0

In IT, inverses are everywhere: encoding and decoding, compressing and decompressing, encrypting and decrypting. A cryptographic hash function is deliberately built so that no practical inverse exists (see Cryptography Basics).

Where functions and graphs show up in IT

  • Monitoring: every dashboard graph is a function of time. Recognizing a straight line (steady growth), an exponential curve (runaway growth, a retry storm) or a level curve (saturation) tells you what is happening before you read the numbers.
  • Capacity planning: a linear trend line forecasts disk usage; an exponential one forecasts data that grows by a percentage each year.
  • Algorithms: running time as a function of input size, compared by growth class.
  • Code: pure functions, composition in pipelines, and inverse pairs such as encode and decode that must round-trip exactly.
  • Machine learning: a trained model is a function from inputs to predictions, fitted to data (see Derivatives and Rates of Change for how fitting uses slopes).