I had taken arrays for granted until a programming class I attended had an exam question to the effect of “Why do programming languages have arrays?”

Even if you have not previously thought it through, the answer is fairly intuitive: arrays allow programs to generalize to inputs and problems of varying sizes.

Collection Types

Arrays, or “collection types” more generally, exist in all modern programming languages. In fact, it is likely that a programming language has an extensive catalog of collection types with different properties:

  • Arrays have a fixed size, use numeric keys (indexes), allow any element to be quickly accessed but changing the size of an array is expensive because new memory needs to be allocated and the contents of the array must be copied.
  • Lists are cheap to grow or shrink, but random access to members is slow.
  • Sorted lists allow random members to be accessed faster, while making the addition and removal of members slower.
  • Stacks are like lists, but elements are inserted and removed using Last In-First Out (LIFO) rule.
  • Dictionaries (AKA maps or associative arrays) optimize for access with a known key, but enumerating over the members of the collection will happen in an undefined order.
  • Ordered dictionaries preserve the fast access with a known key and add a defined order of element enumeration.
  • Sets optimize for testing whether a given value is a member of the set.

Most of these collection types are generic in the sense that the properties of the collection hold regardless of the type of value they contain. Of the exceptions, sorted lists impose the obvious restriction that it must be possible to compare elements amongst themselves, and dictionaries (likely) require that the key type can be “hashed” (ie. mapped to a type that the dictionary implementation uses internally, and for which it still holds that each key value maps to a unique value of the internal type).

Because collection types are generic, they are often implemented using generics, meaning that the collection type is implemented once and then reused as required with any compatible type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Array in Swift is completely generic, with no restrictions on the elements
struct Array<Element>

let materialNumbers: Array<String> = ["3G", "4G", "5G"]


struct MaterialMaster {
    let MaterialNumber: String
    let MaterialType: String
}

let materialMasters: [MaterialMaster] = [
    MaterialMaster(MaterialNumber: "3G", MaterialType: "KMAT"),
    MaterialMaster(MaterialNumber: "4G", MaterialType: "DIEN"),
    MaterialMaster(MaterialNumber: "5G", MaterialType: "DIEN"),
]


// Dictionary in Swift is completely generic with
// regards to Values, but requires that Keys can be hashed
struct Dictionary<Key, Value> where Key : Hashable

Dynamic languages may impose some restrictions on the key type of associative arrays (must be hashable, for example), but the value types are usually a free-for-all, even to the point that a collection may contain multiple different 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
% Erlang has dynamic types, and does not require
% that collection values (or keys for that matter)
% are the same type.

% Lists can contain values of any type
List = [16#a, 10.0, a, "a", <<"a">>, self()],


% Maps (Dictionaries) can contain values of any type,
% and any type can be used as a key.
Ref = make_ref(),

Map = #{
  Ref  => value,
  key  => "value",
  16#a => <<"value">>,
  10.0 => Ref
},


% Property lists (AKA proplists, associative arrays
% before Erlang had a map type) do not restrict
% key or value types, either.

