Recent from talks
Contribute something
Nothing was collected or created yet.
First-class function
View on WikipediaIn computer science, a programming language is said to have first-class functions if it treats functions as first-class citizens. This means the language supports passing functions as arguments to other functions, returning them as the values from other functions, and assigning them to variables or storing them in data structures.[1] Some programming language theorists require support for anonymous functions (function literals) as well.[2] In languages with first-class functions, the names of functions do not have any special status; they are treated like ordinary variables with a function type.[3] The term was coined by Christopher Strachey in the context of "functions as first-class citizens" in the mid-1960s.[4]
First-class functions are a necessity for the functional programming style, in which the use of higher-order functions is a standard practice. A simple example of a higher-ordered function is the map function, which takes, as its arguments, a function and a list, and returns the list formed by applying the function to each member of the list. For a language to support map, it must support passing a function as an argument.
There are certain implementation difficulties in passing functions as arguments or returning them as results, especially in the presence of non-local variables introduced in nested and anonymous functions. Historically, these were termed the funarg problems, the name coming from function argument.[5] In early imperative languages these problems were avoided by either not supporting functions as result types (e.g. ALGOL 60, Pascal) or omitting nested functions and thus non-local variables (e.g. C). The early functional language Lisp took the approach of dynamic scoping, where non-local variables refer to the closest definition of that variable at the point where the function is executed, instead of where it was defined. Proper support for lexically scoped first-class functions was introduced in Scheme and requires handling references to functions as closures instead of bare function pointers,[4] which in turn makes garbage collection a necessity.[citation needed]
Concepts
[edit]In this section, we compare how particular programming idioms are handled in a functional language with first-class functions (Haskell) compared to an imperative language where functions are second-class citizens (C).
Higher-order functions: passing functions as arguments
[edit]In languages where functions are first-class citizens, functions can be passed as arguments to other functions in the same way as other values (a function taking another function as argument is called a higher-order function). In the language Haskell:
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs
Languages where functions are not first-class often still allow one to write higher-order functions through the use of features such as function pointers or delegates. In the language C:
void map(int (*f)(int), int x[], size_t n) {
for (int i = 0; i < n; ++i) {
x[i] = f(x[i]);
}
}
There are a number of differences between the two approaches that are not directly related to the support of first-class functions. The Haskell sample operates on lists, while the C sample operates on arrays. Both are the most natural compound data structures in the respective languages and making the C sample operate on linked lists would have made it unnecessarily complex. This also accounts for the fact that the C function needs an additional parameter (giving the size of the array.) The C function updates the array in-place, returning no value, whereas in Haskell data structures are persistent (a new list is returned while the old is left intact.) The Haskell sample uses recursion to traverse the list, while the C sample uses iteration. Again, this is the most natural way to express this function in both languages, but the Haskell sample could easily have been expressed in terms of a fold and the C sample in terms of recursion. Finally, the Haskell function has a polymorphic type, as this is not supported by C we have fixed all type variables to the type constant int.
Anonymous and nested functions
[edit]In languages supporting anonymous functions, we can pass such a function as an argument to a higher-order function:
main = map (\x -> 3 * x + 1) [1, 2, 3, 4, 5]
In a language which does not support anonymous functions, we have to bind it to a name instead:
int f(int x) {
return 3 * x + 1;
}
int main() {
int list[] = {1, 2, 3, 4, 5};
map(f, list, 5);
}
Non-local variables and closures
[edit]Once we have anonymous or nested functions, it becomes natural for them to refer to variables outside of their body (called non-local variables):
main = let a = 3
b = 1
in map (\x -> a * x + b) [1, 2, 3, 4, 5]
If functions are represented with bare function pointers, we can not know anymore how the value that is outside of the function's body should be passed to it, and because of that a closure needs to be built manually. Therefore we can not speak of "first-class" functions here.
typedef struct {
int (*f)(int, int, int);
int a;
int b;
} Closure;
void map(Closure* closure, int x[], size_t n) {
for (int i = 0; i < n; ++i) {
x[i] = (closure->f)(closure->a, closure->b, x[i]);
}
}
int f(int a, int b, int x) {
return a * x + b;
}
void main() {
int l[] = {1, 2, 3, 4, 5};
int a = 3;
int b = 1;
Closure closure = {f, a, b};
map(&closure, l, 5);
}
Also note that the map is now specialized to functions referring to two ints outside of their environment. This can be set up more generally, but requires more boilerplate code. If f would have been a nested function we would still have run into the same problem and this is the reason they are not supported in C.[6]
Higher-order functions: returning functions as results
[edit]When returning a function, we are in fact returning its closure. In the C example any local variables captured by the closure will go out of scope once we return from the function that builds the closure. Forcing the closure at a later point will result in undefined behaviour, possibly corrupting the stack. This is known as the upwards funarg problem.
Assigning functions to variables
[edit]Assigning functions to variables and storing them inside (global) data structures potentially suffers from the same difficulties as returning functions.
f :: [[Integer] -> [Integer]]
f = let a = 3
b = 1
in [map (\x -> a * x + b), map (\x -> b * x + a)]
Equality of functions
[edit]As one can test most literals and values for equality, it is natural to ask whether a programming language can support testing functions for equality. On further inspection, this question appears more difficult and one has to distinguish between several types of function equality:[7]
- Extensional equality
- Two functions f and g are considered extensionally equal if they agree on their outputs for all inputs (∀x. f(x) = g(x)). Under this definition of equality, for example, any two implementations of a stable sorting algorithm, such as insertion sort and merge sort, would be considered equal. Deciding on extensional equality is undecidable in general and even for functions with finite domains often intractable. For this reason no programming language implements function equality as extensional equality.
- Intensional equality
- Under intensional equality, two functions f and g are considered equal if they have the same "internal structure". This kind of equality could be implemented in interpreted languages by comparing the source code of the function bodies (such as in Interpreted Lisp 1.5) or the object code in compiled languages. Intensional equality implies extensional equality (assuming the functions are deterministic and have no hidden inputs, such as the program counter or a mutable global variable.)
- Reference equality
- Given the impracticality of implementing extensional and intensional equality, most languages supporting testing functions for equality use reference equality. All functions or closures are assigned a unique identifier (usually the address of the function body or the closure) and equality is decided based on equality of the identifier. Two separately defined, but otherwise identical function definitions will be considered unequal. Referential equality implies intensional and extensional equality. Referential equality breaks referential transparency and is therefore not supported in pure languages, such as Haskell.
Type theory
[edit]In type theory, the type of functions accepting values of type A and returning values of type B may be written as A → B or BA. In the Curry–Howard correspondence, function types are related to logical implication; lambda abstraction corresponds to discharging hypothetical assumptions and function application corresponds to the modus ponens inference rule. Besides the usual case of programming functions, type theory also uses first-class functions to model associative arrays and similar data structures.
In category-theoretical accounts of programming, the availability of first-class functions corresponds to the closed category assumption. For instance, the simply typed lambda calculus corresponds to the internal language of Cartesian closed categories.
Language support
[edit]Functional programming languages, such as Erlang, Scheme, ML, Haskell, F#, and Scala, all have first-class functions. When Lisp, one of the earliest functional languages, was designed, not all aspects of first-class functions were then properly understood, resulting in functions being dynamically scoped. The later Scheme and Common Lisp dialects do have lexically scoped first-class functions.
Many scripting languages, including Perl, Python, PHP, Lua, Tcl/Tk, JavaScript and Io, have first-class functions.
For imperative languages, a distinction has to be made between Algol and its descendants such as Pascal, the traditional C family, and the modern garbage-collected variants. The Algol family has allowed nested functions and higher-order taking function as arguments, but not higher-order functions that return functions as results (except Algol 68, which allows this). The reason for this was that it was not known how to deal with non-local variables if a nested-function was returned as a result (and Algol 68 produces runtime errors in such cases).
The C family allowed both passing functions as arguments and returning them as results, but avoided any problems by not supporting nested functions. (The gcc compiler allows them as an extension.) As the usefulness of returning functions primarily lies in the ability to return nested functions that have captured non-local variables, instead of top-level functions, these languages are generally not considered to have first-class functions.
Modern imperative languages often support garbage-collection making the implementation of first-class functions feasible. First-class functions have often only been supported in later revisions of the language, including C# 2.0 and Apple's Blocks extension to C, C++, and Objective-C. C++11 has added support for anonymous functions and closures to the language, but because of the non-garbage collected nature of the language, special care has to be taken for non-local variables in functions to be returned as results (see below).
| Language | Higher-order functions | Nested functions | Non-local variables | Notes | ||||
|---|---|---|---|---|---|---|---|---|
| Arguments | Results | Named | Anonymous | Closures | Partial application | |||
| Algol family | ALGOL 60 | Yes | No | Yes | No | Downwards | No | Have function types. |
| ALGOL 68 | Yes | Yes[8] | Yes | Yes | Downwards[9] | No | ||
| Pascal | Yes | No | Yes | No | Downwards | No | ||
| Ada | Yes | No | Yes | No | Downwards | No | ||
| Oberon | Yes | Non-nested only | Yes | No | Downwards | No | ||
| Delphi | Yes | Yes | Yes | 2009 | 2009 | No | ||
| C family | C | Yes | Yes | Yes in GNU C | Yes in Clang(Blocks) | Yes in Clang(Blocks) | No | Has function pointers. |
| C++ | Yes | Yes | C++11[10] | C++11[11] | C++11[11] | C++11 | Has function pointers, function objects. (Also, see below.)
Explicit partial application possible with | |
| C# | Yes | Yes | 7 | 2.0 / 3.0 | 2.0 | 3.0 | Has delegates (2.0) and lambda expressions (3.0). | |
| Objective-C | Yes | Yes | Using anonymous | 2.0 + Blocks[12] | 2.0 + Blocks | No | Has function pointers. | |
| Java | Yes | Yes | Using anonymous | Java 8 | Java 8 | Yes | Has anonymous inner classes. | |
| Go | Yes | Yes | Using anonymous | Yes | Yes | Yes[13] | ||
| Limbo | Yes | Yes | Yes | Yes | Yes | No | ||
| Newsqueak | Yes | Yes | Yes | Yes | Yes | No | ||
| Rust | Yes | Yes | Yes | Yes | Yes | Yes[14] | ||
| Functional languages | Lisp | Syntax | Syntax | Yes | Yes | Common Lisp | No | (see below) |
| Scheme | Yes | Yes | Yes | Yes | Yes | SRFI 26[15] | ||
| Julia | Yes | Yes | Yes | Yes | Yes | Yes | ||
| Clojure | Yes | Yes | Yes | Yes | Yes | Yes | ||
| ML | Yes | Yes | Yes | Yes | Yes | Yes | ||
| Haskell | Yes | Yes | Yes | Yes | Yes | Yes | ||
| jq | Yes | No | Yes | Expressions only | Downwards | No | ||
| Scala | Yes | Yes | Yes | Yes | Yes | Yes | ||
| Erlang | Yes | Yes | Yes | Yes | Yes | Yes | ||
| Elixir | Yes | Yes | Yes | Yes | Yes | Yes | ||
| F# | Yes | Yes | Yes | Yes | Yes | Yes | ||
| OCaml | Yes | Yes | Yes | Yes | Yes | Yes | ||
| Scripting languages | Io | Yes | Yes | Yes | Yes | Yes | No | |
| JavaScript | Yes | Yes | Yes | Yes | Yes | ECMAScript 5 | Partial application possible with user-land code on ES3 [16] | |
| Lua | Yes | Yes | Yes | Yes | Yes | Yes[17] | ||
| PHP | Yes | Yes | Using anonymous | 5.3 | 5.3 | No | Partial application possible with user-land code. | |
| Perl | Yes | Yes | 6 | Yes | Yes | 6[18] | ||
| Python | Yes | Yes | Yes | Expressions only | Yes | 2.5[19] | (see below) | |
| Ruby | Syntax | Syntax | Unscoped | Yes | Yes | 1.9 | (see below) | |
| Other languages | Fortran | Yes | Yes | Yes | No | No | No | |
| Maple | Yes | Yes | Yes | Yes | Yes | No | ||
| Mathematica | Yes | Yes | Yes | Yes | Yes | No | ||
| MATLAB | Yes | Yes | Yes | Yes[20] | Yes | Yes | Partial application possible by automatic generation of new functions.[21] | |
| Smalltalk | Yes | Yes | Yes | Yes | Yes | Partial | Partial application possible through library. | |
| Swift | Yes | Yes | Yes | Yes | Yes | Yes | ||
- C++
- C++11 closures can capture non-local variables by copy construction, by reference (without extending their lifetime), or by move construction (the variable lives as long as the closure does). The first option is safe if the closure is returned but requires a copy and cannot be used to modify the original variable (which might not exist any more at the time the closure is called). The second option potentially avoids an expensive copy and allows to modify the original variable but is unsafe in case the closure is returned (see dangling references). The third option is safe if the closure is returned and does not require a copy but cannot be used to modify the original variable either.
- Java
- Java 8 closures can only capture final or "effectively final" non-local variables. Java's function types are represented as Classes. Anonymous functions take the type inferred from the context. Method references are limited. For more details, see Anonymous function § Java limitations.
- Lisp
- Lexically scoped Lisp variants support closures. Dynamically scoped variants do not support closures or need a special construct to create closures.[22]
- In Common Lisp, the identifier of a function in the function namespace cannot be used as a reference to a first-class value. The special operator
functionmust be used to retrieve the function as a value:(function foo)evaluates to a function object.#'fooexists as a shorthand notation. To apply such a function object, one must use thefuncallfunction:(funcall #'foo bar baz). - Python
- Explicit partial application with
functools.partialsince version 2.5, andoperator.methodcallersince version 2.6. - Ruby
- The identifier of a regular "function" in Ruby (which is really a method) cannot be used as a value or passed. It must first be retrieved into a
MethodorProcobject to be used as first-class data. The syntax for calling such a function object differs from calling regular methods. - Nested method definitions do not actually nest the scope.
- Explicit currying with
[1].
See also
[edit]- Defunctionalization
- eval
- First-class message
- Kappa calculus – a formalism which excludes first-class functions
- Man or boy test
- Partial application
Notes
[edit]- ^ Abelson, Harold; Sussman, Gerald Jay (1984). Structure and Interpretation of Computer Programs. MIT Press. Formulating Abstractions with Higher-Order Procedures. ISBN 0-262-01077-1. Archived from the original on 2021-09-21. Retrieved 2021-09-27.
- ^ Programming language pragmatics, by Michael Lee Scott, section 11.2 "Functional Programming".
- ^ Roberto Ierusalimschy; Luiz Henrique de Figueiredo; Waldemar Celes (2005). "The Implementation of Lua 5.0". Journal of Universal Computer Science. 11 (7): 1159–1176. doi:10.3217/jucs-011-07-1159.
- ^ a b Burstall, Rod; Strachey, Christopher (2000). "Understanding Programming Languages" (PDF). Higher-Order and Symbolic Computation. 13 (52): 11–49. doi:10.1023/A:1010052305354. S2CID 1989590. Archived from the original on February 16, 2010.
{{cite journal}}: CS1 maint: bot: original URL status unknown (link) (also on 2010-02-16 - ^ Joel Moses. "The Function of FUNCTION in LISP, or Why the FUNARG Problem Should be Called the Environment Problem". MIT AI Memo 199, 1970.
- ^ "If you try to call the nested function through its address after the containing function has exited, all hell will break loose." (GNU Compiler Collection: Nested Functions)
- ^ Andrew W. Appel (1995). "Intensional Equality ;=) for Continuations".
- ^ Tanenbaum, A.S. (1977). "A comparison of PASCAL and Algol 68". The Computer Journal. 21 (4): 319. doi:10.1093/comjnl/21.4.316.
- ^ "The History of Python: Origins of Python's "Functional" Features". 21 April 2009.
- ^ Nested functions using lambdas/closures
- ^ a b Doc No. 1968: V Samko; J Willcock, J Järvi, D Gregor, A Lumsdaine (February 26, 2006) Lambda expressions and closures for C++
- ^ "Mac Dev Center: Blocks Programming Topics: Introduction". Archived from the original on 2009-08-31.
- ^ "2 examples in Go that you can have partial application".
- ^ "partial_application". Docs.rs. Retrieved 2020-11-03.
- ^ "SRFI 26: Notation for Specializing Parameters without Currying".
- ^ "John Resig - Partial Application in JavaScript".
- ^ Katz, Ian (July 23, 2010). "Lua Code for Curry (Currying Functions)". Archived from the original on 2018-11-06.
- ^ "Blog | Perlgeek.de :: Currying".
- ^ "What's New in Python 2.5 — Python 3.10.0 documentation".
- ^ "Anonymous Functions - MATLAB & Simulink - MathWorks United Kingdom".
- ^ Partial Function Evaluation in MATLAB
- ^ Closures in ZetaLisp Archived 2012-03-19 at the Wayback Machine
References
[edit]- Leonidas Fegaras. "Functional Languages and Higher-Order Functions". CSE5317/CSE4305: Design and Construction of Compilers. University of Texas at Arlington.
External links
[edit]- First-class functions on Rosetta Code.
- Higher order functions Archived November 12, 2019, at the Wayback Machine at IBM developerWorks
First-class function
View on Grokipediaconst greet = () => console.log("Hello");), passing it as an argument (setTimeout(greet, 1000);), or returning it from another function to create dynamic behaviors.[2]
Definition and Basics
Core Definition
In computer science, a first-class function in a programming language is one that is treated as a first-class citizen, meaning it can be stored in data structures, passed as an argument to other functions, returned as a result from other functions, and created dynamically at runtime.[3] This treatment allows functions to be manipulated uniformly with other values, such as integers or objects, without imposing special restrictions that limit their usage compared to primitive types. Essential properties of first-class functions include their full integration into the language's type system, enabling them to be bound to names, composed with other functions, and stored or retrieved from collections like lists or arrays. For instance, a function can be assigned to a variable and later invoked through that variable, just as one might assign and use a numerical value. This parity ensures that functions participate seamlessly in the language's expressive power, facilitating modular and reusable code structures.[3][5] The concept originated in the context of functional programming during the 1960s, with British computer scientist Christopher Strachey formalizing the distinction between first-class and second-class functions in his seminal work on programming language fundamentals.[3] Early practical examples appeared in Lisp, developed by John McCarthy starting in 1958, where functions were represented as symbols that could be manipulated like data, passed as arguments, and evaluated dynamically.[6] A basic illustration in pseudocode is assigning a function to a variable, such asadd = lambda x, y: x + y, which then allows add(2, 3) to compute the sum. This capability underpins higher-order functions as a natural consequence.[6]
Distinction from Second-Class Functions
Second-class functions, also known as restricted or second-class citizens in programming languages, are those that cannot be fully treated as ordinary values and are limited in their manipulability. Unlike first-class functions, they are typically confined to static definition and invocation, without the ability to be passed as arguments, returned from other functions, assigned to variables, or stored in data structures dynamically.[7][8] In languages such as early Fortran or C, functions are generally second-class due to these constraints; they are identified solely by their names and cannot be created anonymously or reassigned at runtime. For instance, early Fortran lacks any mechanism for function pointers, preventing functions from being stored or passed, while in C, although function pointers exist to reference functions by address (e.g.,int (*f)(int) = &some_func;), they impose type restrictions, prevent anonymous function creation, and cannot capture dynamic environments, making storage in arrays or general manipulation cumbersome and not equivalent to data values.[7][9][10]
The elevation to first-class status offers significant advantages over second-class functions, including greater flexibility in code reuse, enhanced modularity through higher-order constructs, and the ability to implement functional programming paradigms such as map and reduce without excessive boilerplate or workarounds. This enables programmers to define computations that were previously impossible or inefficient in restricted systems, borrowing techniques like callbacks and iterators from functional languages into imperative ones.[7][11]
A illustrative comparison highlights these differences: in C, where functions are second-class, function pointers like void (*callback)(int) allow limited passing but require explicit declaration and cannot be seamlessly integrated as object properties or dynamically generated, leading to verbose and type-rigid code; in contrast, JavaScript treats functions as first-class objects, permitting them to be assigned to variables, added as object properties (e.g., obj.method = function(x) { return x * 2; };), or created anonymously for immediate use, fostering concise and interchangeable code.[9][1]
Practical Features
Assignment to Variables
In programming languages that support first-class functions, such as Python and JavaScript, functions can be bound to variables at runtime, treating them as ordinary values that can be dynamically rebound or aliased. This mechanic allows developers to reference a function through a variable name, enabling flexible code reuse without direct invocation by the function's original identifier. For instance, in Python, a lambda expression can be assigned to a variable to create a simple squaring operation:square = [lambda](/page/Lambda) x: x**2
result = square(5) # Returns 25
square = [lambda](/page/Lambda) x: x**2
result = square(5) # Returns 25
inc = (+ 1), which binds an increment function to the variable inc for later use.[12]
Assigning functions to variables simplifies code organization by allowing modular definitions that can be swapped or extended easily. A key use case is configuring behavior through function variables, such as callbacks in event-driven systems; for example, in Python, a greeting function can be assigned to a variable and passed to a higher-order function to customize output:
def greet(name):
return f"Hello, {name}!"
callback = greet
print(callback("Alice")) # Outputs "Hello, Alice!"
def greet(name):
return f"Hello, {name}!"
callback = greet
print(callback("Alice")) # Outputs "Hello, Alice!"
operations = {
'add': [lambda](/page/Lambda) x, y: x + y,
'subtract': [lambda](/page/Lambda) x, y: x - y
}
result = operations['add'](3, 4) # Returns 7
operations = {
'add': [lambda](/page/Lambda) x, y: x + y,
'subtract': [lambda](/page/Lambda) x, y: x - y
}
result = operations['add'](3, 4) # Returns 7
Passing as Arguments
One of the key capabilities enabled by first-class functions is their use as arguments to other functions, thereby defining higher-order functions—those that accept functions as parameters or return them as results.[1] This feature relies on the first-class status of functions, allowing them to be treated as values that can be passed dynamically during program execution.[14] In practice, passing a function as an argument involves supplying it directly in a function call, often using syntax that references the function by name or defines it inline. For instance, in JavaScript, themap method on arrays accepts a function to transform each element:
const doubled = [1, 2, 3].map(function(x) { return x * 2; }); // Results in [2, 4, 6]
const doubled = [1, 2, 3].map(function(x) { return x * 2; }); // Results in [2, 4, 6]
map, which applies it to each array item.[15] Similarly, in Python, the map function takes a callable as its first argument:
doubled = list(map(lambda x: x * 2, [1, 2, 3])) # Results in [2, 4, 6]
doubled = list(map(lambda x: x * 2, [1, 2, 3])) # Results in [2, 4, 6]
map use curried syntax for passing:
doubled = map (*2) [1, 2, 3] -- Results in [2, 4, 6]
doubled = map (*2) [1, 2, 3] -- Results in [2, 4, 6]
(*2) acts as the function argument.[17]
The primary benefits of passing functions as arguments lie in promoting abstraction and generality in code design. Higher-order functions enable the creation of reusable algorithms that operate on data structures without hardcoding specific operations, allowing developers to parameterize behavior at runtime. For example, operations like map, filter, and reduce (or fold) in functional programming languages abstract common patterns over collections: filter passes a predicate function to select elements, while reduce passes an accumulator function to aggregate values, making these utilities applicable to diverse data transformations without rewriting core logic.[18] This modularity reduces code duplication and enhances expressiveness, as seen in libraries across languages like JavaScript's Array methods or Python's functools module.[19]
Historically, this concept traces back to Lisp, where the apply function, introduced in John McCarthy's 1960 design, allowed passing a function and a list of arguments to invoke it dynamically, laying foundational support for higher-order programming in early symbolic computation.[6]
Returning as Results
In programming languages that support first-class functions, a function can return another function as its result, treating the returned function as a value that can be stored, invoked, or further manipulated. This capability allows for dynamic generation of functions at runtime, often using anonymous functions or lambdas to create specialized behaviors without defining them explicitly beforehand. For instance, in Python, a higher-order function can return a lambda expression that captures parameters from the outer scope, as in the following example:def multiplier(n):
return lambda x: x * n
double = multiplier(2)
print(double(5)) # Outputs: 10
def multiplier(n):
return lambda x: x * n
double = multiplier(2)
print(double(5)) # Outputs: 10
fun addn n x = n + x, where invoking addn 7 yields a new function that adds 7 to its input. These patterns facilitate modular code design by allowing functions to be composed and specialized on demand.[9]
The benefits of this feature include support for lazy evaluation, where computations are deferred until needed, and on-demand function creation, which promotes efficiency in scenarios like conditional logic or event handling. For example, a function might return different event handlers based on runtime conditions, such as user preferences, without evaluating all possibilities upfront. In JavaScript, this is exemplified by factory functions that return closures for event listeners:
function createLogger(level) {
return function(message) {
console.log(`[${level}] ${message}`);
};
}
errorLogger = createLogger('ERROR');
errorLogger('Something went wrong'); // Outputs: [ERROR] Something went wrong
function createLogger(level) {
return function(message) {
console.log(`[${level}] ${message}`);
};
}
errorLogger = createLogger('ERROR');
errorLogger('Something went wrong'); // Outputs: [ERROR] Something went wrong
Enabling Constructs
Anonymous Functions
Anonymous functions, also known as lambda expressions or function literals, are nameless functions defined inline within an expression, allowing for the dynamic creation of functions without prior declaration.[22] This concept originates from Alonzo Church's lambda calculus, introduced in the 1930s, where the lambda operator abstracts parameters to form anonymous functions, such as λn.n³, which maps each input n to its cube.[23] In programming languages supporting first-class functions, anonymous functions enable concise construction of one-off functions that can be passed as arguments or returned from other functions, facilitating higher-order programming without the overhead of naming and storing them separately.[24] Syntax for anonymous functions varies by language but typically involves a keyword or operator to denote the function body. In Python, the lambda keyword creates an anonymous function, as inlambda x: x**2, which defines a function that squares its input argument x.[24] Similarly, JavaScript's arrow function syntax provides a compact form for anonymous functions, such as x => x**2, introduced in ECMAScript 2015 to simplify functional-style code.[25] These constructs support first-class treatment by producing function objects that integrate seamlessly into expressions, such as supplying them to higher-order functions like map or filter.
Anonymous functions are particularly useful for quick, temporary implementations in higher-order contexts, such as defining a sorting key on-the-fly or transforming data in a callback without declaring a separate named function.[24] For instance, in Python, one might use sorted(items, key=lambda item: item.length) to sort a list by item length.[24] This promotes code brevity and readability in functional paradigms, aligning with the dynamic nature of first-class functions.
However, anonymous functions often face syntactic limitations to maintain simplicity and prevent abuse as full-fledged code blocks. In many languages, including Python, lambda expressions are restricted to a single expression, excluding statements like loops or conditionals, which must be handled via equivalent expression forms such as conditional expressions.[24] JavaScript arrow functions similarly limit the body to expressions or a single block, though they allow implicit returns for conciseness, but they cannot be used as constructors or access certain keywords like arguments.[25] These constraints ensure anonymous functions remain lightweight tools for inline use rather than complex logic.
Nested Functions
Nested functions, also known as inner functions, are functions defined within the body of another function, a feature that enhances modularity and encapsulation in programming languages supporting first-class functions. In such languages, the inner function inherits the lexical scope of the outer function, allowing it to access variables from the enclosing scope without explicit passing. This scoping mechanism promotes code organization by keeping related functionality grouped together. For instance, in Python, nested functions are declared using standard def statements inside another function, enabling the creation of localized helpers that are not visible outside the outer function. The mechanics of nested functions involve the inner function being able to reference and utilize variables from the outer function's scope during execution, which facilitates data hiding and reduces namespace pollution. Consider this Python example where an outer function defines a variable and an inner function accesses it:def outer(x):
def inner(y):
return x + y # inner accesses x from outer's scope
return inner
add_five = outer(5)
result = add_five(3) # Returns 8
def outer(x):
def inner(y):
return x + y # inner accesses x from outer's scope
return inner
add_five = outer(5)
result = add_five(3) # Returns 8
inner captures the value of x from outer, demonstrating how nested functions enable the creation of specialized, context-bound operations that maintain access to enclosing data. This access is resolved at definition time through lexical scoping, ensuring the inner function behaves predictably even after the outer function returns. In JavaScript, a similar structure uses function declarations or expressions inside another function, with the inner function retaining access to the outer's variables via closure-like behavior, though JavaScript emphasizes functional expressions for this purpose.
Nested functions find use cases in scenarios requiring helper functions tailored to a specific outer context, such as implementing private utilities for algorithms or maintaining temporary state without global exposure. For example, they are employed in recursive algorithms where subroutines need localized variables, or in event handlers that encapsulate configuration data. In data processing pipelines, nested functions can serve as private implementations for transformations that are irrelevant beyond the parent function, promoting cleaner and more maintainable code. This pattern is particularly valuable in functional programming paradigms for composing behaviors without cluttering the global scope.
Language support for nested functions varies even among those with first-class function capabilities; while Python and JavaScript provide full lexical nesting, others impose restrictions. In C++, traditional nested functions are not natively supported in standard functions, though lambdas introduced in C++11 offer similar scoping for anonymous inner constructs, requiring extensions like GNU C for true named nesting. Haskell, despite strong first-class support, handles nesting through let bindings or where clauses rather than direct function-in-function declarations, adapting the concept to its pure functional model. These variations highlight how nested functions adapt to language design philosophies, balancing expressiveness with implementation constraints.
Advanced Implications
Closures and Non-Local Variables
In programming languages that support first-class functions, a closure is formed when a function is defined within another function and captures references to variables from its enclosing lexical scope, creating a bundle of the function and its environment that persists even after the outer function has returned.[11] This mechanism allows the inner function to access and potentially modify non-local variables long after the surrounding context has ended, effectively preserving the state of the lexical environment.[26] The mechanics of closures typically involve capturing non-local variables—also known as free variables—by reference in languages like JavaScript and Python, meaning that all closures sharing the same captured variable will observe and reflect changes to it.[26] Some languages, such as C++ with lambda expressions, offer choices between capture by reference (allowing shared mutation) or by value (creating independent copies to avoid unintended side effects).[27] For instance, in JavaScript, the following code demonstrates a closure where the returned anonymous function captures thecount variable by reference:
function makeCounter() {
let count = 0;
return function() {
return ++count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
function makeCounter() {
let count = 0;
return function() {
return ++count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
count, illustrating how the closure maintains access to the non-local variable without relying on global state.[26]
Non-local variables in closures are resolved through the lexical scoping chain, enabling the creation of stateful functions that encapsulate data privately and avoid global namespace pollution.[11] This resolution binds free variables to the values present in the enclosing scope at the time of closure creation, supporting modular and reusable code patterns.[26]
Closures find practical use in implementing iterators, where they maintain iteration state across calls without external storage; in creating module-like structures that hide internal variables behind public interfaces; and in function currying, where partial application returns a new closure with bound arguments for later use.[26] For example, closures enable the simulation of iterators by capturing and updating a loop counter in a returned function.[28]
However, closures introduce pitfalls when capturing mutable state that is shared across multiple instances, as modifications in one closure can unexpectedly alter behavior in others, leading to bugs that are difficult to debug due to the hidden dependencies.[26] This issue is particularly pronounced in pre-ES6 JavaScript with var declarations in loops, where all closures capture the same mutable variable, causing them to reference the final loop value rather than the intended per-iteration state.[26] To mitigate such problems, developers often use block-scoped variables or capture by value where available.[27]
Function Equality
In programming languages that treat functions as first-class citizens, equality between functions can be assessed through several distinct criteria: referential equality, which checks if two function values refer to the same object in memory; structural equality, which compares the internal representation or code structure of the functions; and extensional equality, which verifies if the functions produce identical outputs for all possible inputs, effectively equating them based on observable behavior.[29] These approaches vary in computational feasibility, with referential equality being efficient but coarse, while extensional equality is theoretically ideal yet often undecidable in practice due to the halting problem.[29] A key challenge in determining function equality arises from closures, where functions capture variables from their surrounding lexical environment, making structural comparisons nontrivial. For instance, two lambda expressions with identical bodies but capturing different variable instances or values from their environments are unequal, as the closure binds the specific references or state at creation time, altering the function's effective behavior despite superficial similarity.[26] This environmental dependency complicates optimization passes that rely on equating functions and can lead to unexpected results in testing or caching mechanisms.[29] Different languages adopt varied strategies for function equality. In JavaScript, strict equality (===) performs referential comparison for functions, as they are objects, returning true only if both operands point to the identical function instance in memory.[30] In contrast, functional languages like Haskell incorporate eta-equivalence in their core semantics, allowing extensional checks where functions are considered equal if one is eta-equivalent to the other (e.g., equals ), though runtime equality for functions is not directly supported via the Eq typeclass, relying instead on definitional equality in the type system.[31][32] These equality mechanisms have significant implications for software development and implementation. Referential equality simplifies storage in data structures like sets or maps but may prevent optimizations such as common subexpression elimination; structural equality aids debugging and testing by ignoring minor syntactic differences, yet struggles with closures; and extensional equality enables powerful abstractions in functional programming but demands approximations in real systems to avoid undecidability, influencing compiler design, unit testing frameworks, and functional composition patterns.[29]Theoretical Aspects
Role in Type Theory
In untyped lambda calculus, functions serve as the central abstraction, with all computational entities expressible as lambda abstractions and applications, enabling the encoding of data structures and operations purely in terms of functions. This foundational approach, developed by Alonzo Church, culminates in Church encoding, where even primitive values like natural numbers and booleans are represented as higher-order functions; for instance, the Church numeral for zero is , applying the function zero times to . Such encodings underscore the universality of functions, treating them as first-class citizens capable of manipulation without distinction from other terms. In type theory, the treatment of first-class functions evolves through typed lambda calculi, where function types are denoted as , indicating transformations from values of type to type . The simply typed lambda calculus supports basic higher-order functions but restricts generality due to the absence of polymorphism, limiting the reuse of functions across diverse types. In contrast, System F, or the polymorphic lambda calculus, introduced by Jean-Yves Girard, incorporates universal quantification over types, permitting higher-kinded types that fully elevate functions to first-class status; a representative type is , which abstracts over type variables to define polymorphic higher-order combinators.[33][34] This polymorphism ensures functions can be parameterized by types themselves, fostering expressive yet decidable type checking.[35] The implications of first-class functions in these systems enable type-safe higher-order programming, where functions can be abstracted, applied, and composed without runtime type errors, a property absent in the simply typed lambda calculus due to its rigid type assignments. Currying, a key concept transforming a multi-argument function into a sequence of single-argument functions (e.g., equivalent to ), facilitates partial application and enhances modularity in typed settings.[36] Furthermore, parametricity provides a reasoning framework, yielding "free theorems" about polymorphic functions—such as the inability to distinguish between isomorphic types—directly from their type signatures, as formalized by Philip Wadler.[37] This parametric behavior ensures lawful and predictable higher-order manipulations, underpinning the logical foundations of functional programming.[38]Language Implementations
Languages with Full Support
Programming languages with full support for first-class functions treat them as complete citizens of the type system, allowing dynamic creation at runtime, assignment to variables, passage as arguments to other functions, and return as results from functions. This capability enables higher-order functions, where functions accept or produce other functions, and supports advanced constructs like closures for capturing lexical environments. Such support originated with the Lisp family, which pioneered these features in the early days of computing.[39] The Lisp family, including dialects like Scheme and Common Lisp, provides exemplary full support. Lisp, introduced by John McCarthy in 1960, was the first language to implement functions as first-class objects, representable as symbolic expressions (S-expressions) that could be manipulated like data, passed as arguments (e.g., via theapply function), and stored in structures.[39] Scheme, developed in 1975 as an extension of lambda calculus within Lisp, further emphasized first-class functions through lexical scoping and procedures as values, enabling recursive control structures and dynamic creation via lambda expressions.[40] Common Lisp, standardized in 1994 by ANSI, inherits and extends this support, allowing functions to be first-class objects that can be passed around, stored, and called dynamically, facilitating functional composition in both interpreted and compiled environments.[41]
Other core languages include functional paradigms with robust first-class function support. Haskell, first defined in 1990, treats functions as first-class via its pure functional model, using lambda abstractions (e.g., \x -> x + 1) for anonymous creation and higher-order functions like map or monadic operations in Control.Monad for sequencing computations.[42] Variants of ML, such as Standard ML (defined in 1990 with revisions), support first-class functions as data objects that can be passed as arguments, returned, or bound to variables, with curried function types (e.g., int -> int -> int) enabling partial application.[43] Python, since its 1.0 release in 1994, implements first-class functions through objects that can be assigned (e.g., def add(x): return x + 1; increment = add), passed to higher-order functions like map, and returned, supporting functional styles alongside imperative code.[20]
Imperative and scripting languages also offer full support. JavaScript, first standardized as ECMAScript in 1997, provides first-class functions via arrow functions or declarations (e.g., const greet = () => console.log('Hello');), which can be assigned to variables, passed as callbacks, or returned, underpinning event-driven and asynchronous programming.[1] Ruby, released in 1995, achieves this through Proc and Lambda objects, which encapsulate code blocks as first-class entities storable in variables, passable to methods (e.g., Proc.new { |x| x * 2 }), and callable, enabling higher-order usage despite methods themselves not being directly first-class.[44]
Modern additions like Swift, introduced by Apple in 2014, extend full support via closures, which are self-contained, first-class blocks of functionality (e.g., { (x: Int) -> Int in return x * 2 }) that capture surrounding state, can be passed around, and facilitate functional paradigms in an object-oriented context.[45] These features across languages facilitate functional programming paradigms by promoting code reusability, abstraction over control flow, and composability, reducing reliance on mutable state and enabling elegant solutions to problems like data transformation and event handling.[42]
