Lambda functions – also known as anonymous functions – are a common and useful feature of programming languages.

Why are lambda functions useful?

One typical use of a lambda function is to transform a list into another list, like here in Javascript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// a "React component"
function List(props) {

    const mapFn = function(item) {
        return <li>{item.name}</li>
    }
    
    const listItems = props.items.map(mapFn)
    
    /*
    // The same with a loop
    const listItems = []
    for (let i = 0; i < props.items.length; ++i) {
        let item = props.items[i]
        listItems.push(<li>{item.name}</li>)
    }
    */
    
    return (
        <ul>
            {listItems}
        </ul>
    )
}

Here we already have a named function (List) and an anonymous function (the function in variable mapFn). In such a simple example, the benefit of using an anonymous function over a loop is not quite clear. As the loop gets more complicated, the benefits start to come into focus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// a "React component"
function List(props) {
    
    // Using Arrow functions the expression becomes even more concise
    const listItems = props.items
        .filter(item => !item.hidden)
        .map(item => <li>{item.name}</li>)
    
    /*
    // The same with a loop
    const listItems = []
    for (let i = 0; i < props.items.length; ++i) {
        let item = props.items[i]
        
        if (item.hidden)
            continue
        
        listItems.push(<li>{item.name}</li>)
    }
    */
    
    return (
        <ul>
            {listItems}
        </ul>
    )
}

Here we conditionally omit some items. As more logic gets added, the loop becomes busier and busier and harder to understand. The "Collection Pipeline", in contrast, is clear and easy to read. Fowler (2019, 231–236) suggests a related refactoring called “Replace Loop with Pipeline”.

The collection pipeline is made possible by having two generic functions (filter() and map()) in the list, each of which can be customized by giving it a function to apply. They will then exclude members of the list based on the function return value for a given item (filter()) or add that return value to the new list under construction (map()).

The same could be achieved by using classes or function pointers, but both of those are cumbersome and a poor fit for the task. Depending on the language, either of those might require one to create a new global or module-level artifact for one-off use (since languages without lambdas cannot, by definition, define and/or create these types of objects when needed and in the most restricted scope possible). Even worse, all those would need to be named.

Compared to these, lambda functions hit a sweet-spot:

  • Can be effortlessly created for the task at hand
  • Can effortlessly enclose state or data from the definition context with closures (extremely useful/convenient for callbacks)
  • Closures also make it possible to create a lambda from a template ((x) => (y) => y > x)
  • Provides an isolated scope for anything more complicated should it be required
  • Don’t need to be named, or the naming can be restricted to method/function scope
  • (Ideally) self-similar to the context (ie. in Javascript a function can be defined inside a function, requiring no special syntax)

What the world would look like without lambda functions

It is also instructive to look into languages that lack lambda functions. Enter ABAP.

ABAP doesn’t have lambda functions, but recent versions have added support for reducing and mapping tables, which makes it an interesting case to study.

Here are two reduces and a map in another language (Erlang) with lambda functions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
List = [1, 2, 3, 4, 5],

% lists:foldl/3 is Erlang's version of Array.prototype.reduce()
% https://www.erlang.org/doc/man/lists.html#foldl-3

SumFn = fun (Element, Acc) -> Element + Acc end,
15 = lists:foldl(SumFn, 0, List),

MaxFn = fun (Element, CurrentMax) -> max(Element, CurrentMax) end,
5 = lists:foldl(MaxFn, -1, List),

MapFn = fun (Element) -> {tag, Element} end,
[{tag, 1}, {tag, 2}, {tag, 3}, {tag, 4}, {tag, 5}] = lists:map(MapFn, List).

Nothing special here: defining lambda functions is almost like defining normal functions in modules, except that recursion requires jumping through extra hoops and sans the function naming. For mapping we could have used list comprehension, but map serves the example better.

Now for how these look in ABAP:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
DATA list TYPE STANDARD TABLE OF i.
list = VALUE #( ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) ).

DATA(sum) = REDUCE i(
  INIT tmp = 0
  FOR sum_element IN list
  NEXT tmp = tmp + sum_element
).

DATA(max) = REDUCE i(
  INIT current_max = 0
  FOR max_element IN list
  NEXT current_max = lcl_util=>max( im_1 = max_element im_2 = current_max )
).