[
  {10.0, Ref},
  {16#a, <<"value">>},
  {key, "value"},
  {Ref, value}
] = lists:sort(maps:to_list(Map)).

ABAP, however, differs from most programming languages when it comes to collection types.

ABAP Collections

One of the distinctive features of ABAP is that the only built-in collection type is the list, which is officially called an “internal table” and which will be henceforth referred to as a “table type” in this post. (Were I forced to speculate, I would guess that the name “internal table” tries to draw a distinction between an in-memory table and a database table, which would probably be called an “external table”)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
DATA lt_material_master TYPE mara_tty.

DATA ls_material_master TYPE mara.

ls_material_master-matnr = '3G'.
INSERT ls_material_master INTO TABLE lt_material_master.

ls_material_master-matnr = '4G'.
INSERT ls_material_master INTO TABLE lt_material_master.


" Will print 3G and 4G
LOOP AT lt_material_master INTO ls_material_master.
  WRITE / ls_material_master-matnr.
ENDLOOP.

Not only that, but table type is not generic: you have to define a type for every data type you need to treat as a collection. This sounds worse than it is and does not actually differ terribly much from other programming languages. The main difference is where and how you can define the table type that you want to use.

For example, in Swift this definition happens by specifying the generic type and the type parameter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func sumOf(those integers: Array<Int>) -> (Int, Array<Int>) {
    let copyOfThoseIntegers: Array<Int> = integers
    
    struct StructOfThoseIntegers {
        let thoseIntegers: Array<Int>
    }
    
    let structOfThoseIntegers = StructOfThoseIntegers(thoseIntegers: integers)
  
    let sum = integers.reduce(0) {
        $0 + $1
    }
    
    return (sum, integers)
}


let integers: Array<Int> = [1, 2, 3]

sumOf(those: integers)

Array<Int> works everywhere, be it declaring a variable (in any scope), parameter, member of a structure or the return value of a function.

In ABAP, you can sometimes create a table by just referring to the contained data type (as one does in languages with generic types). In other cases you must have an already defined data type available:

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

  PUBLIC SECTION.
    TYPES: BEGIN OF ty_s_sum_of,
             sum       TYPE i,
             t_integer TYPE zinteger_tty,
           END OF ty_s_sum_of.
    " Table types must be defined before they can be
    " used as method parameters.
    "
    " Writing
    " IMPORTING it_integer TYPE STANDARD TABLE OF i
    " would be a syntax error. Likewise with RETURNING parameters.
    CLASS-METHODS sum_of
      IMPORTING it_integer TYPE zinteger_tty
      RETURNING VALUE(re_result) TYPE ty_s_sum_of.

ENDCLASS.

CLASS class IMPLEMENTATION.

  METHOD sum_of.

    LOOP AT it_integer INTO DATA(lv_integer).
      ADD lv_integer TO re_result-sum.
    ENDLOOP.

    re_result-t_integer = it_integer.
  
  ENDMETHOD.

ENDCLASS.


START-OF-SELECTION.
  " The more convenient way to declare tables is available
  " when declaring variables and class attributes. You only
  " need to specify the contained type:
  DATA lt_integer TYPE STANDARD TABLE OF i.
  
  DO 10 TIMES.
    INSERT sy-index INTO TABLE lt_integer.
  ENDDO.
  
  class=>sum_of( it_integer = lt_integer ).

However, depending on how the ZINTEGER_TTY type was defined, the above may not compile:

ABAP compilation error popup with text: "LT_INTEGER" is not type-compatible with formal parameter "IT_INTEGER"

Incompatible tables

Which brings us to the topic of different types of table types.

Types of Table Types

The most obvious way that two table types could be incompatible is, of course, that the types that they contain are different. It does not make sense that a table of integers would work in places that expect a table of strings. This also applies to ABAP.

In our example, however, two tables of integers are incompatible. How can two tables containing integers still be different? The answer is that not all table types in ABAP are the same; the table type definition contains more than just the contained type:

Table types

In truth, table types in ABAP can be STANDARD, SORTED or HASHED tables. These correspond roughly to a List, a Sorted List and a Dictionary; that is, the two latter options provide additional guarantees as to the order of elements and uniqueness of keys. These guarantees also cause STANDARD, SORTED and HASHED table types to be incompatible when used as parameter types: it would not make sense to allow a STANDARD table, with potentially multiple instances of the same key, to be used in a parameter that required a HASHED table, where every key only occurs once.

The next obvious question is: “Keys? Since when have Lists had keys?” And in this regard, too, ABAP is unique. Every table type in ABAP has an explicitly or implicitly defined key:

Table keys

In case you are wondering what a “standard key” could possibly mean, here is the definition:

Standard key definition

The key fields of the standard key are defined as follows:

  • In tables with a structured row type, the standard key is formed from all components with character-like and byte-like data types, with any substructures being expanded by elementary components. If the row type does not contain components like these, the standard key is empty for standard tables, meaning it does not contain any key fields.
  • The standard key for tables with non-structured row types is the entire table row, if the row type itself is not table-like. If the row type is table-like, the standard key is empty for standard tables.

Empty standard keys are not possible for sorted tables and hashed tables, and an error occurs if an attempt is made to create a key like this.

In other words, we would have had to define our table type variable such that it was compatible with the parameter type:

1
2
3
4
5
6
7
8
" We previously used:
"DATA lt_integer TYPE STANDARD TABLE OF i.

" Which actually meant
"DATA lt_integer TYPE STANDARD TABLE OF i WITH DEFAULT KEY.

" While the parameter was of type
DATA lt_integer TYPE SORTED TABLE OF i WITH NON-UNIQUE KEY table_line.

Table keys can also be specified when defining the table type in ABAP dictionary:

Table type primary key

The list titled “Key component” makes more sense when the table type is defined for a structure:

Structure type (ZSTRUCTURE)

Table type for structure type (ZSTRUCTURE_TTY)

Accessing SORTED and HASHED tables by the table key should be faster than when using a STANDARD table. However, this performance gain seems to only manifest with huge tables while all three are more or less the same when the number of rows is small.

The presence of keys and the fact that programmers can decide which fields make up the key introduces another potential source for table type incompatibility. For example, if we have yet another table type with the same key fields in reverse order:

Another table type for structure type

These tables are no longer compatible:

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

  PUBLIC SECTION.
    CLASS-METHODS method
      IMPORTING it_structure TYPE zstructure_tty.

ENDCLASS.

CLASS class IMPLEMENTATION.

  METHOD method.
  ENDMETHOD.

ENDCLASS.


START-OF-SELECTION.
  DATA(lt_structure) = VALUE zstructure_another_tty(
    (
      field1 = 'value1'
      field2 = 'value2'
      field3 = 'value3'
    )
    (
      field1 = 'value4'
      field2 = 'value5'
      field3 = 'value6'
    )
  ).
  
  class=>method( it_structure = lt_structure ).
ABAP compilation error popup with text: "LT_STRUCTURE is not type-compatible with formal parameter "IT_STRUCTURE"

Tables with different keys are incompatible

This makes sense, since different keys imply different orders of entries in the tables. Table types with identical definitions are, however, compatible. The following table type would work anywhere where ZSTRUCTURE_ANOTHER_TTY would work:

This type is compatible with ZSTRUCTURE_ANOTHER_TTY

Somewhat surprisingly, table types for identical structures with differently named components are still compatible. The following structure and table type are compatible with ZSTRUCTURE and table type ZSTRUCTURE_TTY:

Structure type with different component names

Table type for the new structure 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
CLASS class DEFINITION.

  PUBLIC SECTION.
    CLASS-METHODS method
      IMPORTING it_structure TYPE zanother_structure_tty.

ENDCLASS.

CLASS class IMPLEMENTATION.

  METHOD method.
    WRITE 'It works!'.
  ENDMETHOD.

ENDCLASS.


START-OF-SELECTION.
  DATA(lt_structure) = VALUE zstructure_tty(
    (
      field1 = 'value1'
      field2 = 'value2'
      field3 = 'value3'
    )
    (
      field1 = 'value4'
      field2 = 'value5'
      field3 = 'value6'
    )
  ).
  
  class=>method( it_structure = lt_structure ).

This makes a kind of sense, since two tables containing identical structures and ordered according to the same components (as far as the order of components in the structure is considered) will result in the same order of rows (excluding cases where multiple instances of the same key are allowed). Then again, even though the components are technically identical, their different naming would imply that the two structures probably do not represent exactly the same thing, which suggests that the two types should probably be incompatible.

The table key does not only determine table compatibility, but also the implicit sort order. In ABAP, you can sort a table without specifying the fields according to which the sort should happen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
DATA lt_structure TYPE STANDARD TABLE OF zstructure.
lt_structure = VALUE #(
  (
    field1 = 'A'
    field2 = '2'
  )
  (
    field1 = 'B'
    field2 = '1'
  )
).

