I have previously written that ABAP does not have higher-order functions, since there are no anonymous functions in the language. What this means is that a function (ie. callable piece of code) cannot be passed around in ABAP. We can, however, pass object references around like in any object-oriented language. These two things don’t immediately seem related, but William R. Cook gives some food for thought. After all, when we pass an object reference into a method, what have we actually done? According to Cook (2009, Section 3.1):

Object interfaces are essentially higher-order types, in the same sense that passing functions as values is higher-order. Any time an object is passed as a value, or returned as a value, the object-oriented program is passing functions as values and returning functions as values. The fact that the functions are collected into records and called methods is irrelevant. As a result, the typical object-oriented program makes far more use of higher-order values than many functional programs.

So an interface can be thought of as a structure of functions. These functions would ideally be pure (all functions in Cook’s example are) and the interface immutable by design (ie. a value type), though whether the functions are pure or not is immaterial: in any case, parameters (functions) customize what the method (higher order function) does. Returning interfaces (ie, functions) is also fairly common in ABAP. So ABAP does, indeed, have higher-order functions.

What I think mostly confuses this intuition is that object references almost always come with some associated, mutable state and have a name. This leads us to think of them more in terms of their state and identity than their methods, ie. ZCL_TIME_VALUE instead of func inSeconds() -> Int, making the connection with functions less than clear.

So What?

Now that we know that ABAP has higher-order functions, what can we do with them?

Reduce

We could combine higher-order functions with REDUCE ABAP operation. First we create a type for a function that will act as if it were the callback parameter in Array.prototype.reduce in JavaScript.

1
2
3
4
5
6
7
8
INTERFACE zif_reduce_int_table_function.

  METHODS execute
    IMPORTING im_result        TYPE i
              im_entry         TYPE i
    RETURNING VALUE(re_result) TYPE i.

ENDINTERFACE.

Then we create a few implementations for it.

 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
CLASS zcl_sum_table DEFINITION.

  PUBLIC SECTION.
    INTERFACES zif_reduce_int_table_function.

ENDCLASS.

CLASS zcl_sum_table IMPLEMENTATION.

  METHOD zif_reduce_int_table_function~execute.
    re_result = im_result + im_entry.
  ENDMETHOD.

ENDCLASS.


CLASS zcl_min_of_table DEFINITION.

  PUBLIC SECTION.
    INTERFACES zif_reduce_int_table_function.

ENDCLASS.

CLASS zcl_min_of_table IMPLEMENTATION.

  METHOD zif_reduce_int_table_function~execute.
    re_result = nmin( val1 = im_result
                      val2 = im_entry ).
  ENDMETHOD.

ENDCLASS.


CLASS zcl_max_of_table DEFINITION.

  PUBLIC SECTION.
    INTERFACES zif_reduce_int_table_function.

ENDCLASS.

CLASS zcl_max_of_table IMPLEMENTATION.

  METHOD zif_reduce_int_table_function~execute.
    re_result = nmax( val1 = im_result
                      val2 = im_entry ).
  ENDMETHOD.

ENDCLASS.

And then we create a higher-order method that will use this callback to alter what it does.

 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
CLASS class DEFINITION.

  PUBLIC SECTION.
    CLASS-METHODS reduce_int_table
      IMPORTING io_fn            TYPE REF TO zif_reduce_int_table_function
                im_initial_value TYPE i
      RETURNING VALUE(re_result) TYPE i.

ENDCLASS.

CLASS class IMPLEMENTATION.

  METHOD reduce_int_table.

    DATA lt_int_table TYPE STANDARD TABLE OF i.
    lt_int_table = VALUE #( ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) ).

    re_result = REDUCE #(
      INIT result = im_initial_value
      FOR entry IN lt_int_table
      NEXT result = io_fn->execute( im_result = result
                                    im_entry  = entry )
    ).

  ENDMETHOD.

ENDCLASS.

The behavior of this method is now determined by the “function” we pass as a parameter.

1
2
3
4
5
6
7
8
9
START-OF-SELECTION.
  ASSERT 5 = class=>reduce_int_table( io_fn            = NEW zcl_max_of_table( )
                                      im_initial_value = -1 ).

  ASSERT 1 = class=>reduce_int_table( io_fn            = NEW zcl_min_of_table( )
                                      im_initial_value = 1000 ).

  ASSERT 15 = class=>reduce_int_table( io_fn            = NEW zcl_sum_table( )
                                       im_initial_value = 0 ).