TYPES: BEGIN OF ty_s_tuple,
         value1 TYPE string,
         value2 TYPE i,
       END OF ty_s_tuple,
       ty_t_tuple TYPE STANDARD TABLE OF ty_s_tuple WITH EMPTY KEY.

DATA(lt_tuple) = VALUE ty_t_tuple(
  FOR map_element IN list
  (
    value1 = |tag|
    value2 = map_element
  )
).

At first glance it doesn’t look that bad. We loop the elements of the list to create the sum and to find the max value. We also loop the list to create another list.

But the trouble starts when you realize that both REDUCE and VALUE introduce new syntax for defining variables and REDUCE also introduces brand-new syntax to control the looping.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
" Classic ABAP variable declaration
DATA lv_material TYPE matnr.

" Inline variable declaration
DATA(lv_material2) = VALUE matnr( ).

" REDUCE
REDUCE i(
  INIT current_max = 0 " New integer variable current_max
  FOR max_element IN list " New integer variable max_element
  " ...
).

" VALUE
VALUE ty_t_tuple(
  FOR map_element IN list " New integer variable map_element
  " ...
).

Declaring new variables in VALUE and REDUCE looks nothing like it does elsewhere in ABAP – and it even differs between the two. This might be tolerable, though harder to remember, if these new variables had an extremely limited scope (for example just the statement), but no: in ABAP, the variables declared in REDUCE and VALUE are created in the scope containing the statement. Ie. they create new variables in the method they are used in. Even worse, there’s a strange inconsistency to what is allowed and what is not:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
""" Option 1
" This will produce a syntax error: "TMP/ELEMENT already declared"

DATA(tmp) = 0.
DATA(element) = 0.

DATA(max) = REDUCE i(
  INIT tmp = 0        " <- error
  FOR element IN list " <- error
  NEXT current_max = lcl_util=>max( im_1 = max_element im_2 = current_max )
).

"""

""" Option 2
" This will produce a syntax error: "Variable TMP cannot be used here"

DATA(max) = REDUCE i(
  INIT tmp = 0
  FOR element IN list
  NEXT current_max = lcl_util=>max( im_1 = max_element im_2 = current_max )
).

tmp = 1. " <- error

"""

""" Option 3
" This, on the other hand, is allowed

DATA(max) = REDUCE i(
  INIT tmp = 0
  FOR element IN list
  NEXT current_max = lcl_util=>max( im_1 = max_element im_2 = current_max )
).

DATA(sum) = REDUCE i(
  INIT tmp = 0
  FOR element IN list
  NEXT tmp = tmp + sum_element
).

DATA(lt_tuple) = VALUE ty_t_tuple(
  FOR tmp IN list
  (
    value1 = |tag|
    value2 = tmp
  )
).

"""

The variable names are not allowed to collide, even though variables declared in VALUE and REDUCE are not actually usable outside those expressions.

The NEXT part of REDUCE is unlike most ABAP syntax, making it hard to remember what is allowed and how it should be used.

And ultimately, REDUCE in ABAP is limited. Although there are quite complicated additions to the FOR part, the NEXT can only accommodate a fairly simple assignment expression. This means that any complicated logic basically needs to be implemented in a new method (which might be easier or harder, depending on if the statement is already in an object context).

Wikipedia has this to say of ABAP:

In contrast with languages like C/C++ or Java, which define a limited set of language-specific statements and provide most functionality via libraries, ABAP contains an extensive amount of built-in statements. These statements traditionally used sentence-like structures and avoided symbols, making ABAP programs relatively verbose. However, in more recent versions of the ABAP language, a terser style is possible.

Thus it was also with REDUCE: new syntax had to be created in order to make anything like REDUCE possible in ABAP. And new syntax, especially the rarely occurring kind, is hard to remember, is hard to use, is ultimately a limited special case compared to normal statements and even shares variable scope with all that surrounds the statement.

In comparison, lambda functions have none of these shortcomings. Especially the fact that lambda functions (in the best languages) look and behave like any other functions, requiring little to no special syntax. One might say that lambda functions, rather than specialized syntax, produce a more composable, flexible language.

Sources

  • Fowler, M. (2019) Refactoring: improving the design of existing code. Addison-Wesley. 978-0-13-475759-9.