SORT lt_structure BY field2.
SORT lt_structure.

The latter SORT statement sorts the table according to the table type primary keys, which would be A2 and B1 (contents of all character fields of the row concatenated) in the example. In this case, the result would be fairly sane.

Nevertheless, there is probably never a reason to use the implicit primary key for sorting: it is far more expressive and clear when the fields are written out when sorting a table. And, in cases where the key of a STANDARD table if empty, the SORT table statement will not actually even do anything. If the order of entries in a table really matters, it is probably better to use a SORTED table which will then always enforce the correct order, rather than rely on the programmer sorting the table after rows are manipulated.

Dangerous ABAP Statements

The craziest thing about the different ABAP table type types is that, depending on the type, only some table modification statements are safe to use.

For example, APPEND statement adds one or more new rows as the last row of the table. This works without issues when used with STANDARD tables, since elements can be freely added anywhere as there is no defined order, but will crash if used with SORTED tables:

1
2
3
4
5
START-OF-SELECTION.
  DATA lt_sorted TYPE SORTED TABLE OF i WITH NON-UNIQUE KEY table_line.
  
  APPEND 2 TO lt_sorted. " This works
  APPEND 1 TO lt_sorted. " This crashes, since 1 cannot appear after 2
ABAP short dump with the following cause: ITAB_ILLEGAL_SORT_ORDER - Error while inserting or changing rows in a sorted table