Unfortunately, ABAP’s clunky type system and lack of actual lambda functions means that this type of programming is not terribly well supported. Already we had to create an interface and three classes, and those types have had to have been global and have names. Those types will also only work for integers given how ABAP supports generic types. We could change the IMPORTING parameters to generic type numeric, but RETURNING parameters cannot be generically typed, ie. at least the returned type will be fixed.

Compose

Reduce is just one type of higher-order function. Another is compose, which creates new functions from existing functions. This is a type of higher-order function that ABAP makes even harder to use.

Continuing on from our previous example, which more or less dealt with functions of type [Integer] -> Integer, maybe we additionally have a function of type [Integer] -> [Integer] that we would like to use with our previous function. Ie, we would like to do something with the table of integers before it is handled by the reduce. In Haskell, this would be simple.

1
2
3
4
5
6
7
8
9
doubleList :: [Integer] -> [Integer]
doubleList list = map (*2) list

sumList :: [Integer] -> Integer
sumList list = sum list

sumDoubledList :: [Integer] -> Integer
sumDoubledList = sumList . doubleList
-- above line is equivalent to: sumDoubledList list = sumList(doubleList(list))

Now for how this could be done in ABAP. First we will extract the reduce operation from reduce_int_table method so that we have a function of type [Integer] -> Integer. We also accept the reduce callback function in the constructor for the same reason. The method is defined as an instance method because otherwise we could not carry around a reference to it.

 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
INTERFACE zif_reduce_int_table.

  TYPES ty_t_integer TYPE STANDARD TABLE OF i WITH EMPTY KEY.

  METHODS execute
    IMPORTING it_integer       TYPE ty_t_integer
              im_initial_value TYPE i
    RETURNING VALUE(re_result) TYPE i.

ENDINTERFACE.


CLASS zcl_reduce_int_table DEFINITION.

  PUBLIC SECTION.
    INTERFACES zif_reduce_int_table.
    ALIASES execute FOR zif_reduce_int_table~execute.

    METHODS constructor
      IMPORTING io_fn TYPE REF TO zif_reduce_int_table_function.

  PRIVATE SECTION.
    DATA o_fn TYPE REF TO zif_reduce_int_table_function.

ENDCLASS.

CLASS zcl_reduce_int_table IMPLEMENTATION.

  METHOD constructor.
    o_fn = io_fn.
  ENDMETHOD.

  METHOD zif_reduce_int_table~execute.

    re_result = REDUCE #(
      INIT result = im_initial_value
      FOR entry IN it_integer
      NEXT result = o_fn->execute( im_result = result
                                   im_entry  = entry )
    ).

  ENDMETHOD.

ENDCLASS.

Then we define a function of type [Integer] -> [Integer] and an implementation for it.

 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
INTERFACE zif_map_int_table.

  TYPES ty_t_integer TYPE STANDARD TABLE OF i WITH EMPTY KEY.

  METHODS execute
    IMPORTING it_integer       TYPE ty_t_integer
    RETURNING VALUE(re_result) TYPE ty_t_integer.

ENDINTERFACE.


CLASS zcl_double_int_table DEFINITION.

  PUBLIC SECTION.
    INTERFACES zif_map_int_table.
    ALIASES execute FOR zif_map_int_table~execute.

ENDCLASS.

CLASS zcl_double_int_table IMPLEMENTATION.

  METHOD zif_map_int_table~execute.
    re_result = VALUE #(
      FOR vv_integer IN it_integer
      ( vv_integer * 2 )
    ).
  ENDMETHOD.

ENDCLASS.

Now we would like to compose our double function with our reduce function. Calling one after the other is not too bad. We wrap the composed operation in a new class so that we can pass around a reference to it.

 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
CLASS zcl_sum_doubled_table DEFINITION CREATE PRIVATE.

  PUBLIC SECTION.
    INTERFACES zif_reduce_int_table.

    CLASS-METHODS get_instance
      RETURNING VALUE(re_result) TYPE REF TO zif_reduce_int_table.

    CLASS-METHODS execute
      IMPORTING it_integer       TYPE zif_reduce_int_table=>ty_t_integer
      RETURNING VALUE(re_result) TYPE i.

ENDCLASS.

