Using UDFs effectively

This section provides information that will help you use user-defined functions more effectively.

Using Application.cfm and function include files

Consider the following techniques for making your functions available to your ColdFusion pages:

The next section describes other techniques for making UDFs available to your ColdFusion pages.

Specifying the scope of a function

User-defined function names are essentially ColdFusion variables. ColdFusion variables are names for data. Function names are names (references) for segments of CFML code. Therefore, like variables, functions belong to scopes.

About functions and scopes

Like ColdFusion variables, UDFs exist in a scope:

Selecting a function scope

The following table describes the advantages and disadvantages of scopes that you might considering using for your functions:
Scope
Considerations
Application
Makes the function available across all invocations of the application. Unlike with functions defined in Application.cfm or included from other ColdFusion pages, all pages use the same in-memory copy of the function. Using an Application scope function can save memory and the processing required to define a function multiple times. However, Application scope functions have the following limitations:
  • You must lock the code that puts the function name in the Application scope, but you do not have to lock code that calls the function.
  • Application scope functions can cause processing bottlenecks because the server can only execute one copy of the function at a time. All requests that require the function must wait their turn.
Request
Makes the function available for the life of the current HTTP request, including in all custom tags and nested custom tags. This scope is useful if a function is used in a page and in the custom tags it calls, or in nested custom tags.
Server
Makes the function available to all pages on a single server. In most cases, this scope is not a good choice because in clustered systems, it only makes the function available on a single server, and all code that uses the function must be inside a cflock block.
Session
Makes the function available to all pages during the current user session. This scope has no significant advantages over the Application scope.

Using the Request scope

You can effectively manage functions that are used in application pages and custom tags by doing the following:

  1. Define the functions on a function definitions page.
  2. On the functions page, assign the functions to the request scope.
  3. Use a cfinclude tag to include the function definition page on the application page, but do not include it on any custom tag pages.
  4. Always call the functions using the request scope.

This way you only need to include the functions once per request and they are available throughout the life of the request. For example, create a myFuncs.cfm page that defines your functions and assigns them to the Request scope using syntax such as the following:

function MyFunc1(Argument1, Argument2)
{ Function definition goes here }
Request.MyFunc1 = MyFunc1

The application page includes the myFuncs.cfm page:

<cfinclude template="myfuncs.cfm">

The application page and all custom tags (and nested custom tags) call the functions as follows:

Request.MyFunc1(Value1, Value2)

Using the Request scope for static variables and constants

This section describes how to partially break the rule described in the section "Referencing caller variables". Here, the function defines variables in the Request scope. However, it is a specific solution to a specific issue, where the following circumstances exist:

In these circumstances, you can improve efficiency and save processing time by defining your function's variables in the Request scope, rather than the Function scope. The function tests for the Request scope variables and initializes them if they do not exist. In subsequent calls, the variables exist and the function does not reset them.

The NumberAsString function, written by Ben Forta and available from www.cflib.org, takes advantage of this technique.

Using function names as function arguments

Because function names are ColdFusion variables, you can pass a function's name as an argument to another function. This technique allows a function to use another function as a component. For example, a calling page can call a calculation function, and pass it the name of a function that does some subroutine of the overall function.

This way, the calling page could use a single function for different specific calculations, such as calculating different forms of interest. The initial function provides the framework, while the function whose name is passed to it can implement a specific algorithm that is required by the calling page.

The following simple example shows this use. The binop function is a generalized function that takes the name of a function that performs a specific binary operation and two operands. The binop function simply calls the specified function and passes it the operands. This code defines a single operation function, the sum function. A more complete implementation would define multiple binary operations.