APPEND crashes when the order of rows is incorrect

INSERT statement works with all table types, except when trying to insert multiple rows with identical keys into a table with a unique key constraint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
START-OF-SELECTION.
  DATA lt_sorted TYPE SORTED TABLE OF i WITH NON-UNIQUE KEY table_line.

  INSERT 2 INTO TABLE lt_sorted.
  INSERT 1 INTO TABLE lt_sorted.
  
  
  DATA lt_hashed TYPE HASHED TABLE OF i WITH UNIQUE KEY table_line.
  
  INSERT 2 INTO TABLE lt_hashed.
  INSERT 1 INTO TABLE lt_hashed.
  
  
  DATA lt_integer TYPE STANDARD TABLE OF i.
  lt_integer = VALUE #( ( 1 ) ( 2 ) ).
  
  INSERT LINES OF lt_integer INTO TABLE lt_hashed. " This crashes
ABAP short dump with the following cause: ITAB_DUPLICATE_KEY - A row already exists with this key

INSERT crashes when the operation would result in duplicate keys

Implicit conversion between table types can also crash for the same reason:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
START-OF-SELECTION.
  DATA lt_integer TYPE STANDARD TABLE OF i.
  
  INSERT 2 INTO TABLE lt_integer.
  INSERT 1 INTO TABLE lt_integer.
  INSERT 1 INTO TABLE lt_integer.


  DATA lt_sorted TYPE SORTED TABLE OF i WITH NON-UNIQUE KEY table_line.

  lt_sorted = lt_integer. " This works, and the rows will be in the correct order


  DATA lt_hashed TYPE HASHED TABLE OF i WITH UNIQUE KEY table_line.

  lt_hashed = lt_integer. " This crashes
ABAP short dump with the following cause: ITAB_DUPLICATE_KEY - A row already exists with this key

Conversion crashes when the operation would result in duplicate keys

Key fields are also protected from changes in SORTED and HASHED tables:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
START-OF-SELECTION.
  DATA lt_structure TYPE HASHED TABLE OF zstructure WITH UNIQUE KEY field1.
  lt_structure = VALUE #(
    (
      field1 = 'value1'
    )
    (
      field1 = 'value2'
    )
  ).
  
  LOOP AT lt_structure REFERENCE INTO DATA(lsr_structure).
    lsr_structure->field1 = 'newValue'.
  ENDLOOP.
ABAP short dump with the following cause: MOVE_TO_LIT_NOTALLOWED_NODATA - Assignment error: Overwriting a protected field

Key field are write-protected

Tables of Classes and Interfaces

Classes and interfaces add more interesting cases for table type (in)compatibility. A table of class instances is compatible with a table of interface instances if that class implements the interface:

 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
INTERFACE interface.
ENDINTERFACE.

CLASS class DEFINITION.

  PUBLIC SECTION.
    INTERFACES interface.

ENDCLASS.

CLASS class IMPLEMENTATION.
ENDCLASS.


CLASS do DEFINITION.

  PUBLIC SECTION.
    TYPES interface_tty TYPE STANDARD TABLE OF REF TO interface.
    CLASS-METHODS something
      IMPORTING it_instance TYPE interface_tty.

ENDCLASS.

CLASS do IMPLEMENTATION.

  METHOD something.
    WRITE 'It works!'.
  ENDMETHOD.

ENDCLASS.


START-OF-SELECTION.
  DATA lt_instance TYPE STANDARD TABLE OF REF TO class.
  lt_instance = VALUE #(
    ( NEW class( ) )
    ( NEW class( ) )
  ).
  
  do=>something( it_instance = lt_instance ).