CLASS zcl_sum_doubled_table IMPLEMENTATION.

  METHOD get_instance.

    re_result = NEW zcl_sum_doubled_table( ).

  ENDMETHOD.

  METHOD execute.

    " Already, reusing the zif_int_table_reduce introduces an
    " unnecessary extra parameter im_initial_value into our
    " "function"
    re_result = get_instance( )->execute( it_integer       = it_integer
                                          im_initial_value = 0 ).

  ENDMETHOD.

  METHOD zif_reduce_int_table~execute.

    DATA(lo_reducer) = NEW zcl_reduce_int_table( io_fn = NEW zcl_sum_table( ) ).

    DATA(lo_doubler) = NEW zcl_double_int_table( ).

    re_result = lo_reducer->execute( it_integer       = lo_doubler->execute( it_integer = it_integer )
                                     im_initial_value = 0 ).

  ENDMETHOD.

ENDCLASS.


START-OF-SELECTION.
  DATA lt_integer TYPE STANDARD TABLE OF i WITH EMPTY KEY.
  lt_integer = VALUE #( ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) ).
  
  ASSERT 30 = zcl_sum_doubled_table=>execute( it_integer = lt_integer ).

Not the prettiest, but it works. Now we would like to generically compose any two methods of these two types.

 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
52
53
54
CLASS zcl_compose_map_reduce_int_tbl DEFINITION CREATE PRIVATE.

  PUBLIC SECTION.
    INTERFACES zif_reduce_int_table.

    CLASS-METHODS compose
      IMPORTING io_map_fn    TYPE REF TO zif_map_int_table
                io_reduce_fn TYPE REF TO zif_reduce_int_table
      RETURNING VALUE(re_result) TYPE REF TO zif_reduce_int_table.

  PRIVATE SECTION.
    DATA o_map_fn TYPE REF TO zif_map_int_table.
    DATA o_reduce_fn TYPE REF TO zif_reduce_int_table.

    METHODS constructor
      IMPORTING io_map_fn    TYPE REF TO zif_map_int_table
                io_reduce_fn TYPE REF TO zif_reduce_int_table.

ENDCLASS.

CLASS zcl_compose_map_reduce_int_tbl IMPLEMENTATION.

  METHOD constructor.

    o_map_fn = io_map_fn.
    o_reduce_fn = io_reduce_fn.

  ENDMETHOD.

  METHOD zif_reduce_int_table~execute.

    re_result = o_reduce_fn->execute( it_integer       = o_map_fn->execute( it_integer = it_integer )
                                      im_initial_value = im_initial_value ).

  ENDMETHOD.

  METHOD compose.

    re_Result = NEW zcl_compose_map_reduce_int_tbl( io_map_fn    = io_map_fn
                                                    io_reduce_fn = io_reduce_fn ).

  ENDMETHOD.

ENDCLASS.


START-OF-SELECTION.
  DATA(lo_reduce_fn) = NEW zcl_reduce_int_table( io_fn = NEW zcl_sum_table( ) ).

  DATA(lo_composed_fn) = zcl_compose_map_reduce_int_tbl=>compose( io_map_fn    = NEW zcl_double_int_table( )
                                                                  io_reduce_fn = lo_reduce_fn ).

  ASSERT 30 = lo_composed_fn->execute( it_integer       = VALUE #( ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 ) )
                                       im_initial_value = 0 ).

That is quite a few classes and interfaces! Given the poor support for generics and the fact that every function composition requires an interface and a class, any extensive use of these types of compositions leads to an explosion in the number of types that need to be defined and implemented.

Conclusion

The above demonstrates that while higher-order methods are possible, the support for different types of uses of higher-order functions in ABAP varies. Accepting and returning functions is something that you can easily do. Creating new functions from existing ones, in contrast, is arduous and combines poorly with ABAP’s type system. Creating composites or decorators would be simple, but this is because those patterns, by definition, deal with instances of the same exact type; but even here, the decorators and composites cannot be ad-hoc or defined in a single method, but have to instead be either global or visible in a report or class scope.

Higher-order methods are probably always useful to a degree, but they do not exist in a vacuum: what also matters is how they combine and interact with the rest of the language. In languages with dynamic types (JavaScript, Erlang), first-class functions combine with generic lists and other stuff. In languages with static types (Swift, Haskell), first-class functions combine with type inference and generics to allow for somewhat the same type of convenience. In ABAP, the lack of first-class functions combines with the partial support for generics in a way that makes higher-order functions less useful and causes an explosion in the number of types that need to be defined and implemented.

