Modern ABAP Syntax: VALUE, FOR, REDUCE, COND, Table Expressions
1. Why modern syntax?
Since ABAP 7.40 (2013) the language has a shorter, expression-based style. Compared with classical ABAP it gives you:
- Fewer lines, with no long
DATAlist at the top. - Fewer errors, because values are created where they are used.
- A functional feel: describe the result, not every step.
Most of this blog needs ABAP 7.40 or newer. A few operators (FILTER, xsdbool) need 7.40 SP08. Run System > Status in SAP GUI to see your version. Every example has a classical equivalent if you are on an older system.
2. Inline declarations
Declare a variable at the place of its first use with DATA( ). ABAP derives the type from the value.
" Classical
DATA lv_date TYPE d.
lv_date = sy-datum.
" Modern
DATA(lv_date) = sy-datum.
" Other places inline declarations work
SELECT vbeln, erdat FROM vbak INTO TABLE @DATA(lt_vbak) UP TO 10 ROWS.
LOOP AT lt_vbak INTO DATA(ls_vbak). ENDLOOP.
LOOP AT lt_vbak ASSIGNING FIELD-SYMBOL(<ls>). ENDLOOP.
READ TABLE lt_vbak INTO DATA(ls_row) INDEX 1.
CALL METHOD lo_obj->get( IMPORTING ev_value = DATA(lv_val) ).
An inline variable is visible from its declaration to the end of the method, form, or event block. You cannot declare the same name twice in that block, even inside a loop.
3. VALUE: build structures and tables
VALUE creates a structure or internal table in one expression. Use the type name, or # to let ABAP infer it from the target.
" Structure
DATA(ls_order) = VALUE ty_order( vbeln = '0000000001' erdat = sy-datum ).
" Table with rows: each row in its own brackets
DATA(lt_orders) = VALUE ty_order_t(
( vbeln = '0000000001' erdat = '20261001' netwr = 500 )
( vbeln = '0000000002' erdat = '20261002' netwr = 300 ) ).
" Empty table (also the way to clear a table)
lt_orders = VALUE #( ).
" Keep the old rows and add one
lt_orders = VALUE #( BASE lt_orders ( vbeln = '0000000003' ) ).
" Inline in an APPEND
APPEND VALUE #( vbeln = '0000000004' ) TO lt_orders.
- Perfect for test data, constants tables, and initial values.
- Without
BASE, the new rows replace all existing rows.
4. NEW: create objects
DATA(lo_report) = NEW lcl_report( ). " object of a class
DATA lo_other TYPE REF TO lcl_report.
lo_other = NEW #( ). " type inferred from target
DATA(lr_table) = NEW ty_order_t( ). " reference to a new table
Some classes, such as CL_SALV_TABLE, hide their constructor and give you a factory method instead (Blog 15). Use NEW only for classes that allow public instantiation.
5. COND and SWITCH
Both return a value, so you can use an IF-like decision inside an assignment.
" COND: based on conditions (like IF)
DATA(lv_status) = COND string( WHEN sy-subrc = 0 THEN 'OK' ELSE 'ERROR' ).
DATA(lv_size) = COND string(
WHEN ls_order-netwr > 10000 THEN 'Large'
WHEN ls_order-netwr > 1000 THEN 'Medium'
ELSE 'Small' ).
" SWITCH: based on one value (like CASE)
DATA(lv_text) = SWITCH string( ls_order-vbtyp
WHEN 'A' THEN 'Inquiry'
WHEN 'B' THEN 'Quotation'
WHEN 'C' THEN 'Order'
ELSE 'Other' ).
COND | SWITCH | |
|---|---|---|
| Like | IF ... ELSEIF | CASE |
| Tests | Any logical conditions | One value against several constants |
Without ELSE | Result is the initial value | Raises an exception if nothing matches |
Use them for a simple value that depends on a condition. If the branches need several statements, use a normal IF.
6. REDUCE: table to one value
REDUCE walks over a table and produces a single result: a sum, a count, a text.
" Sum
DATA(lv_total) = REDUCE netwr( INIT s = CONV netwr( 0 )
FOR ls IN lt_orders
NEXT s = s + ls-netwr ).
" Count with a condition
DATA(lv_count) = REDUCE i( INIT c = 0
FOR ls IN lt_orders WHERE ( vbtyp = 'C' )
NEXT c = c + 1 ).
" Join values into one text
DATA(lv_list) = REDUCE string( INIT t = ``
FOR ls IN lt_orders
NEXT t = COND #( WHEN t IS INITIAL THEN ls-vbeln
ELSE |{ t }, { ls-vbeln }| ) ).
INITsets the start value.FORpicks the rows.NEXTsays how to update.- The type before the bracket (
netwr,i,string) is the type of the result. - Replaces a
LOOPplus a helper variable.
7. FOR: table from table
Inside VALUE, FOR builds a new table from an existing one.
" Map: only the order numbers
DATA(lt_vbeln) = VALUE ty_vbeln_t( FOR ls IN lt_orders ( ls-vbeln ) ).
" Filter: only category C rows
DATA(lt_c) = VALUE ty_order_t( FOR ls IN lt_orders
WHERE ( vbtyp = 'C' ) ( ls ) ).
" Transform: build new rows from old ones
DATA(lt_big) = VALUE ty_order_t(
FOR ls IN lt_orders WHERE ( netwr > 1000 )
( vbeln = ls-vbeln
erdat = ls-erdat
netwr = ls-netwr * 2 ) ).
" With the row number
DATA(lt_numbered) = VALUE ty_text_t(
FOR ls IN lt_orders INDEX INTO lv_idx
( |{ lv_idx }: { ls-vbeln }| ) ).
| Need | Use |
|---|---|
| Pick one column | FOR ls IN lt ( ls-field ) |
| Keep matching rows | FOR ls IN lt WHERE ( ... ) ( ls ) |
| Change values | Fill the fields in the row brackets |
| Keep rows of a sorted or hashed table | FILTER (see bonus operators) |
lv_idx in the last example must be declared first as an integer variable. Use FOR to build or reshape a table. Do not use it when every row needs several statements.
8. Table expressions
You met these in Blog 4. Square brackets read a row inside any expression.
DATA(ls_first) = lt_orders[ 1 ]. " by index
DATA(ls_found) = lt_orders[ vbeln = '0000000002' ]. " by key
DATA(lv_date) = lt_orders[ 1 ]-erdat. " one field
lt_orders[ 1 ]-erdat = sy-datum. " write a field
IF line_exists( lt_orders[ vbeln = '0000000009' ] ).
DATA(lv_pos) = line_index( lt_orders[ vbeln = '0000000009' ] ).
ENDIF.
" Safe read: no exception, empty row if missing
DATA(ls_safe) = VALUE #( lt_orders[ vbeln = '0000000009' ] OPTIONAL ).
" Safe read with your own fallback row
DATA(ls_dflt) = VALUE #( lt_orders[ vbeln = '0000000009' ]
DEFAULT ls_fallback ).
If no row matches, a table expression raises CX_SY_ITAB_LINE_NOT_FOUND. It does not set sy-subrc for you to check. Guard it with line_exists( ), OPTIONAL, or TRY ... CATCH.
9. String templates and functions
A string template is text between pipe characters. Anything in curly braces is evaluated and inserted.
DATA(lv_msg) = |Order { lv_vbeln } created on { lv_erdat DATE = USER }|.
|{ lv_amount NUMBER = USER }| " number in the user's format
|{ lv_amount CURRENCY = 'INR' }| " amount with currency decimals
|{ lv_text WIDTH = 20 ALIGN = RIGHT }| " fixed width, right aligned
|{ lv_vbeln ALPHA = OUT }| " remove leading zeros
Templates replace most uses of CONCATENATE ... SEPARATED BY and WRITE ... TO.
String functions
| Function | Result |
|---|---|
to_upper( lv_text ) / to_lower( ) | Changes case |
condense( lv_text ) | Removes extra blanks |
strlen( lv_text ) | Length of the text |
substring( val = lv_text off = 0 len = 5 ) | Part of the text |
replace( val = lv_text sub = 'old' with = 'new' ) | Replaces text |
find( val = lv_text sub = 'abc' ) | Position of the match, or -1 |
contains( val = lv_text sub = 'abc' ) | True or false, for use in IF |
concat_lines_of( table = lt_text sep = ',' ) | Joins a table of texts |
" Splitting uses the SPLIT statement, with an inline target table
SPLIT lv_text AT ',' INTO TABLE DATA(lt_parts).
10. Bonus operators
| Operator | What it does |
|---|---|
CONV type( value ) | Converts a value to another type inside an expression |
CORRESPONDING #( struct ) | Copies fields with the same name between structures or tables |
xsdbool( condition ) | Turns a condition into abap_true or abap_false |
FILTER #( lt WHERE ... ) | Fast row filter. The table needs a sorted or hashed key, or a secondary key. |
LET ... IN | Defines a helper variable inside an expression |
11. Before and after
The same job, written both ways. Goal: list of order numbers, total value, and a text for each category.
Classical
DATA: lt_vbeln TYPE ty_vbeln_t,
ls_vbak TYPE ty_order,
lv_total TYPE netwr,
lv_text TYPE string.
LOOP AT lt_orders INTO ls_vbak.
APPEND ls_vbak-vbeln TO lt_vbeln.
ENDLOOP.
CLEAR lv_total.
LOOP AT lt_orders INTO ls_vbak.
lv_total = lv_total + ls_vbak-netwr.
ENDLOOP.
CASE ls_vbak-vbtyp.
WHEN 'C'. lv_text = 'Order'.
WHEN 'A'. lv_text = 'Quotation'.
WHEN OTHERS. lv_text = 'Other'.
ENDCASE.
Modern
DATA(lt_vbeln) = VALUE ty_vbeln_t( FOR ls IN lt_orders ( ls-vbeln ) ).
DATA(lv_total) = REDUCE netwr( INIT s = CONV netwr( 0 )
FOR ls IN lt_orders
NEXT s = s + ls-netwr ).
DATA(lv_text) = SWITCH string( ls_vbak-vbtyp
WHEN 'C' THEN 'Order'
WHEN 'A' THEN 'Quotation'
ELSE 'Other' ).
The modern version has no helper variables to declare or clear, and each result is created in one place.
12. When modern syntax hurts
- Deep nesting. A
REDUCEwith aCONDinside aFORis hard to read. Rule of thumb: three or more nested expressions, use a classical loop. - Several statements per row. Use
LOOP. - Performance. Modern syntax is usually as fast as the classical code, but not always. Measure big tables (Blog 13).
- Your team. Colleagues who do not know the syntax cannot maintain it. Agree on a style and comment unusual expressions.
13. Try it yourself
- Rewrite the grouping example from Blog 4 with
VALUE,FORandREDUCE. - Build a test table of 5 orders with
VALUE #( ( ... ) ( ... ) ). - Total
netwrwithREDUCE. - Keep only
vbtyp = 'C'rows withFOR ... WHERE. - Join all order numbers with
REDUCEand a string template. - Compare each result with its classical version side by side.
14. Common mistakes
- Table expression without a guard. A missing row causes a short dump.
- Forgetting
BASE.VALUE #( ( ... ) )replaces the whole table. - The same inline name twice in one method or event block. It is a syntax error.
REDUCEor table expression inside aLOOPover the same table. It can turn a fast job into a nested search.- Missing type for
CONDorVALUEwhere#cannot be inferred, such as in aDATA( )declaration. SWITCHwithoutELSE. An unexpected value raises an exception.- Over-nesting. If you need to read it three times, write a loop.
- Calling
split( )like a function. Use theSPLITstatement.
Up next: Blog 13
Performance and Best Practices. Fast SELECT statements, the right internal table type, avoiding nested loops, and the tools to find slow code.
WhatsApp us