Unfortunately this does not seem to work outside of classes:

 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 interface.
ENDINTERFACE.

CLASS class DEFINITION.

  PUBLIC SECTION.
    INTERFACES interface.

ENDCLASS.

CLASS class IMPLEMENTATION.
ENDCLASS.


TYPES interface_tty TYPE STANDARD TABLE OF REF TO interface.

FORM do_something USING it_instance TYPE interface_tty.
  WRITE 'It does not work'.
ENDFORM.


START-OF-SELECTION.
  DATA lt_instance TYPE STANDARD TABLE OF REF TO class.
  lt_instance = VALUE #(
    ( NEW class( ) )
    ( NEW class( ) )
  ).
  
  PERFORM do_something USING lt_instance.

Table of class instances implementing the interface does not work

Generic Table Types

Some interesting things to note about the ABAP dictionary table type definition screenshots were the generic options for table type (“Index Table” and “Not specified”) and for table key (“Not Specified” for key definition and key category). These types of tables cannot be used when defining variables:

Generic table type

1
2
START-OF-SELECTION.
  DATA lt_structure TYPE zstructure_generic_tty.
ABAP compilation error popup with text: "ZSTRUCTURE_GENERIC_TTY" is a generic type. Use this only for defining field symbols and formal parameters.

Generic table type error

Rather, these generic table types can be used to restrict allowed parameter types, without specifying exactly what type of table is expected. The most lenient table type (“Not Specified”) would just specify what sort of data the table contains, while allowing the actual type to be a STANDARD, SORTED or HASHED table. “Index Table” would only allow STANDARD and SORTED tables.

Leaving the key generic would allow different types of STANDARD, SORTED or HASHED tables to a be used as the actual parameter.

Of note is that the type definition STANDARD TABLE OF x stands for either a generic table definition (when used in a parameter, or rather, when used in a TYPES declaration), or a table type with an implicit standard key (when used in a variable definition, ie. when used in a DATA declaration):

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

  PUBLIC SECTION.
    TYPES ty_sorted_table_of_zstructure TYPE SORTED TABLE OF zstructure.
    TYPES ty_hashed_table_of_zstructure TYPE HASHED TABLE OF zstructure.
    TYPES ty_index_table_of_zstructure TYPE INDEX TABLE OF zstructure.
    TYPES ty_any_table_of_zstructure TYPE ANY TABLE OF zstructure.
  
  
    TYPES ty_std_table_of_zstructure TYPE STANDARD TABLE OF zstructure.
    
    METHODS method
      IMPORTING it_table TYPE ty_std_table_of_zstructure. " Generic table type

ENDCLASS.

CLASS class IMPLEMENTATION.

  METHOD method.

    " This is actually TYPE STANDARD TABLE OF zstructure WITH DEFAULT KEY
    DATA lt_structure TYPE ty_std_table_of_zstructure.
    
    " None of the other table types can be used to define a variable.

  ENDMETHOD.

ENDCLASS.

Parameters can also be specified as a table of some kind without even specifying the type the table contains, but this is so generic as to be virtually useless and is only maybe useful in highly specific situations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
CLASS class DEFINITION.

  PUBLIC SECTION.
    METHODS method
      IMPORTING it_standard_table TYPE TABLE
                it_sorted_table   TYPE SORTED TABLE
                it_hashed_table   TYPE HASHED TABLE
                it_index_table    TYPE INDEX  TABLE
                it_any_table      TYPE ANY    TABLE.

ENDCLASS.

CLASS class IMPLEMENTATION.

  METHOD method.
  ENDMETHOD.

ENDCLASS.

It is good to keep in mind that generic parameters always have the potential to transform type incompatibilities from compilation errors into runtime errors (previously mentioned in Function Modules, Methods and Type Safety). In the following example, the parameter type ty_index_table_of_zstructure allows for STANDARD and SORTED tables, whereas parameter type ty_std_table_of_zstructure explicity requires a STANDARD table with default key. The code compiles, because the generic parameter might contain the correct type of table, but will crash because the actual types are incompatible at runtime:

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

  PUBLIC SECTION.
    TYPES ty_index_table_of_zstructure TYPE INDEX TABLE OF zstructure.
    CLASS-METHODS method
      IMPORTING it_generic_structure TYPE ty_index_table_of_zstructure.

  PRIVATE SECTION.
    TYPES ty_std_table_of_zstructure TYPE STANDARD TABLE OF zstructure WITH DEFAULT KEY.
    CLASS-METHODS another_method
      IMPORTING it_structure TYPE ty_std_table_of_zstructure.

