A reference to a function or procedure may be passed as an argument to another function or procedure. This is an advanced technique that is mainly useful in multi-level (often recursive) algorithms consisting of a generic intermediate layer and a specific lower layer which may need to be customized for each particular problem. The indirect function call technique allows you to effectively customize the generic intermediate routine without actually having to modify it. Instead, you pass your custom routine to it, which it then calls indirectly to do the parts of the algorithm which you want to customize.
The custom routine in this scenario is sometimes referred to as a "callback routine", since the idea is for the generic intermediate routine to call your routine back to perform the part of the algorithm requiring customization.
For example, when parsing an XML file, you may want to employ a generic routine to walk the tree, but a custom routine to process nodes that meet some criteria. Because of the recursive nature of the routine to walk the tree, it is difficult for it to exit back to your application each time it hits one of the target nodes, without losing its position and ability to continue to the next node. The solution is to design the generic routine to accept a callback routine to be called for each target node. Each application can then use the generic routine in a different way by passing a different callback routine to it.
The scheme consists of three parts:
1. The Callback Routine.
This is a normal function or procedure.
2. The Generic/Intermediate Routine
This receives the callback reference and makes calls to the callback routine. In this routine, the parameter to receive the indirect/callback reference must be defined as follows:
Procedure Foo( ..., @fn'cb() as lblref, ... )
For functions, the formal name must follow the standard naming rules, i.e. start with "Fn'" and end with $ if the functions returns a string value. In all cases it must start with "@", and end with the "as lblref" clause. An empty pair of parentheses, as in the example above, is optional. (Parameters to be passed to the indirect routine are not specified in the declaration.) Beyond that, the name may be anything, for example:
@fn'callback as lblref |
@FN'PROC$ AS LBLREF
@Fn'Custom$() as lblref
@myproc() as lblref
The above parameter declarations would apply to a numeric function, two string functions, and a procedure, in that order.
Inside the intermediate routine, you can call the callback routine using the name specified in the declaration. For example:
call fn'cb(arg1,...) ! call actual callback with actual args
Note: unlike nearly all other formal parameter types, lblref parameters default to .NULL rather than "" if the caller does not supply the actual parameter. This allows you to test whether the function reference was supplied, e.g.
if .ISNULL(fn'cb) then ! if myproc not passed
call my'default'proc(...) ! use a default routine
else
call fn'cb(...) ! else call the passed routine
endif
Within the intermediate routine, if you need to pass the callback routine indirectly again to another routine, or recursively to yourself, then retain the @ prefix, e.g.
Procedure Foo(..., @fn'cb() as lblref, ...) ! intermediate rtn
...
call Foo(...,, @fn'cb(), ...) ! pass callback again indirectly
...
endprocedure
3. The call to the generic/intermediate routine
In this you pass the reference to your callback routine as a parameter. Here you must precede the actual callback routine name with "@" and use an empty set of parens, e.g.
Function Fn'Bar(...) ! actual callback routine
...
Endfunction
...
call Foo(...,@Fn'Bar(), ...) ! passing callback to intermediate Foo()
Note: if you fail to specify the "@" prefix when passing the callback routine to the intemediate routine, it will still be syntactically and semantically legal, but the result will be completely different. To clarify, consider the following two examples:
call Foo(Fn'Bar())
call Foo(@Fn'Bar())
In the first call, the function Fn'Bar() is evaluated first and its return value is passed to the procedure Foo(). In the second call, a reference to the Fn'Bar() function is passed indirectly to the procedure Foo(), which will then be able to call that function without knowing its real name.
Example 1
The following example illustrates the scheme using a generic intermediate routine which scans a directory tree recursively looking for files whose names match a pattern. For each file that matches, it calls your custom routine with the filespec so that you can perform some processing on it. By using the callback technique, the generic recursive directory scanning logic can be re-used without modification in many different applications; the customized callback routine allows each application to process the files differently.
Function Fn'Dir'Scan(dir$ as s260, wild$ as s100, &
@procfile() as lblref) as i4
do
file$ = <next entry in directory>
if <file$ is a normal file and matches wild$>
Fn'Dir'Scan += 1 ! count matching files
call procfile(file$) ! callback routine for file
elseif <file$ is a subdir>
count += Fn'DirScan(file$, wild$, @procfile()) ! recursion
endif
loop until <end of directory>
EndFunction
To use the Fn'Dir'Scan routine, we need to define our callback routine and pass it by reference to the Fn'Dir'Scan routine. For a simple example, here's a callback routine that just prints the file name:
Procedure Show'File'Info(fspec$ as s260:inputonly)
print fspec$
EndProcedure
Now we can use the generic Fn'Dir'Scan() function to print the names of the .bp files in all of the subdirectories of c:\vm as follows...
count = Fn'Dir'Scan("c:\vm", ".bp", @Show'File'Info())
Later in the same app we might want to copy all the matching files to a backup directory. We can use the same Fn'Dir'Scan() routine, but supply a different callback function...
Procedure Backup'File(fspec$ as s260:inputonly)
xcall MIAMEX, MX_COPYFILE, fspec$, ...
EndProcedure
...
count = Fn'Dir'Scan("c:\vm", "*.bp", @Backup'File())
Example 2
See this more complete example Fn'Dir'Scan() in SOSLIB:[907,10].