<cfscript>
function binop(operation, operand1, operand2)
{ return (operation(operand1, operand2); }
function sum(addend1, addend2)
{ return addend1 + addend2;}
x = binop(sum, 3, 5);
writeoutput(x);
</cfscript>

Handling query results using UDFs

When you call a UDF in the body of a tag that has a query attribute, such as a cfloop query=... tag, any function argument that is a query column name passes a single element of the column, not the entire column. Therefore, the function must manipulate a single query element.

For example, the following code defines a function to combine a single first name and last name to make a full name. It queries the CompanyInfo database to get the first and last names of all employees, then it uses a cfoutput tag to loop through the query and call the function on each row in the query.

<cfscript>
function FullName(aFirstName, aLastName)
 { return aFirstName & " " & aLastName; }
</cfscript>

<cfquery name="GetEmployees" datasource="CompanyInfo"> 
  SELECT FirstName, LastName
  FROM Employee
</cfquery>

<cfoutput query="GetEmployees">
#FullName(FirstName, LastName)#<br>
</cfoutput>

You generally use functions that manipulate many rows of a query outside tags that loop over queries. Pass the query to the function and loop over it inside the function. For example, the following function changes text in a query column to uppercase. It takes a query name as an argument.

function UCaseColumn(myquery, colName)
{
  var currentRow = 1;
  for (; currentRow lte myquery.RecordCount; 
    currentRow = currentRow + 1)
  {
    myquery[colName][currentRow] =
      UCase(myquery[colName][currentRow]);
  }
  Return "";
}

The following code uses a script that calls the UCaseColumn function to convert all the last names in the GetEmployees query to uppercase. It then uses cfoutput to loop over the query and display the contents of the column.

<cfscript>
  UCaseColumn(GetEmployees, "LastName");
</cfscript>
<cfoutput query="GetEmployees">
  #LastName#<br>
</cfoutput>

Identifying and checking for UDFs

You can use the IsCustomFunction function to determine whether a name represents a UDF. The IsCustomFunction function generates an error if its argument does not exist. As a result, you must ensure that the name exists before calling the function, for example, by calling the IsDefined function. The following code shows this use:

<cfscript>
if( IsDefined("MyFunc"))
  if( IsCustomFunction( MyFunc ))
    WriteOutput("MyFunc is a user-defined function");
  else
    WriteOutput("Myfunc is defined but is NOT a user-defined function");
else
  WriteOutput("MyFunc is not defined");
</cfscript>

You do not surround the argument to IsCustomFunction in quotation marks, so you can use this function to determine if function arguments are themselves functions.

Using the Evaluate function

If your user-defined function uses the Evaluate function on arguments that contain strings, you must make sure that all variable names you use as arguments include the scope identifier. Doing so avoids conflicts with function-only variables.

The following example returns the result of evaluating its argument. It produces the expected results, the value of the argument, if you pass the argument using its fully scoped name, Variables.myname. However, the function returns the value of the function local variable if you pass the argument as myname, without the Variables scope identifier.

<cfscript>
  myname = "globalName";
  function readname( name )
  {
    var myname = "localName";
    return (Evaluate( name ));
  }
</cfscript>

<cfoutput>
<!--- This one collides with local variable name --->
  The result of calling readname with myname is: 
      #readname("myname")# <br>
<!--- This one finds the name passed in  --->
  The result of calling readname with Variables.myname is:  #readname("Variables.myname")#  
</cfoutput>

Passing complex data

Structures, queries, and complex objects such as COM objects are passed to UDFs by reference, so the function uses the same copy of the data as the caller. Arrays are passed to user-defined functions by value, so the function gets a new copy of the array data and the array in the calling page is unchanged by the function. As a result, you must handle arrays differently from all other complex data types.

Passing structures, queries, and objects

For your function to modify the caller's copy of a structure, query, or object, you must pass the variable as an argument. Because the function gets a reference to the caller's structure, the caller variable reflects all changes in the function. You do not have to return the structure to the caller. After the function, returns, the calling page accesses the changed data by using the structure variable that it passed to the function.

If you do not want a function to modify the caller's copy of a structure, query, or object, use the Duplicate function to make a copy and pass the copy to the function.

Passing arrays

If you want your function to modify the caller's copy of the array, the simplest solution is to pass the array to the function and return the changed array to the caller in the function return statement. In the caller, use same variable name in the function argument and return variable.

The following example shows how to directly pass and return arrays. In this example, the doubleOneDArray function doubles the value of each element in a one-dimensional array.

<cfscript>
//Initialize some variables
//This creates a simple array.
a=ArrayNew(1);
a[1]=2;
a[2]=22;
//Define the function.
function doubleOneDArray(OneDArray)
{ 
  var i = 0;
  for ( i = 1; i LE arrayLen(OneDArray); i = i + 1)
    { OneDArray[i] = OneDArray[i] * 2; }
  return OneDArray;
}
//Call the function.
a = doubleOneDArray(a);
</cfscript>
<cfdump var="#a#">

This solution is simple, but it is not always optimal:

If you do not use the return statement to return the array to the caller, you can pass the array as an element in a structure and change the array values inside the structure. Then the calling page can access the changed data by using the structure variable it passed to the UDF.

The following code shows how to rewrite the previous example using an array in a structure. It returns True as a status indicator to the calling page and uses the structure to pass the array data back to the calling page.

<cfscript>
//Initialize some variables.
//This creates an simple array as an element in a structure.
arrayStruct=StructNew();
arrayStruct.Array=ArrayNew(1);
arrayStruct.Array[1]=2;
arrayStruct.Array[2]=22;
//Define the function.
function doubleOneDArrayS(OneDArrayStruct)
{ 
  var i = 0;
  for ( i = 1; i LE arrayLen(OneDArrayStruct.Array); i = i + 1)
    { OneDArrayStruct.Array[i] = OneDArrayStruct.Array[i] * 2; }
  return True;
}
//Call the function.
Status = doubleOneDArrayS(arrayStruct);
WriteOutput("Status: " & Status);
</cfscript>
</br>
<cfdump var="#arrayStruct#">

You must use the same structure element name for the array (in this case Array) in the calling page and the function.

Using recursion

A recursive function is a function that calls itself. Recursive functions are useful when a problem can be solved by an algorithm that repeats the same operation multiple times using the results of the preceding repetition. Factorial calculation, used in the following example, is one case where recursion is useful. The Towers of Hanoi game is also solved using a recursive algorithm.

A recursive function, like looping code, must have an end condition that always stops the function. Otherwise, the function will continue until a system error occurs or you stop the ColdFusion Server.

The following example calculates the factorial of a number, that is, the product of all the integers from 1 through the number; for example, 4 factorial is 4 X 3 X 2 X 1 = 24.

function Factorial(factor)
{
  If (factor LTE 1) 
    return 1;
  else 
    return factor * Factorial(factor -1);
}

If the function is called with a number greater than 1, it calls itself using an argument one less than it received. It multiplies that result by the original argument, and returns the result. Therefore, the function keeps calling itself until the factor is reduced to 1. The final recursive call returns 1, and the preceding call returns 2 * 1, and so on until all the initial call returns the end result.

Caution:   If a recursive function calls itself too many times, it causes a stack overflow. Always test any recursive functions under conditions that are likely to cause the maximum number of recursions to ensure that they do not cause a stack overflow.

Handling errors in UDFs

This section discusses the following topics:

The technique you use depends on the circumstances of your function and application and on your preferred programming style. However, most functions should use the second or third technique, or a combination of the two. The following sections discuss the uses, advantages, and disadvantages of each technique, and provides examples of their use.

Displaying error messages

Your function can test for errors and use the WriteOutput function to display an error message directly to the user. This method is particularly useful for providing immediate feedback to users for simple input errors. You can use it independently or in conjunction with either of the other two error-handling methods.

For example, the following variation on a "Hello world" function displays an error message if you do not enter a name in the form:

<cfform method="POST" action="#CGI.script_name#">
  <p>Enter your Name:&nbsp;
  <input name="name" type="text" hspace="30" maxlength="30">
  <input type="Submit" name="submit" value="OK">
</cfform>
<cfscript>
  function HelloFriend(Name)
  { 
    if (Name is "") WriteOutput("You forgot your name!");
    else WriteOutput("Hello " & name &"!");
    return "";
  }
  if (IsDefined("Form.submit")) HelloFriend(Form.name);
</cfscript>

Reviewing the code

The following table describes the code:
Code
Description
<cfform method="POST" action="#CGI.script_name#">
  <p>Enter your Name:&nbsp;
  <input name="name" type="text" hspace="30"
    maxlength="30">
  <input type="Submit" name="submit" value="OK">
</cfform>
Creates a simple form requesting you to enter your name.
Uses the script_name CGI variable to post to this page without specifying a URL.
If you do not enter a name, the form posts an empty string as the name field.
<cfscript>
  function HelloFriend(Name)
  { 
    if (Name is "") WriteOutput("You forgot your
      name!");
    else WriteOutput("Hello " & name &"!");
    return "";
  }
  if (IsDefined("Form.submit"))
    HelloFriend(Form.name);
</cfscript>
Defines a function to display "Hello name!" First, checks whether the argument is an empty string. If so, displays an error message.
Otherwise displays the hello message.
Returns the empty string. (The caller does not use the return value). It is not necessary to use curly braces around the if or else statement bodies because they are single statements.
If this page has been called by submitting the form, calls the HelloFriend function. Otherwise, the page just displays the form.

Providing status information

In some cases, such as those where the function cannot provide a corrective action, the function cannot, or should not, handle the error directly. In these cases, your function can return information to the calling page. The calling page must handle the error information and act appropriately.

Consider the following mechanisms for providing status information:

Each of these methods can have variants, and each has advantages and disadvantages. Which technique you use should depend on the type of function, the application in which you use it, and your coding style.

The following example, which modifies the function used in "A User-defined function example", uses one version of the status variable method. It provides two forms of error information:

The TotalInterest function

After changes to handle errors, the TotalInterest function looks like the following. Code that is changed from the example in "A User-defined function example" is in bold.

<cfscript>
function TotalInterest(principal, annualPercent, months, status)
{
  Var years = 0;
  Var interestRate = 0;
  Var totalInterest = 0;
  principal = trim(principal);
  principal = REReplace(principal,"[\$,]","","ALL");
  annualPercent = Replace(annualPercent,"%","","ALL");
  if ((principal LE 0) OR (annualPercent LE 0) OR (months LE 0))
  {
    Status.errorMsg = "All values must be greater than 0";
    Return -1;
  }
  interestRate = annualPercent / 100;
  years = months / 12;
  totalInterest = principal*(((1+ interestRate)^years)-1);
  Return DollarFormat(totalInterest);
}
</cfscript>

Reviewing the code

The following table describes the code that has been changed or added to the previous version of this example. For a description of the initial code, see "A User-defined function example".
Code
Description
function TotalInterest(principal, 
  annualPercent, months, status)
The function now takes an additional argument, a status structure. Uses a structure for the status variable so that changes that the function makes affect the status structure in the caller.
if ((principal LE 0) OR 
  (annualPercent LE 0) OR 
  (months LE 0))
{
  Status.errorMsg = "All values 
    must be greater than 0";
  Return -1;
}
Checks to make sure the principal, percent rate, and duration are all greater than zero.
If any is not, sets the errorMsg key (the only key) in the Status structure to a descriptive string. Also, returns -1 to the caller and exits the function without processing further.

Calling the function

The code that calls the function now looks like the following. Code that is changed from the example in "A User-defined function example" is in bold.

<cfset status = StructNew()>
<cfset myInterest = TotalInterest(Form.Principal, 
  Form.AnnualPercent,Form.Months, status)>
<cfif myInterest EQ -1>
  <cfoutput>
    ERROR: #status.errorMsg#<br>
  </cfoutput>
<cfelse>
  <cfoutput> 
    Loan amount: #Form.Principal#<br>
    Annual percentage rate:
      #Form.AnnualPercent#<br>
    Loan duration: #Form.Months# months<br>
    TOTAL INTEREST: #myInterest#<br>
  </cfoutput>
</cfif>

Reviewing the code

The following table describes the code that has been changed or added:
Code
Description
<cfset status = StructNew()>
Creates a structure to hold the function status.
<cfset myInterest = TotalInterest
  (Form.Principal, Form.AnnualPercent,
  Form.Months, status)>
Calls the function. This time, the function requires four arguments, including the status variable.
<cfif myInterest EQ -1>
  <cfoutput>
    ERROR: #status.errorMsg#<br>
  </cfoutput>
If the function returns -1, there must be an error. Displays the message that the function placed in the status.errorMsg structure key.
<cfelse>
  <cfoutput> 
    Loan amount: #Form.Principal#<br>
    Annual percentage rate:
      #Form.AnnualPercent#<br>
    Loan duration: #Form.Months# months<br>
    TOTAL INTEREST: #myInterst#<br>
  </cfoutput>
</cfif>
If the function does not return -1, it returns an interest value. Displays the input values and the function return value.

Using exceptions

UDFs written in CFScript can handle exceptions using the try and catch statements. UDFs written using the cffunction tag can use the cftry, cfcatch, cfthrow, and cfrethrow tags. Using exceptions corresponds to the way many functions in other programming languages handle errors, and can be an effective way to handle errors. In particular, it separates the functional code from the error-handling code, and it can be more efficient than other methods at runtime, because it does not require testing and branching.

Exceptions in UDFs have the following two dimensions:

Handling exceptions in UDFs

A UDF should use try/catch blocks to handle exceptions in the same conditions that any other ColdFusion application uses try/catch blocks. These are typically circumstances where the function uses an external resource, such as a Java, COM, or CORBA object, a database, or a file. When possible, your application should prevent, rather than catch, exceptions caused by invalid application data. For example, the application should prevent users from entering a zero value for a form field that is used to divide another number, rather than handling exceptions generated by dividing by zero.

When ColdFusion catches an exception, the function can use any of the following methods to handle the exception:

Generating exceptions in UDFs

If you define your function using the cffunction tag, you can use the cfthrow and cfrethrow tags to throw errors to the page that called the function. You can use this technique whenever your UDF identifies an error, instead of displaying a message or returning an error status. For example, the following code rewrites the example from "Providing status information" to use the cffunction tag and CFML, and to throw and handle an exception if any of the form values are not positive numbers.

The lines that identify invalid data and throw the exception are in bold. The remaining lines are equivalent to the CFScript code in the previous example. However, the code that removes unwanted characters must precede the error checking code.

<cffunction name="TotalInterest">
  <cfargument name="principal" required="Yes">
  <cfargument name="annualPercent" required="Yes"> 
  <cfargument name="months" required="Yes">
  <cfset principal = trim(principal)>
  <cfset principal = REReplace(principal,"[\$,]","","ALL")>
  <cfset annualPercent = Replace(annualPercent,"%","","ALL")>

  <cfif ((principal LE 0) OR (annualPercent LE 0) OR (months LE 0))>
    <cfthrow type="InvalidData" message="All values must be greater than 0.">
  </cfif>

  <cfset years = 0>
  <cfset interestRate = 0>
  <cfset totalInterest = 0>
  <cfset interestRate = annualPercent / 100>
  <cfset years = months / 12>
  <cfset totalInterest = principal*
      (((1+ interestRate)^years)-1)>
  <cfreturn DollarFormat(totalInterest)>
</cffunction>

The code that calls the function and handles the exception looks like the following. The changed lines are in bold.

<cftry>
  <cfset status = StructNew()>
  <cfset myInterest = TotalInterest(Form.Principal, Form.AnnualPercent,
    Form.Months, status)>
  <cfoutput> 
    Loan amount: #Form.Principal#<br>
    Annual percentage rate: #Form.AnnualPercent#<br>
    Loan duration: #Form.Months# months<br>
    TOTAL INTEREST: #myInterest#<br>
  </cfoutput>
<cfcatch type="InvalidData">
  <cfoutput>
    #cfcatch.message#<br>
  </cfoutput>
</cfcatch>
</cftry>

Comments