1
votes

A continued annoyance in ABAP is that one is unable to distinguish a variable that was never set from one that was set to the initial value for its data type, and that failing accesses to internal tables via itab[...] expressions throw CX_SY_ITAB_LINE_NOT_FOUND which is costly in terms of runtime to deal with.

For enumerated types, one can define the first variant as invalid to guard against the first confusion:

TYPES:
  BEGIN OF ENUM my_bool,
    invalid,
    true,
    false,
  END OF ENUM my_bool.

(Do not actually use this type as an ABAP_BOOL replacement. The way predicative method calls behave, a method meth returning a my_bool would lead to obj->meth( ) inside an IF obj->meth( ). being truthy only if it returns invalid, since the predicative method call is equivalent to IF obj->meth( ) IS NOT INITIAL.)

For non-enumerated types, we're out of luck. Trying to implement something like an option type to return from methods that tried and failed to access some data is blocked by the weak typing of generics:

CLASS zcl_option DEFINITION
    FINAL.
PUBLIC SECTION.
  CLASS-METHODS some
    IMPORTING VALUE(val) TYPE any
    RETURNING VALUE(option) TYPE REF TO zcl_option.
  CLASS-METHODS none
    RETURNING VALUE(option) TYPE REF TO zcl_option.
  METHODS is_some
    RETURNING VALUE(is_some) TYPE abap_bool.
  METHODS get
    RETURNING VALUE(val) TYPE ???.
ENDCLASS.

where trying to use a generic type in the return value of get fails with "returning parameters must be fully typed".

Is there a way I can construct something like the option type in ABAP, or even more generally sum types?

3
I think there's no better answer than your proposal, and you might obtain only opinionated answers how to code it generically (for instance by using inheritance and defining the value in the subclass as a READ-ONLY attribute, etc.) Maybe you should give the context in which you want to use it, as it currently looks very theoretical, so that to get more precise answers.Sandra Rossi
@SandraRossi While I have a concrete context in mind, I'd find such a type generally useful to return from methods that query resources that may or may not exist, i.e. where non-existence is not an exceptional state as such. (Example: get the TADIR entry of a class, if it exists. For example, many classes generated by things like the BSP framework don't have one, but that's not an error.)ACuriousMind

3 Answers

1
votes

This is what I came up with. I was about to throw it away when it started looking not 100 % useless, so here it goes :D :D


PROGRAM ztest_options_tongue_in_cheek.

*&---------------------------------------------------------------------*
TYPES:
  BEGIN OF ENUM t_options,
    some,
    none,
  END OF ENUM t_options.

DEFINE macro_option_type.
  CLASS zcl_option_&1 DEFINITION FINAL.
    PUBLIC SECTION.
      METHODS:
        constructor
          IMPORTING VALUE(i_val)     TYPE &1
                    VALUE(i_option)  TYPE t_options,
        is_some
          RETURNING VALUE(e_is_some) TYPE abap_bool,
        get
          RETURNING VALUE(e_val)     TYPE &1.
    PRIVATE SECTION.
      DATA:
        lv_the_value TYPE &1,
        lv_is_none   TYPE abap_bool.
  ENDCLASS.

  CLASS zcl_option_&1 IMPLEMENTATION.
    METHOD constructor.
      lv_the_value = i_val.
      lv_is_none   = COND #( WHEN i_option = none THEN abap_true ELSE abap_false ).
    ENDMETHOD.

    METHOD is_some.
      e_is_some = COND #( WHEN lv_is_none = abap_false THEN abap_true ELSE abap_false ).
    ENDMETHOD.

    METHOD get.
      e_val = lv_the_value.
    ENDMETHOD.
  ENDCLASS.
END-OF-DEFINITION.
*&---------------------------------------------------------------------*


*&---------------------------------------------------------------------*
* Static declaration of required option types
macro_option_type:
  i, string, char256, float, uzeit, datum.
*&---------------------------------------------------------------------*


*&---------------------------------------------------------------------*
DEFINE option.
* Rely on garbage collector for simplicity
  &2 = NEW zcl_option_&1( i_val = CONV &1( &3 ) i_option = &4 ).
