We already saw in this article that it was possible to store a REFERENCE to a function in a variable. Therefore, it was to be expected that we can also pass a function as a parameter to another function.
Passing references to functions as parameters to another function simply means that the “receiving” function can accept REFERENCES to other functions as arguments. (Of course, there is also the possibility of returning a function as return
).
This allows the receiving function to use the function it receives as a parameter. Whether by invoking it directly, storing it for later use, passing it to another function, or even doing nothing with it. Ultimately, it can do whatever it wants with it (as if it were any other parameter).
Why would someone want to pass a function to another? Well, because it allows for very interesting things. Mainly, it allows us to write more general and reusable functions, since they can work with different behaviors specified by the referenced function.
In short, as a mechanism, it is the milk foundation for creating powerful and reusable tools. It might take you some time to get used to them, but the effort is worth it. So let’s see its use with a couple of examples.
Use Cases for Function References
For example, imagine you have a function that iterates over the elements of a collection. Moreover, the way to iterate through it is complex, so you would like to reuse that code as much as possible.
Now you want to use your function to iterate through the collection and perform an action on the elements that meet a criterion. We can reuse your recorre_coleccion
function by also passing the criterio
function and the action
function to be performed.
function recorre_coleccion(criterio, action)
{
// do things
}
Another example, imagine that a function has to perform a very long and complicated process. Additionally, this process can go well 👍, or produce an error ❌.
You can create a function haz_algo_muy_complicado
that performs the process. You pass it two functions. One for success
that will execute if everything goes well, and one for error
if something goes wrong.
function haz_algo_muy_complicado(exito, error)
{
// do something very complicated
// with possible calls to error()
// and if everything goes well, success
exito();
}
What do these two examples have in common? That passing functions as parameters has allowed us to reuse the code of a very complicated function. We have added versatility, and we can use it in many more cases.
Examples of Functions as Parameters in Different Languages
Let’s see how the use of Higher-Order Functions and passing functions as parameters would be implemented in different programming languages.
In this example, we will create two functions Sumar
and Restar
, which receive two parameters a
and b
and perform the appropriate operation.
On the other hand, we will have a higher-order function called EjecutarOperacion
. This function receives two parameters a
and b
, and the operation to execute.
// Delegate to represent a function that takes two integers and returns an integer
delegate int Operacion(int a, int b);
// Function to add two numbers
int Sumar(int a, int b)
{
return a + b;
}
// Function to subtract two numbers
int Restar(int a, int b)
{
return a - b;
}
// Function to execute a mathematical operation
int EjecutarOperacion(int a, int b, Operacion operacion)
{
return operacion(a, b);
}
Console.WriteLine(EjecutarOperacion(5, 3, Sumar)); // Prints: 8
Console.WriteLine(EjecutarOperacion(5, 3, Restar)); // Prints: 2
// Function pointer type declaration that takes two integers and returns an integer
typedef int (*Operacion)(int, int);
// Function to add two numbers
int Sumar(int a, int b)
{
return a + b;
}
// Function to subtract two numbers
int Restar(int a, int b)
{
return a - b;
}
// Function to execute a mathematical operation
int EjecutarOperacion(int a, int b, Operacion operacion)
{
return operacion(a, b);
}
int main()
{
// Use function references to perform operations
std::cout << EjecutarOperacion(5, 3, Sumar) << std::endl; // Prints: 8
std::cout << EjecutarOperacion(5, 3, Restar) << std::endl; // Prints: 2
return 0;
}
// Function to add two numbers
function sumar(a, b) {
return a + b;
}
// Function to subtract two numbers
function restar(a, b) {
return a - b;
}
// Function to execute a mathematical operation
function ejecutarOperacion(a, b, operacion) {
return operacion(a, b);
}
// Use function references to perform operations
console.log(ejecutarOperacion(5, 3, sumar)); // Prints: 8
console.log(ejecutarOperacion(5, 3, restar)); // Prints: 2
# Function to multiply two numbers
def multiplicar(a, b):
return a * b
# Function to divide two numbers
def dividir(a, b):
return a / b
# Function to execute a mathematical operation
def ejecutar_operacion(a, b, operacion):
return operacion(a, b)
# Use function references to perform operations
print(ejecutar_operacion(5, 3, multiplicar)) # Prints: 15
print(ejecutar_operacion(5, 3, dividir)) # Prints: 1.6666666666666667
Higher-Order Functions
Functions that accept other functions as arguments or return functions as results are known as higher-order functions.
In other words, a Higher-Order Function is one that:
- Can receive one or more functions as arguments
- Can return a function as a result (with
return
).
It is a term that has become popular because it plays an important (especially theoretical) role in functional programming. And functional programming is “in vogue”.
// "super cool" function that receives another function
function FuncionDeOrdenSuperior(function otraFuncion)
{
}
// poor common function that only receives numbers
function FuncionNormal(int numero)
{
}
But don’t give it too much importance. In the end, they are just ordinary functions, like any others.
Simply that some of the data types they handle are Function References. (but there is a huge fuss about naming everything 😄).
Relation to Lambda Functions
Lambda functions (also known as anonymous functions) are a concise way to define inline functions without the need to assign them a name.
Lambda functions are especially useful when you need a function that you will only use once, especially if it is short. Here it doesn’t make sense to create a “normal” function, and lambda functions make your life easier.
So on one side we have a syntax for short functions, and on the other functions that accept other functions. You see where I’m going, right? These are concepts that fit very well together.
function ExecuteAction(action)
{
// do whatever
}
ExecuteAction(() => console.log("hello")); // look how convenient 😉
The combination of lambda functions and passing function references as parameters is a common and very usual practice in many modern programming languages.
Internal Functioning Advanced
The internal functioning of passing and returning functions from a function is very simple, if you have understood the concept of a REFERENCE to a function.
Recall that a reference to a function is simply a variable that contains the address of a function, instead of a data point, or the address of another variable.
Aside from that, for the receiving function, the management is exactly the same as with any other type of variable. It receives it, returns it, copies it, stores it… the function essentially doesn’t care.
// this function doesn't care if something is 10, "potato" or a function
function FuncionDeOrdenASaber(algo)
{
}
The difference will be in the use made of the variable. And, if the language is typed, in the checks it makes to ensure the types are correct.
function FuncionDeOrdenASaber(algo)
{
algo(); // here is the difference, when using the variable
}
// here is another difference, when passing the variable
FuncionDeOrdenASaber(() => console.log("hello"));
But in reality, there is no difference in the functioning of a Reference to a function compared to any other type of variable.