Ask Reuben

Reflection

What is Reflection? 

How can I write more generic code? 

How can I write library functions to work with all arrays?

The Reflection API was introduced in Genero 4.00 allowing you to introspect and use program elements that are not known at compile time.  The reflect package has a value class to inspect and modify variables, a type class to inspect type, and a method class to inspect methods.

It is useful to write generic code in conjunction with base.SqlHandle and ui.Dialog class methods.  The base.SqlHandle allows you to write generic database interaction, ui.Dialog allows you to write generic user interface code, the new reflect package is the missing piece in the puzzle that allows you to write generic code that operates with different datatypes.

A reflect.Value object is the runtime representation of a variable that has methods to tell you about itself.  It can also be thought of as a pointer to a variable and is useful to pass to/from generic routines.

An example I had early on was …


IMPORT reflect

FUNCTION sql2array(sql STRING, r_arr reflect.Value) RETURNS()
DEFINE h base.SqlHandle
DEFINE column_idx, row_idx, column_count INTEGER 

    CALL r_arr.clear()  
    
    LET h = base.SqlHandle.create()
    CALL h.prepare(sql)
    CALL h.open()

    LET row_idx = 0
    WHILE TRUE
        CALL h.fetch()
        IF sqlca.sqlcode = NOTFOUND THEN
           EXIT WHILE
        END IF
        CALL r_arr.appendArrayElement()
        LET row_idx = row_idx + 1 
        VAR r_row = r_arr.getArrayElement(row_idx)
        LET column_count = h.getResultCount()

        FOR column_idx=1 TO column_count
            -- Assumes table and array structure match, you could modify so it
            -- was more forgiving by using r_rec.getFieldByName, h.getResultByName etc 
            CALL r_row.getField(column_idx).set(reflect.Value.copyOf(h.getResultValue(column_idx)))
        END FOR
    END WHILE
    CALL h.close()
END FUNCTION

… that can be called with code like …


IMPORT reflect
&define TEST_TABLE_STRUCTURE idx INTEGER, dat CHAR(3), dmy DATE

MAIN
DEFINE arr DYNAMIC ARRAY OF RECORD
   TEST_TABLE_STRUCTURE
END RECORD

    CONNECT TO ":memory:+driver='dbmsqt'"
    CREATE TABLE foo( TEST_TABLE_STRUCTURE)
    INSERT INTO foo VALUES(1,"AAA", TODAY)
    INSERT INTO foo VALUES(2,"BBB", TODAY)

    CALL sql2array("SELECT * FROM foo", reflect.Value.valueOf(arr))

    DISPLAY arr[1].*
    DISPLAY arr[2].*
END MAIN

So note in the call CALL sql2array("SELECT * FROM foo", reflect.Value.valueOf(arr)) , the reflect.Value.valueOf method is effectively creating a pointer to the variable arr which is being passed to the function sql2array(). The sql2array function is then populating the arr dynamic array variable with the result of the select sql statement that is the first parameter.

You can tweak that code as noted in the comments to make it more forgiving but hopefully you can see that that 30 line function allows you to take any SELECT statement and populate a dynamic array with the result set of the SQL statement in one line of code!.

Other examples I have to show the power of reflection are these examples to manipulate arrays.

Something like the following …


FUNCTION array_delete_range(r_arr reflect.Value, from INTEGER, to INTEGER)
DEFINE idx INTEGER

    -- Will need some error handling at beginning
    IF r_arr.getType().toString() MATCHES "DYNAMIC ARRAY*" THEN
        -- OK to continue
    ELSE
        RETURN -- Throw exception when able
    END IF

    IF from >= 1 AND from <= r_arr.getLength() THEN
        -- OK to continue
    ELSE
        RETURN -- Throw exception when able
    END IF

    IF to >= 1 AND to <= r_arr.getLength() THEN
        -- OK to continue
    ELSE
        RETURN -- Throw exception when able
    END IF
    IF from <= to THEN
        -- OK tp continue
    ELSE
        RETURN -- Throw exception when able
    END IF
        
    FOR idx = to TO from STEP -1
        CALL r_arr.deleteArrayElement(idx)
    END FOR
END FUNCTION

… allows you to delete multiple rows from an array with a one line function call.

Similarly the following …


FUNCTION array_appendto(r_arr1 reflect.Value, r_arr2 reflect.Value, start INTEGER)  
DEFINE i INTEGER
DEFINE row1, row2 reflect.Value

    -- add error handling
    IF r_arr1.getType().toString() = r_arr2.getType().toString() THEN
        -- Same type
    ELSE
        -- Throw error when able
    END IF
    
    IF start >=0 AND start <= r_arr1.getLength() THEN
        #OK (Note if start=0 then append to end)
    ELSE
        -- Throw error when able
    END IF

    FOR i = 1 TO r_arr2.getLength()
        LET row2 = r_arr2.getArrayElement(i)
        IF start = 0 THEN -- append at end
            CALL r_arr1.appendArrayElement()
            LET row1 = r_arr1.getArrayElement(r_arr1.getLength())
        ELSE
            CALL r_arr1.insertArrayElement(start+i-1)
            LET row1 = r_arr1.getArrayElement(start+i-1)
        END IF
        CALL row1.set(row2)
    END FOR
END FUNCTION

… allows you to append two arrays together.

In all cases passing to the functions, a pointer to the array achieved via use of reflect.Value.valueOf call e.g.
CALL arr_delete_range(reflect.Value.valueOf(arr), 3,5) --delete 5 rows starting at row 3
CALL array_appendto(reflect.Value.valueOf(arr1),reflect.Value.valueOf(arr2), 0) -- append arr2 to arr1

If you have not already, I would encourage you to study the reflect package and note how you can reduce the number of lines of code in your application and make it easier for junior developers to be more productive by adding some useful methods to your libraries.  Neil did a presentation at the last WWDC https://4js.com/wwdc-21-online-conference-archive/#tab-id-3 which includes some of these examples and more.

I think there is a lot of potential using the reflect package to write some good generic code.  One thing I wish is it was simpler to type reflect.value.valueOf().  I think if I had a production system I might have a pre-processor macro of &define ptr(p1) reflect.Value.ValueOf(p1)   so that reading the code I see ptr(array_variable) and read it as pointer to an array variable!