The features of
implementations of callable units evolved over time and varies by context. This section describes features of the various common implementations.
General characteristics Most modern programming languages provide features to define and call functions, including
syntax for accessing such features, including: • Delimit the implementation of a function from the rest of the program • Assign an
identifier, name, to a function • Define
formal parameters with a name and
data type for each • Assign a
data type to the return value, if any • Specify a return value in the function body • Call a function • Provide
actual parameters that correspond to a called function's formal parameters •
Return control to the caller at the point of call • Consume the return value in the caller • Dispose of the values returned by a call • Provide a private
naming scope for variables • Identify variables outside the function that are accessible within it • Propagate an
exceptional condition out of a function and to handle it in the calling context • Package functions into a container such as
module,
library,
object, or
class Naming Some languages, such as
Pascal,
Fortran,
Ada and many
dialects of
BASIC, use a different name for a callable unit that returns a value (
function or
subprogram) vs. one that does not (
subroutine or
procedure). Other languages, such as
C,
C++,
C# and
Lisp, use only one name for a callable unit,
function. The C-family languages use the keyword void to indicate no return value.
Call syntax If declared to return a value, a call can be embedded in an
expression in order to consume the return value. For example, a square root callable unit might be called like y = sqrt(x). A callable unit that does not return a value is called as a stand-alone
statement like print("hello"). This syntax can also be used for a callable unit that returns a value, but the return value will be ignored. Some older languages require a keyword for calls that do not consume a return value, like CALL print("hello").
Parameters Most implementations, especially in modern languages, support
parameters which the callable declares as
formal parameters. A caller passes
actual parameters, a.k.a.
arguments, to match. Different programming languages provide different conventions for passing arguments.
Return value In some languages, such as BASIC, a callable has different syntax (i.e. keyword) for a callable that returns a value vs. one that does not. In other languages, the syntax is the same regardless. In some of these languages an extra keyword is used to declare no return value, such as void in C, C++ and C#. In some languages, such as Python, the difference is whether the body contains a return statement with a value, and a particular callable may return with or without a value based on control flow.
Side effects In many contexts, a callable may have
side effect behavior such as modifying passed or global data, reading from or writing to a
peripheral device, accessing a
file, halting the program or the machine, or temporarily pausing program execution. Side effects are considered undesirable by
Robert C. Martin, who is known for promoting design principles. Martin argues that side effects can result in
temporal coupling or order dependencies. In strictly
functional programming languages such as
Haskell, a function can have no
side effects, which means it cannot change the state of the program. Functions always return the same result for the same input. Such languages typically only support functions that return a value, since there is no value in a function that has neither return value nor side effect.
Local variables Most contexts support
local variables memory owned by a callable to hold intermediate values. These variables are typically stored in the call's
activation record on the
call stack along with other information such as the
return address.
Nested call recursion If supported by the language, a callable may call itself, causing its execution to suspend while another
nested execution of the same callable executes.
Recursion is a useful means to simplify some complex algorithms and break down complex problems. Recursive languages provide a new copy of local variables on each call. If the programmer desires the recursive callable to use the same variables instead of using locals, they typically declare them in a shared context such static or global. Languages going back to
ALGOL,
PL/I and
C and modern languages, almost invariably use a call stack, usually supported by the instruction sets to provide an activation record for each call. That way, a nested call can modify its local variables without affecting any of the suspended call's variables. Recursion allows direct implementation of functionality defined by
mathematical induction and recursive
divide and conquer algorithms. Here is an example of a recursive function in C to find
Fibonacci numbers: int fibonacci(unsigned int n) { if (n Early languages like
Fortran did not initially support recursion because only one set of variables and return address were allocated for each callable. Early computer instruction sets made storing return addresses and variables on a stack difficult. Machines with
index registers or
general-purpose registers, e.g.,
CDC 6000 series,
PDP-6,
GE 635,
System/360,
UNIVAC 1100 series, could use one of those registers as a
stack pointer.
Nested scope Some languages, e.g.,
Ada,
Pascal,
PL/I,
Python, support declaring and defining a function inside, e.g., a function body, such that the name of the inner is only visible within the body of the outer. A simple example in Pascal:function E(x: real): real; function F(y: real): real; begin F := x + y end; begin E := F(3) + F(4) end;The function F is nested within E. Note that E's parameter x is also visible in F (as F is a part of E) while both x and y are invisible outside E and F respectively.
Reentrancy If a callable can be executed properly even when another execution of the same callable is already in progress, that callable is said to be
reentrant. A reentrant callable is also useful in
multi-threaded situations since multiple threads can call the same callable without fear of interfering with each other. In the
IBM CICS transaction processing system,
quasi-reentrant was a slightly less restrictive, but similar, requirement for application programs that were shared by many threads.
Overloading Some languages support
overloading allow multiple callables with the same name in the same scope, but operating on different types of input. Consider the square root function applied to real number, complex number and matrix input. The algorithm for each type of input is different, and the return value may have a different type. By writing three separate callables with the same name. i.e.
sqrt, the resulting code may be easier to write and to maintain since each one has a name that is relatively easy to understand and to remember instead of giving longer and more complicated names like
sqrt_real,
sqrt_complex,
qrt_matrix. Overloading is supported in many languages that support
strong typing. Often the compiler selects the overload to call based on the type of the input arguments or it fails if the input arguments do not select an overload. Older and weakly-typed languages generally do not support overloading. Here is an example of overloading in
C++, two functions area that accept different types: // returns the area of a rectangle defined by height and width double area(double height, double width) { return h * w; } // returns the area of a circle defined by radius double area(double radius) { return radius * radius * std::numbers::pi; } int main() { double rectangleArea = area(3, 4); double circleArea = area(5); } PL/I has the GENERIC attribute to define a generic name for a set of entry references called with different types of arguments. Example: DECLARE gen_name GENERIC( name WHEN(FIXED BINARY), flame WHEN(FLOAT), pathname OTHERWISE); Multiple argument definitions may be specified for each entry. A call to "gen_name" will result in a call to "name" when the argument is FIXED BINARY, "flame" when FLOAT", etc. If the argument matches none of the choices "pathname" will be called.
Closure A
closure is a callable plus values of some of its variables captured from the environment in which it was created. Closures were a notable feature of the Lisp programming language, introduced by
John McCarthy. Depending on the implementation, closures can serve as a mechanism for side-effects.
Exception reporting Besides its
happy path behavior, a callable may need to inform the caller about an
exceptional condition that occurred during its execution. Most modern languages support exceptions which allows for exceptional control flow that pops the call stack until an exception handler is found to handle the condition. Languages that do not support exceptions can use the return value to indicate success or failure of a call. Another approach is to use a well-known location like a global variable for success indication. A callable writes the value and the caller reads it after a call. In the
IBM System/360, where return code was expected from a subroutine, the return value was often designed to be a multiple of 4—so that it could be used as a direct
branch table index into a branch table often located immediately after the call instruction to avoid extra conditional tests, further improving efficiency. In the
System/360 assembly language, one would write, for example: BAL 14, SUBRTN01 go to a subroutine, storing return address in R14 B TABLE(15) use returned value in reg 15 to index the branch table, • branching to the appropriate branch instr. TABLE B OK return code =00 GOOD } B BAD return code =04 Invalid input } Branch table B ERROR return code =08 Unexpected condition }
Call overhead A call has runtime
overhead, which may include but is not limited to: • Allocating and reclaiming call stack storage • Saving and restoring processor registers • Copying input variables • Copying values after the call into the caller's context • Automatic testing of the return code • Handling of exceptions • Dispatching such as for a virtual method in an object-oriented language Various techniques are employed to minimize the runtime cost of calls.
Compiler optimization Some optimizations for minimizing call overhead may seem straight forward, but cannot be used if the callable has side effects. For example, in the expression (f(x)-1)/(f(x)+1), the function f cannot be called only once with its value used two times since the two calls may return different results. Moreover, in the few languages which define the order of evaluation of the division operator's operands, the value of x must be fetched again before the second call, since the first call may have changed it. Determining whether a callable has a side effect is difficult indeed,
undecidable by virtue of
Rice's theorem. So, while this optimization is safe in a purely functional programming language, a compiler for a language not limited to functional typically assumes the worst case, that every callable may have side effects.
Inlining Inlining eliminates calls for particular callables. The compiler replaces each call with the compiled code of the callable. Not only does this avoid the call overhead, but it also allows the
compiler to
optimize code of the caller more effectively by taking into account the context and arguments at that call. Inlining, however, usually increases the compiled code size, except when only called once or the body is very short, like one line.
Sharing Callables can be defined within a program, or separately in a
library that can be used by multiple programs.
Inter-operability A
compiler translates call and return statements into machine instructions according to a well-defined
calling convention. For code compiled by the same or a compatible compiler, functions can be compiled separately from the programs that call them. The instruction sequences corresponding to call and return statements are called the procedure's
prologue and epilogue.
Built-in functions A
built-in function, or
builtin function, or
intrinsic function, is a function for which the compiler generates code at
compile time or provides in a way other than for other functions. A built-in function does not need to be defined like other functions since it is
built in to the programming language.
compile time evaluated functions In some programming languages, for instance
C++,
Rust and
Zig, functions can be evaluated at
compile time; reducing the runtime overhead in the ways such as not using additional
stack frame, using less memory and baking the return values to the
binary code itself. == Programming ==