END-OF-DEFINITION.
*&---------------------------------------------------------------------*


*&---------------------------------------------------------------------*
DEFINE declare_init_option.
  DATA &2 TYPE REF TO zcl_option_&1.
  option &1 &2 space none.
END-OF-DEFINITION.
*&---------------------------------------------------------------------*



*&---------------------------------------------------------------------*
START-OF-SELECTION.
*&---------------------------------------------------------------------*
  PERFORM test_options.


*&---------------------------------------------------------------------*
*& Form TEST_OPTIONS
*&---------------------------------------------------------------------*
FORM test_options .

  declare_init_option:
    i      lo_integer_option,
    string lo_string_option,
    float  lo_float_option,
    uzeit  lo_time_option.



  option i lo_integer_option 123 some.

  option string lo_string_option 'I am now a string' some.

  option string lo_string_option `` none. "back to none

* e.g.,
  IF lo_integer_option->is_some( ) = abap_true.
    WRITE: / |lo_integer_option is { lo_integer_option->get( ) }|.
  ELSE.
    WRITE: / 'lo_integer_option is nothing'.
  ENDIF.

ENDFORM.
0
votes

Yes, it is possible. However, it is not as type-safe and thus convenient as in programming languages that support generic typing.

CLASS zcl_optional DEFINITION PUBLIC FINAL CREATE PUBLIC.

  PUBLIC SECTION.
    METHODS constructor
      IMPORTING
        value TYPE simple OPTIONAL.
    METHODS is_available
      RETURNING
        VALUE(result) TYPE abap_bool.
    METHODS put_into
      IMPORTING
        reference TYPE REF TO data.

  PRIVATE SECTION.
    DATA value TYPE REF TO data.

ENDCLASS.

CLASS zcl_optional IMPLEMENTATION.

  METHOD constructor.
    IF value IS SUPPLIED.
      DATA(type_descriptor) =
        CAST cl_abap_datadescr( cl_abap_datadescr=>describe_by_data( value ) ).
      CREATE DATA me->value TYPE HANDLE type_descriptor.
      ASSIGN me->value->* TO FIELD-SYMBOL(<target>).
      <target> = value.
    ENDIF.
  ENDMETHOD.

  METHOD is_available.
    result = xsdbool( me->value IS BOUND ).
  ENDMETHOD.

  METHOD put_into.
    IF value IS BOUND.
      ASSIGN value->* TO FIELD-SYMBOL(<source>).
      ASSIGN reference->* TO FIELD-SYMBOL(<target>).
      <target> = <source>.
    ENDIF.
  ENDMETHOD.

ENDCLASS.

used like

DATA(optional_integer) = NEW zcl_optional( 42 ).
cl_abap_unit_assert=>assert_true( optional_integer->is_available( ) ).

DATA integer_variable TYPE i.
optional_integer->put_into( REF #( integer_variable ) ).
cl_abap_unit_assert=>assert_equals( act = integer_variable exp = 42 ).

DATA(optional_string) = NEW zcl_optional( `abc` ).
cl_abap_unit_assert=>assert_true( optional_string->is_available( ) ).

DATA string_variable TYPE string.
optional_string->put_into( REF #( string_variable ) ).
cl_abap_unit_assert=>assert_equals( act = string_variable exp = `abc` ).

DATA(empty_optional) = NEW zcl_optional( ).
cl_abap_unit_assert=>assert_false( empty_optional->is_available( ) ).

DATA another_variable TYPE string.
empty_optional->put_into( REF #( another_variable ) ).
cl_abap_unit_assert=>assert_initial( another_variable ).

The problem being that methods returning an optional have no means to describe the actual type of the content.

METHODS do_something
  RETURNS
    VALUE(result) TYPE REF TO zcl_optional. " int4 or string?
0
votes

The initial value for internal table is always empty table without a rows. So your whole idea has no sense.

Setting initial value will not prevent you from getting CX_SY_ITAB_LINE_NOT_FOUND exception.