ENDCLASS.

CLASS class IMPLEMENTATION.

  METHOD method.
    another_method( it_structure = it_generic_structure ). " This will crash
  ENDMETHOD.
  
  METHOD another_method.
  ENDMETHOD.

ENDCLASS.


START-OF-SELECTION.
  DATA lt_structure TYPE SORTED TABLE OF zstructure WITH NON-UNIQUE KEY field1.
  
  class=>method( it_generic_structure = lt_structure ).
ABAP short dump with the following cause: CALL_METHOD_CONFLICT_TYPE - Type conflict in method call.

Generic type masks the actual parameter and crashes when executed

Implementing Sets

Although ABAP does not have built-in Set type, the most rudimentary operations can be easily simulated with a HASHED table:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
DATA lt_set_of_integers TYPE HASHED TABLE OF i WITH UNIQUE KEY table_line.

" Add a member
INSERT 1 INTO TABLE lt_set_of_integers.

" Test for membership
IF line_exists( lt_set_of_integers[ table_line = 1 ] ).
  WRITE 'Is a member of the set'.
ENDIF.

" Remove a member
DELETE lt_set_of_integers WHERE table_line = 1.


DATA lt_another_set TYPE HASHED TABLE OF i WITH UNIQUE KEY table_line.

" Union of two sets
LOOP AT lt_another_set INTO DATA(lv_integer).
  INSERT lv_integer INTO TABLE lt_set_of_integers.
ENDLOOP.

Ranges

Ranges, AKA. Select-Options, are special tables that represent conditions for selecting data from the database. For example, if we wanted to select IDocs in some specific data range, we could use ranges to do this:

 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
" RANGE is a shortcut for creating...
DATA lr_created_on TYPE RANGE OF edi_ccrdat.

" ...tables of this type
TYPES: BEGIN OF lty_s_date_range,
         sign   TYPE c LENGTH 1, " I(nclude) or E(xclude)
         option TYPE c LENGTH 2, " EQ(ual), L(ess)T(han), G(reater)T(han)...
         low    TYPE edi_ccrdat,
         high   TYPE edi_ccrdat,
       END OF lty_s_date_range.
DATA lt_created_on TYPE STANDARD TABLE OF lty_s_date_range.

lt_created_on = VALUE #(
  (
    sign   = 'I'
    option = 'BT' " Between, inclusive
    low    = '20250101'
    high   = '20250102')
).

lr_created_on = lt_created_on.


" This will select the same rows...
SELECT *
  FROM edidc
  INTO TABLE @DATA(lt_idoc_control_record)
 WHERE credat IN @lr_created_on.

" ...as this
SELECT *
  FROM edidc
  INTO TABLE @lt_idoc_control_record
 WHERE credat >= '20250101'
   AND credat <= '20250102'.

We could also use ranges to only select IDocs created on a specific date:

1
2
3
4
5
6
7
DATA lr_created_on TYPE RANGE OF edi_ccrdat.
lr_created_on = VALUE #( ( sign = 'I' option = 'EQ' low = '20250101' ) ).

SELECT *
  FROM edidc
  INTO TABLE @lt_idoc_control_record
 WHERE credat IN @lr_created_on.

Ranges can represent many different sorts of conditions, and also combinations of these different condition types.

Type Inference

In recent versions of ABAP, table types can also be inferred in certain cases. This is especially useful when accessing the database:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SELECT vbeln AS sales_order,
       posnr AS sales_order_item,
       matnr AS material
  FROM vbap
  INTO TABLE @DATA(lt_sales_order_item)
 WHERE vbeln = @lv_sales_order.

LOOP AT lt_sales_order_item INTO DATA(ls_sales_order_item).
  WRITE / ls_sales_order_item-material.
  
  " This is a syntax error, since our inferred structure ls_sales_order_item
  " does not contain field matwa because it was not part of the
  " field list we used in our SELECT.
  WRITE ls_sales_order_item-matwa.
ENDLOOP.

Function Modules and TABLES Parameters