Bonus: Sum Types in ABAP

Fun fact: ABAP did not have actual enum types for the longest time. However, many values in SAP ERP/ECC – especially customizable values – more or less act like enums: for example, a purchase order type can be one of a set of allowed values and a purchase order is just a purchasing document of a specific category. While these values are enums in the database schema, to the ABAP compiler they are just character or number fields, which means that any checks for allowed values need to be implemented explicitly (excluding screens, which can do some of this automagically).

Actual enums in modern ABAP work as one would expect.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
TYPES: BEGIN OF ENUM true_false,
         true,
         false,
       END OF ENUM true_false.


DATA lv_true_or_false TYPE true_false VALUE true.

CASE lv_true_or_false.

  WHEN true.
  
  WHEN false.

ENDCASE.


" Causes a compilation error:
" "ABAP_TRUE" and "lv_true_or_false" cannot be compared.
ASSERT lv_true_or_false = abap_true.

That is, if you expect enums to be just simple values that can be compared within the enum type but do little to nothing extra – somewhat like atoms in Erlang and Prolog, except comparisons can only happen among members of the ABAP enum type.

Languages like Swift and Haskell extend the basic idea of enums to so-called “sum types”. Typical example of a sum type is a Maybe type. A function that might fail for certain values can return a Maybe to represent that it may or may not return some type of result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-- We'll ignore the fact that reading a production order from the database
-- is a non-pure function.
type ProductionOrderNumber = String
data ProductionOrder = ProductionOrder { orderNumber :: ProductionOrderNumber } deriving Show

readProductionOrder :: ProductionOrderNumber -> Maybe ProductionOrder
readProductionOrder orderNumber =
    case orderNumber of
      "1" -> Just ProductionOrder { orderNumber = "1" }
      _   -> Nothing

Traditionally in ABAP, the equivalent would be represented by throwing an exception from a method (thus returning no actual value from the method). Or in BAPIs, which have special rules for their interfaces, this type of failure would be represented by returning empty tables and structures, along with an error message.

Interestingly, Cook (2009, Section 5.2) notes that classes can be used to simulate sum types in ABAP, meaning we can create our own Maybe -type.

 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
52
53
54
55
56
57
58
59
60
61
CLASS zcl_maybe DEFINITION.
ENDCLASS.

CLASS zcl_maybe IMPLEMENTATION.
ENDCLASS.


CLASS zcl_just DEFINITION INHERITING FROM zcl_maybe.

  PUBLIC SECTION.
    DATA value TYPE i.

ENDCLASS.

CLASS zcl_just IMPLEMENTATION.
ENDCLASS.


CLASS zcl_nothing DEFINITION INHERITING FROM zcl_maybe.
ENDCLASS.

CLASS zcl_nothing IMPLEMENTATION.
ENDCLASS.


CLASS class DEFINITION.

  PUBLIC section.
    CLASS-METHODS method
      IMPORTING io_maybe TYPE REF TO zcl_maybe.

ENDCLASS.

CLASS class IMPLEMENTATION.

  METHOD method.
  
    TRY.
        DATA(lo_just) = CAST zcl_just( io_maybe ).
      CATCH cx_sy_move_cast_error.
    ENDTRY.
    
    TRY.
        DATA(lo_nothing) = CAST zcl_nothing( io_maybe ).
      CATCH cx_sy_move_cast_error.
    ENDTRY.
    
    
    CASE io_maybe.
    
      WHEN lo_just.
        " Do something with lo_just->value.
    
      WHEN lo_nothing.
        " Do nothing
    
    ENDCASE.
  
  ENDMETHOD.

ENDCLASS.

It is a bit clunky, given that ABAP also does not support pattern matching. Here, too, ABAP’s type system makes things worse: since ABAP does not support generic types, we must either have a Maybe -type for every type we want to return, or give up type safety and make the Maybe type generic in the way ABAP allows (TYPE string, TYPE REF TO data, TYPE REF TO object). The latter would have the downside that all methods that return a Maybe would have the same return parameter signature RETURNING ... TYPE REF TO zcl_maybe, whereas in the Haskell example we had -> Maybe ProductionOrder.

Again, features of a language combine to make different ways of programming feel natural – or unnatural.

Sources

  • Cook, W. R. (2009) On Understanding Data Abstraction, Revisited. OOPSLA 2009, Orlando, Florida, October 25–29, 2009.