Another peculiarity of ABAP is that, in addition to USING, CHANGING, IMPORTING, EXPORTING and RETURNING parameters, there are also TABLES parameters:

1
2
3
4
5
DATA lt_structure TYPE STANDARD TABLE OF zstructure.

CALL FUNCTION 'ZFUNCTION_MODULE'
  TABLES
    it_structure = lt_structure.

I suspect there are two reasons for the existence of TABLES parameters:

  1. Tables in ABAP are value types, meaning that passing a table around requires copying it and all contained rows. Handling tables efficiently (= without unnecessary copying) as parameters may have required optimizations that were easier to implement when tables had their own parameters.
  2. Tables parameters allow for syntax where only the contained value needs to be specified, allowing for tables to be passed as parameters without having a global table type defined.

The first argument is obsolete, since IMPORTING and CHANGING (and also EXPORTING) parameters achieve similar performance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
DATA lt_structure TYPE STANDARD TABLE OF zstructure.

DO pa_tsize TIMES.
  INSERT VALUE #( field1 = sy-index ) INTO TABLE lt_structure.
ENDDO.


DATA lv_start TYPE timestampl.
DATA lv_end TYPE timestampl.

GET TIME STAMP FIELD lv_start.
DO pa_nloop TIMES.
  CALL FUNCTION 'ZFUNCTION_MODULE'
    EXPORTING
      it_structure = lt_structure.
ENDDO.
GET TIME STAMP FIELD lv_end.

" And similar cases for other EXPORTING, TABLES and CHANGING
 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
FUNCTION ZFUNCTION_MODULE
  IMPORTING
    IT_STRUCTURE TYPE ZSTRUCTURE_STANDARD_TTY OPTIONAL
  EXPORTING
    ET_STRUCTURE TYPE ZSTRUCTURE_STANDARD_TTY
  CHANGING
    CT_STRUCTURE TYPE ZSTRUCTURE_STANDARD_TTY OPTIONAL
  TABLES
    T_STRUCTURE LIKE ZSTRUCTURE OPTIONAL.


  IF it_structure IS SUPPLIED.
    LOOP AT it_structure ASSIGNING FIELD-SYMBOL(<row>).
    ENDLOOP.
  ENDIF.


  IF et_structure IS REQUESTED.
    LOOP AT et_structure ASSIGNING <row>.
    ENDLOOP.
  ENDIF.


  IF ct_structure IS SUPPLIED.
    LOOP AT ct_structure ASSIGNING <row>.
    ENDLOOP.
  ENDIF.


  IF t_structure IS SUPPLIED.
    LOOP AT t_structure ASSIGNING <row>.
    ENDLOOP.
  ENDIF.


ENDFUNCTION.

TABLES parameters are not especially faster or slower than others

The second argument still holds; passing tables using the other parameters requires a globally defined table type, whereas our TABLES parameter refers only to the contained type.

This introduces the following question: since we only the specify the contained type and not the table type, what sort of table is a TABLES parameter, actually. Can we pass a SORTED table into a TABLES parameter?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
DATA lt_structure TYPE SORTED TABLE OF zstructure WITH NON-UNIQUE KEY field1.
lt_structure = VALUE #(
  ( field1 = 'A' )
  ( field1 = 'B' )
  ( field1 = 'C' )
).

CALL FUNCTION 'ZFUNCTION_MODULE'
  TABLES
    t_structure = lt_structure.
ABAP compilation error popup with text: "LT_STRUCTURE" is not a "STANDARD TABLE". Non-standard tables can only be passed to IMPORTING, EXPORTING, or CHANGING parameters. parameters.

Clearly not ("parameters. parameters." is always a nice touch, though)

ABAP documentation also tells us that TABLES parameters are standard tables, but does not go into more detail. It also mentions that “Pass by value is not possible,” which aligns with my previous speculation about tables and performance optimizations:

Defines the table parameters t1 t2 … in the function module interface display in the source code of function modules. Table parameters are obsolete CHANGING parameters that are typed as standard tables with a header line. If an internal table without a header line or a table body is passed as an actual parameter to a formal parameter of this type, an empty local header line is generated in the function module. If an internal table with a header line is used as an actual parameter, both the table body and the header line are passed to the function module. Pass by value is not possible in formal parameters defined using TABLES.

Trying out different types of STANDARD tables inside and outside the function module gives us a bit more information:

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

  PUBLIC SECTION.
    TYPES ty_t_zstructure_default_key TYPE STANDARD TABLE OF zstructure WITH DEFAULT KEY.
    TYPES ty_t_zstructure_empty_key TYPE STANDARD TABLE OF zstructure WITH EMPTY KEY.
    CLASS-METHODS do_something
      IMPORTING it_structure TYPE ty_t_zstructure_default_key.
    CLASS-METHODS do_Something_else
      IMPORTING it_structure TYPE ty_t_zstructure_empty_key.

ENDCLASS.

CLASS class IMPLEMENTATION.

  METHOD do_something.
  ENDMETHOD.

  METHOD do_something_else.
  ENDMETHOD.

ENDCLASS.

FUNCTION ZANOTHER_FUNCTION_MODULE.
*"----------------------------------------------------------------------
*"*"Local Interface:
*"  TABLES
*"      T_STRUCTURE STRUCTURE  ZSTRUCTURE OPTIONAL
*"----------------------------------------------------------------------

  class=>do_something( t_structure[] ).

  " This line causes a '"T_STRUCTURE" is not type-compatible
  " with formal parameter "IT_STRUCTURE".' compilation error, suggesting
  " that the the TABLES parameters appears to be a STANDARD TABLE
  " WITH DEFAULT KEY inside the function module.
  "class=>do_something_else( t_structure[] ).

ENDFUNCTION.

So inside the function module, the TABLES parameters appears to be a STANDARD table with default key, as far as the ABAP compiler is concerned. This requirement, however, does not apply outside the function module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DATA lt_standard_table_empty_key TYPE STANDARD TABLE OF zstructure WITH EMPTY KEY.

CALL FUNCTION 'ZFUNCTION_MODULE'
  TABLES
    t_structure = lt_standard_table_empty_key.


DATA lt_standard_table_random_key TYPE STANDARD TABLE OF zstructure WITH NON-UNIQUE KEY field1.

CALL FUNCTION 'ZFUNCTION_MODULE'
  TABLES
    t_structure = lt_standard_table_random_key.

This snippet compiles (obviously, since function module parameter types are not checked when compiled) and also executes without issues. So while the type of the table inside the function module appears to a STANDARD TABLE OF zstructure WITH DEFAULT KEY, we can actually use any sort of STANDARD table as the parameter.

So which is the actual type? The one we actually pass into the function module or the type that the TABLES parameter appears to be inside the function module?

1
2
3
4
5
DATA lt_standard_table_empty_key TYPE STANDARD TABLE OF zstructure WITH EMPTY KEY.

CALL FUNCTION 'ZANOTHER_FUNCTION_MODULE'
  TABLES
    t_structure = lt_standard_table_empty_key.
ABAP short dump with the following cause: CALL_METHOD_CONFLICT_TYPE - The method call for method "DO_SOMETHING" of class "CLASS" contains errors. An attempt was made to pass an incompatible variable to formal parameter "IT_STRUCTURE".

Function Module masks the actual type

At least when the function module is executed in the same session (function modules called via RFC may behave differently), the actual type is the table type that the caller used in the function module call. This is somewhat similar to cases where generic types mask type incompatibility which were mentioned earlier, but with a slight twist. Instead of the table inside the function module being a generic type which would be compatible with any STANDARD table parameter type, the TABLES parameter explicitly appears as a STANDARD TABLES OF x WITH DEFAULT KEY.

Clearly, a TABLES parameter can hide issues with incompatible types during compilation. Yet there’s also a sort-of solution on offer: given that any function module that compiles can only contain method calls that expect parameters typed STANDARD TABLE OF x (generic STANDARD table) or STANDARD TABLE OF x WITH DEFAULT KEY, using a table declared as DATA lt_table TYPE STANDARD TABLE OF x [WITH DEFAULT KEY] will always work, since any call inside the function module will have to work with a table of that type.

Some Final Thoughts

Although tables are fundamental building blocks that appear multiple times in most ABAP programs, they still hide details that one might not know about. For example, the fact that STANDARD TABLE OF x means different things in DATA vs. TYPES is easy to overlook, as is the exact type of a TABLES parameter.