Chapter 4: User-Defined Methods and Packages

4.1 Introduction

4.2 Diving into User-Defined Methods

4.2.1 Overview

4.2.2 Designing a User-Defined Method

4.3 User-Defined Packages

4.3.1 General Considerations

4.3.2 User-Defined Package Specifics

4.4 Object-Oriented Programming with DS2 Packages

4.4.1 General Considerations

4.4.2 Designing an Object

4.4.3 Using Objects as Building Blocks

4.4 Review of Key Concepts

4.1 Introduction

In this chapter, we will write user-defined methods to create custom, reusable blocks of DS2 code. First, we’ll explore user-defined methods written inline in a DS2 DATA program, and then we’ll learn how to store, share, and reuse user-defined methods in DS2 packages. Specifically, we will cover these points:

   Defining user-defined methods

   Defining inline methods

   Setting Parameters

   Returning a value versus modifying parameter values in place

   Overloading

   Packaging user-defined methods for reuse

   Storing user-defined methods in packages

   Defining a constructor method

   Defining global and local package variables

   Using packages in subsequent DS2 programs

   Declaring and instantiating packages in DS2 programs

   Compile-time versus execution-time instantiation

   Using package constructor arguments to set state

   Using private package global variables to track state

   Using dot notation to execute package methods

   Documenting and maintaining packages

   Using DS2 packages for object-oriented programming

4.2 Diving into User-Defined Methods

4.2.1 Overview

User-defined methods are named code blocks in a DS2 program that execute only when called. Methods that produce values can be designed to deliver those values in one of two ways: the method can either perform like a Base SAS function, returning a single value to the calling process, or it can perform like a Base SAS CALL routine, modifying one or more parameter values at the call site. User-defined methods for one-time use can easily be written inline in DS2 data or thread programs and called from one of the program’s system method blocks.

4.2.2 Designing a User-Defined Method

Methods are a great tool for standardizing processes in your code. Let’s take a simple interest calculation as an example. Here is how a bank could calculate the accumulated interest for my account after one year:

Amount=SUM(Amount,Amount*Rate);

To calculate the amount over several years, it’s possible to execute the formula in a DO loop:

do i=1 to Years;

   Amount=SUM(Amount,Amount*Rate);

end;

4.2.2.1 Method Design Considerations

To create a method to perform interest calculations, we must first decide how we want to pass the calculated interest value back to our calling process. We could choose to return a value, to modify one of the method’s parameters at the call site by defining an IN_OUT parameter, or even use a combination of both to deliver results and a return code. A method without IN_OUT parameters is the most flexible. Because parameters are passed by value, when the method is called, values for the parameters can be provided any number of ways: as a constant, as a variable reference, or as the result of a valid expression. A method with IN_OUT parameters must receive values for those parameters by reference. IN_OUT parameters can’t accept an actual value. Instead, the method requires a variable name to identify the memory location where the value can be found. With that information, the method can directly access the memory location where the value is stored, enabling the method to both retrieve and modify the value. Because the method needs to be able to write its results directly to the IN_OUT parameter’s location in memory, if you provide a constant or expression to an IN_OUT parameter when the method is called, a syntax error will result.  

In addition to the method style, we’ll need to identify the number of parameters required and their data types.  

4.2.2.2 The Interest Method—V1.0

Let’s make version 1.0 of our interest method using an IN_OUT parameter for the initial amount of principal. When we call this method, we will need to pass in a variable name for the amount parameter. The method will retrieve the initial amount invested from the memory location that is associated with the variable name provided and, after calculations are completed, store the result back to that memory location.

proc ds2;

title 'Results for each bank - initial $5,000 deposit';

data;

   dcl double Total having format dollar10.2;

   dcl int Duration;

   keep total duration bank rate;

   method interest(IN_OUT double Amount, double Rate, int Years);

      dcl int i;

      do i=1 to Years;

         Amount=SUM(Amount,Amount*Rate);

      end;

   end;

   method run();

      set sas_data.banks;

      do Duration=1 to 5 by 4;

         Total=5000;

         interest(Total,Rate,Duration);

         output;

      end;

   end;

enddata;

run;

quit;

title;

Figure 4.1 shows the results of using the interest method to calculate the final results of depositing $5,000 in each of the banks in the sas_data.banks data set for a period of one and five years. The user-defined interest method was used to perform the calculations using the interest rates on file for each bank in the data set.

Figure 4.1: Results of Using the User-Defined Interest Method

image

What would we do differently if we wanted our method to return a value like a function instead of modifying one of its parameters? This version would have no IN_OUT parameters, so the calling code must include an expression that is capable of processing the resulting value returned. Let’s make a version that returns a value and that is called from an assignment statement.

proc ds2;

title 'Results for each bank - initial $5,000 deposit';

title2 'method returns a value like a function';

data;

   dcl double Total having format dollar10.2;

   dcl int Duration;

   keep total duration bank rate;

   method interest(double Amount, double Rate, int Years)

          returns double;

      dcl int i;

      do i=1 to Years;

         Amount=SUM(Amount,Amount*Rate);

      end;

      return Amount;

   end;

   method run();

      set sas_data.banks;

      do Duration=1 to 5 by 4;

         Total=interest(5000,Rate,Duration);

         output;

      end;

   end;

enddata;

run;

quit;

title;

Figure 4.2 shows the results of using the interest method to calculate the final results of depositing $5,000 in each of the banks in the sas_data.banks data set for a period of one and five years using the modified user-defined interest method. The results are identical to those produced by the previous version. That is to be expected, because we did not change the algorithm used to calculate interest, but only the method used to return the result to the calling process.

Figure 4.2: Results of Using the Modified User-Defined Interest Method

image

4.2.2.3 The Interest Method – V1.1

Now that everyone in the company is using our new method to do their interest calculations, we detect a flaw: interest can be compounded only annually. What if we need to compound the interest quarterly, weekly, or even daily? Let’s create version 1.1 of our interest that returns the total after compounding for a user-specified number of periods each year. The new interest calculating algorithm looks like this:

do i=1 to Years;

   do j=1 to Periods;

      Amount=SUM(Amount,Amount*(Rate/periods));

   end;

end;

We’ll need to add an additional parameter to accept the number of periods.

proc ds2;

title 'Results for each bank - initial $5,000 deposit';

title2 'variable compounding periods';

data;

   dcl double Total having format dollar10.2;

   dcl int Years Periods;

   keep total Years periods bank rate;

   method interest(double Amount, double Rate, int Years

       , int Periods) returns double;

      dcl int i j;

      do i=1 to Years;

         do j=1 to Periods;

            Amount=SUM(Amount,Amount*(Rate/Periods));

         end;

      end;

      return Amount;

   end;

   method run();

      set sas_data.banks;

      Years=5;

      Periods=4;

      Total=interest(5000,Rate,Years,Periods);

      output;

      Periods=52;

      Total=interest(5000,Rate,Years,Periods);

      output;

   end;

enddata;

run;

quit;

title;

Figure 4.3: Results of Using the Modified User-Defined Interest Method with Four Parameters

image

Now we have a dilemma. If everyone in the company is using our v1.0 interest method, which requires only three parameters, to do their interest calculations and if we replace it with the new v1.1, which requires four parameters, all of the current programs using the interest method will fail because of syntax errors. Because DS2 methods require that all parameters be identified when the method is compiled, we can’t have “optional” parameters. What we would really like to do is write several versions of the same method and have the system choose the most appropriate version at run time. And that, in a nutshell, is method overloading.

4.2.2.4 Method Overloading

Method overloading is accomplished by creating two or more methods that have the same name but that have unique “signatures.” A method’s signature consists of its name and the ordered list of its parameter data classes. Consider the following method definitions:

   method t(double v1) ;

      put 'Double Parameter';

   end;

   method t(real v1) ;

      put 'Real Parameter';

   end;

Executing a program with these methods in the same scope will result in a compilation error:

ERROR: Compilation error.

ERROR: Duplicate declaration for method t.

The signatures for these methods would look something like this:

   t, floating-point numeric

   t, floating-point numeric

When we think of it this way, we can easily see why the compiler is complaining about our code! For a method’s signature to be unique, it must either have a different number of parameters or parameters that are clearly from different data classes. The following method definitions will all have unique signatures:

   method t(double v1) ;

      put 'Double Parameter';

   end;

   method t(decimal(35,5) v1) ;

      put 'Real Parameter';

   end;

   method t(double v1, double v2) ;

      put 'Real Parameter';

   end;

The signatures for these methods would look something like this:

   t, floating-point numeric

   t, fixed-point numeric

   t, floating-point numeric, floating-point numeric

All are easily distinguished from each other.

Now that we know about method overloading, it will be easy to create the different versions of the interest method that we require. All we need now is an easy mechanism for storing, deploying, and sharing our methods with the other programmers in our company.

4.3 User-Defined Packages

4.3.1 General Considerations

If you want to use a standard, user-defined method in several programs across different SAS sessions, you can build a collection of methods and store it in a SAS library. In DS2, we call these collections packages, and you create them using a DS2 PACKAGE program. DS2 packages can be used in subsequent DS2 programs, making their methods easily and safely reusable.

But packages can be so much more than just a storage mechanism for user-defined methods! Packages are the objects of the DS2 language. Just as all DATA programs contain all three system methods (INIT, RUN, and TERM), all packages include a constructor method that has the same name as the package, and a destructor method named DELETE. You can explicitly define a package’s constructor and destructor methods to set up an initialization process for the package when it is instantiated and to specify the cleanup routines to be executed as the package goes out of scope. When you do not explicitly write a constructor or destructor method, a null method is provided by the compiler. A package can also contain private, global variables that we can use to keep track of state within a package instance. We will explore the benefits of an object-oriented approach to application development and show examples of using DS2 packages as objects in Section 4.3.3.

4.3.2 User-Defined Package Specifics

4.3.2.1 Storing User-Defined Methods in Packages

A package is created with a DS2 package program. The PACKAGE statement indicates where the package will be stored. The methods that are defined inside the package program become part of the package.

DS2 packages have no built-in mechanism for determining which methods are included in the package. In addition, to modify a single method, the entire package needs to be re-created. If you use DS2 packages—and I strongly encourage you to do so—you’ll need some administrative controls in place to make them easier to use and administer.  I would recommend the following two practices that can improve usability and make administration easier:

   Include a user-defined CONTENTS method in every package that you create. The CONTENTS method should not accept parameters. When called, this method should document the package in the SAS log. At a minimum, it should provide a list of the methods included in the package and the location of the source code. You will need to have the entire package source code to perform any maintenance on the package, as the whole package must be re-created each time a change is made.

   Every method in the package should be overloaded with a version of the method that accepts no parameters and does nothing but document the method in the SAS log.

Let’s create a simple package that includes the two versions of our interest method and also includes the self-documenting features discussed above:

proc ds2;

package sas_data.MyMethods;

   method contents();

      put ;

      put 'This package contains the following methods:';

      put ' - contents()';

      put ' - interest()';

      put ;

      put 'Package source:';

      put 'z:/ds2_wrangler/programs/4.3.2.sas';

      put ;

   end;

   method interest();

      put ;

      put 'Syntax: interest(Amount,Rate,Years<,Periods>)';

      put '        Amount (double) - original amount';

      put '        Rate (double)   - interest rate';

      put '        Years (int)     - number of years for interest'

          ' yield';

      put '        Periods (int)   - number of compounding periods'

          ' per year (optional)';

      put ;

      put '        If no Periods specified, compounds annually';

      put '        Returns total amount after compounding (double)';

      put ;

   end;

   method interest(double Amount, double Rate, int Years)

          returns double;

      dcl int i;

      do i=1 to Years;

         Amount=SUM(Amount,Amount*Rate);

      end;

      return Amount;

   end;

   method interest(double Amount, double Rate, int Years

          , int Periods) returns double;

      dcl int i j;

      do i=1 to Years;

         do j=1 to Periods;

            Amount=SUM(Amount,Amount*Rate/periods);

         end;

      end;

      return Amount;

   end;

endpackage;

run;

quit;

title;

SAS Log:

NOTE: Created package mymethods in data set sas_data.mymethods.

NOTE: Execution succeeded. No rows affected.

If you are curious like me, you’ll want to peek into the data set sas_data.mymethods to see what a package looks like. Go ahead! As you can see in Figure 4.4, all you’ll find inside is a little clear text header information followed by a bunch of encrypted source code. Cool, but not very interesting reading.

Figure 4.4: Contents of the SAS_DATA.MyMethods Package

image

4.3.2.2 Using Methods Stored in Packages

To use the methods stored in a package, you will need to declare and instantiate an instance of the package. When you declare the package, you will give the instance an identifier. To use the methods in the package, simply call them using dot notation syntax like instance.method().

A package is declared with a DCL PACKAGE statement and can be instantiated at compile time—that is, declared and instantiated at the same time. Alternatively, it can be instantiated at run time—that is, declared first and then later instantiated in a separate executable statement. We normally declare and instantiate the package at compile time because the syntax is less complex. But if the package constructor method requires information that will not become available until the program begins executing, we would need to instantiate the package at run time. We’ll see an example of this later, but because the package that we built doesn’t use constructor parameters, we’ll declare and instantiate it at compile time.

Here’s a program that uses the CONTENTS method from the package that we created in the last section to determine what methods are available in the package:

proc ds2;

data _null_;

   /* Declare and instantiate the package */

   /* This instance's identifier is mm */

   dcl package sas_data.MyMethods mm();

   method init();

      /* Execute the contents method from */

      /* package instance mm */

      mm.contents();

   end;

enddata;

run;

quit;

SAS Log:

This package contains the following methods:

 - contents()

 - interest()

 

Package source:

z:/ds2_wrangler/programs/4.3.2.sas

Next, we’ll use the version of the INTEREST method without parameters to retrieve syntax help for the INTEREST method:

proc ds2;

data _null_;

   dcl package sas_data.MyMethods mm();

   method init();

      mm.interest();

   end;

enddata;

run;

quit;

SAS Log:

Syntax: interest(Amount,Rate,Years<,Periods>)

        Amount (double) - original amount

        Rate (double)   - interest rate

        Years (int)     - number of years

        Periods (int)   - compounding periods/year (optional)

 

        When periods are not specified, interest compounds annually

        Returns total amount after compounding (double)

Now that we know the syntax, let’s use the INTEREST method to perform some real work:

proc ds2;

title 'Results for each bank - initial $5,000 deposit';

title2 'compounding annually and daily ';

data;

   dcl package sas_data.MyMethods mm();

   dcl double Total having format dollar10.2;

   dcl int Years Periods;

   keep total Years periods bank rate;

   method run();

      set sas_data.banks;

      Years=5;

      Periods=1;

      Total=mm.interest(5000,Rate,Years);

      output;

      Periods=365;

      Total=mm.interest(5000,Rate,Years,Periods);

      output;

   end;

enddata;

run;

quit;

title;

Figure 4.5: Output from DS2 DATA Program Using the Packaged INTEREST Method

image

4.4 Object-Oriented Programming with DS2 Packages

4.4.1 General Considerations

The ultimate power of the DS2 package comes from using it to enable object-oriented application design. In object-oriented programming, software objects can be used to model real-world objects, and existing objects can then be assembled to model other, more complex objects without having to re-write or re-test the existing objects’ functionality. This can significantly speed up software development cycles. For those who want to “kick it up a notch” with DS2 packages, we’ll take a brief look at using package instances as objects in a DS2 program.

You can think of a package as a “class” of object. The package serves as the template from which an instance of that object is created at execution time. The template contains the necessary methods to create and destroy that instance, provide the object’s behaviors (methods), and keep track of the object’s current state (private global variables). For example, consider a household lamp. A lamp has functions (methods), such as operating its switch, and it has states—either on or off. In a DS2 package created to emulate a lamp, we would write methods to provide the lamp’s functions and create global package variables to keep track of the lamp’s state.

4.4.2 Designing an Object

Although packages cannot include the automatically executed system methods INIT, RUN, and TERM, every package includes two special methods called the constructor and destructor that are automatically triggered. Constructor and destructor methods do not have return types and cannot return values.

The destructor method is always named DELETE. Every package includes a DELETE method, because certain standard routines are necessary to “destroy” an object (clean up and release system resources) whenever that object is no longer needed. So even if you don’t write one, the DS2 PACKAGE program automatically creates a standard DELETE method for you when the package is built. Whenever a package instance goes out of scope in DS2, the DELETE method executes automatically, and the package instance is destroyed. You can explicitly write a custom DELETE method for your package, if desired. The method you write doesn’t completely replace the standard DELETE method provided by DS2; it merely adds your custom cleanup routines to the routines that DS2 normally uses to destroy a package instance.

The constructor method is automatically executed when a package is instantiated and has the same name as the package itself. For example, if your package was stored in work.test, the constructor method name is TEST. A null constructor method is automatically generated by the DS2 PACKAGE program if you choose not to write one. If you choose to write a user-defined constructor method, it can be overloaded if desired. When using your package in a subsequent DS2 program, the constructor method executes when a DECLARE PACKAGE statement containing constructor arguments is compiled or when a declared package instance is used in an assignment statement with the _NEW_ operator. We’ll use the constructor method in our example program to perform those tasks required every time we create a new instance of an object.

Packages can also include global variable declarations. These variables have package scope—that is, they are globally available to all methods within the package instance, but do not populate the PDV. You can use global variables in your package to keep track of the instance’s status.

When creating complex packages, you will eventually run into a couple of issues I’d like to address here: writing a method definition which calls another user-defined method that has not yet been defined, and writing packages that include variables and methods you would prefer to keep private. Private methods and variables cannot be accessed directly by the end user of your package.

SAS 9.4M4 updated the DS2 language to include the ability to declare private variables and methods in a package. In the following example, we will define a package that creates a lamp object. Each lamp has global variables named lampName and lampIsOn to keep track of the status of the lamp, an overloaded constructor method, and other methods to enable us to query and control the status of the lamp. We want the end user to use the click() method to turn the lamp on and off instead of directly accessing the turnOn() and turnoff() methods, and the lampGetStatus() method to obtain the status of the lamp.

Here is our first attempt:

proc ds2 ;

   package work.lamp /overwrite=yes;

 

   /*Global variables track instance name and lamp status*/

   dcl varchar(15) lampName;

   dcl tinyint lampIsOn;

 

   /* Constructor method is overloaded */

   method lamp();

      LampIsOn=0;

      put '*** Instantiating a lamp ***';

      lampName='Unnamed';

      put lampName 'lamp status not set. It is off (default).';

      put;

   end;

   method lamp(varchar(15) name);

      put '*** Instantiating a lamp ***';

      lampName=name;

      put lampName 'lamp status not set. It is off (default).';

      put;

   end;

   method lamp(varchar(15) name, varchar(3) state);

      put '*** Instantiating a lamp ***';

      lampName=name;

      state=lowcase(state);

 

      if state='on' then

         do;

            LampIsOn=1;

            put lampName 'lamp is on.';

         end;

      else if state='off' then

         do;

            LampIsOn=0;

            put lampName 'lamp is off.';

         end;

      else

         do;

            LampIsOn=0;

            put 'The specified state must be ''off'' or ''on''.';

            put 'You specified' state $quote. ' for the' name 'lamp.';

            put lampName 'lamp is off (default).';

         end;

      put;

   end;

   method lampGetStatus() returns varchar(3);

 

      if lampIsOn=1 then

         return 'on';

      else

         return 'off';

   end;

   method lampGetName() returns varchar(15);

      return lampName;

   end;

   method lampSetName(varchar(15) myName);

      put lampName 'lamp name set to' myName;

      lampName=myName;

   end;

   method turnOn();

      LampIsOn=1;

      put lampName 'lamp was turned on.';

   end;

   method turnOff();

      LampIsOn=0;

      put lampName 'lamp was turned off.';

   end;

   method click();

      dcl varchar(4) state;

      lampIsOn=not(lampIsOn);

      state=cats(lampGetStatus(),'.');

      put lampName 'lamp was toggled' state;

   end;

   endpackage;

   run;

quit;

SAS Log:

NOTE: Created package lamp in data set work.lamp.

NOTE: Execution succeeded. No rows affected.

Let’s write a DS2 data program that instantiates and manipulates a lamp and tests the methods:

proc ds2 ;

   data _null_;

      /* Instantiated with neither name nor status provided */

      dcl package work.lamp test();

      dcl varchar(3) Status;

      method statusCheck();

         dcl varchar(15) name status;

         put;

         put 'The status of the lamp is:';

         name=test.lampGetName();

         status=test.lampGetStatus();

         put name 'lamp is' status;

      end;

      method init();

         put;

         put '********************************************';

         put 'After instantiation: ';

         statusCheck();

         put '********************************************';

         put;

         put;

         put '********************************************';

         put 'Directly setting variable values or using turnOff/On';

         put;

         test.lampName='Private Lamp';

         test.lampIsOn=1;

         statusCheck();

         test.turnOff();

         statusCheck();

         test.turnOn();

         statusCheck();

         put '********************************************';

         test.click();

         put '********************************************';

         put 'After manipulating with click(): ';

         statusCheck();

         put '********************************************';

      end;

   enddata;

   run;

quit;

SAS Log:

*** Instantiating a lamp ***

Unnamed lamp status not set. It is off (default).

********************************************

After instantiation:

 

The status of the lamp is:

Unnamed lamp is off

********************************************

********************************************

Directly setting variable values or using turnOff/On

 

The status of the lamp is:

Private Lamp lamp is on

Private Lamp lamp was turned off.

 

The status of the lamp is:

Private Lamp lamp is off

Private Lamp lamp was turned on.

 

The status of the lamp is:

Private Lamp lamp is on

********************************************

Private Lamp lamp was toggled off.

********************************************

After manipulating with click():

 

The status of the lamp is:

Private Lamp lamp is off

********************************************

Well, I was able to directly access and change the values of the package variables lampName and lampIsOn, and I was able to directly turn the lamp on and off using the turnOn() and turnOff() methods. I need to do something to protect these items in the lamp object. I’ll use the PRIVATE access modifier when declaring those variables and when defining methods that I’d rather the user did not access directly:

proc ds2;

   package work.lamp /overwrite=yes;

 

   /*Private global variables track instance name and lamp status*/

   /*To retrieve values at run time, use lampGetStatus() and     */

   /*lampGetName() */

   dcl private varchar(15) lampName;

   dcl private tinyint lampIsOn;

   

   /* Private constructor method is overloaded */

   /* Constructor method is overloaded */

   private method lamp();

      put '*** Instantiating an unnamed lamp ***';

      lampIsOn=0;

      lampName='Unnamed';

      put lampName 'lamp status not set. It is off (default).';

      put;

   end;

   private method lamp(varchar(15) name);

      put '*** Instantiating lamp' name ' ***';

      lampIsOn=0;

      lampName=name;

      put lampName 'lamp status not set. It is off (default).';

      put;

   end;

   private method lamp(varchar(15) name, varchar(3) state);

      put '*** Instantiating lamp' name ' ***';

      lampName=name;

      state=lowcase(state);

      lampIsOn=0;

      if state='on' then

         do;

            lampIsOn=1;

            put lampName 'lamp is on.';

         end;

      else if state='off' then put lampName 'lamp is off.';

      else

         do;

            put 'The specified state must be ''off'' or ''on''.';

            put 'Lamp ' name 'cannot be ' state $quote. '.';

            put lampName 'lamp is off (default).';

         end;

      put;

   end;

   private method turnOn();

      LampIsOn=1;

      put lampName 'lamp was turned on.';

   end;

   private method turnOff();

      LampIsOn=0;

      put lampName 'lamp was turned off.';

   end;

   method lampGetStatus() returns varchar(3);

      if lampIsOn=1 then

         return 'on';

      else if lampIsOn=0 then

         return 'off';

      else return '???';

   end;

   method lampGetName() returns varchar(15);

      return lampName;

   end;

   method lampSetName(varchar(15) myName);

      put lampName 'lamp name set to' myName;

      lampName=myName;

   end;

   method lampClick();

      dcl varchar(4) state;

      state=cats(lampGetStatus(),'.');

      lampIsOn=not(lampIsOn);

      put lampName 'lamp was toggled' state ;

   end;

   /* Let's add a method for directly turning a lamp on or off */

   method lampClick(varchar(3) state);

      dcl varchar(4) status;

      state=upcase(state);

      if state not in ('ON','OFF') then do;

         put 'Attempt to set' lampName 'to' status

           'not successful.';

         status=cats(lampGetStatus(),'.');

         put lampName 'lamp remains' status;

      end;

      else do;

         lampIsOn=if state='ON' then 1 else 0;

         status=cats(lampGetStatus(),'.');

         put lampName 'lamp was turned' status;

      end;

   end;

   endpackage;

   run;

quit;

SAS Log:

NOTE: Created package lamp in data set work.lamp.

NOTE: Execution succeeded. No rows affected.

Let’s test it again to make sure we’ve got everything properly locked down:

proc ds2;

   data _null_;

      /* Instantiated with neither name nor status provided */

      dcl package work.lamp lamp1();

      dcl varchar(3) Status;

      method statusCheck();

         dcl varchar(15) name status;

         put;

         name=lamp1.lampGetName();

         status=lamp1.lampGetStatus();

         put 'The lamp named' name 'is' status;

      end;

      method init();

         put;

         put '********************************************';

         put 'After instantiation: ';

         statusCheck();

         put '********************************************';

         put;

         put;

         put '********************************************';

         put 'Directly setting variable values or using turnOff/On';

         put;

/* When attempting to access private variables and methods,   */

/* each line generates an error and must be tested separately */

         lamp1.lampName='Private Lamp';

/*         lamp1.lampIsOn=1;*/

/*         lamp1.turnOff();*/

/*         statusCheck();*/

/*         lamp1.turnOn();*/

/*         statusCheck();*/

      end;

   enddata;

   run;

quit;

SAS Log (compilation from various attempts):

ERROR: Compilation error.

ERROR: Attempt to get/set attribute lampName which has PRIVATE access.

ERROR: Attempt to get/set attribute lampIsOn which has PRIVATE access.

ERROR: Attempt to call method turnOff which has PRIVATE access.

ERROR: Attempt to call method turnOn which has PRIVATE access.

Success! Before we use our new lamp object in a larger project, let’s write a DS2 data program that instantiates and manipulates several lamps and puts all of the methods through their paces. We will use this program to do quality assurance (QA) testing of the code for our new lamp object to prove that it works as expected:

proc ds2;

   data _null_;

      /* 4 instances of lamp for execution time instantiation */

      dcl package work.lamp desk;

      dcl package work.lamp floor;

      dcl package work.lamp stove;

      dcl package work.lamp task;

      dcl varchar(3) Status;

      method statusCheck();

         dcl varchar(15) name status;

         put;

         status=desk.lampGetStatus();

         name=desk.lampGetName();

         put 'The' name 'lamp is' status;

         status=floor.lampGetStatus();

         name=floor.lampGetName();

         put 'The' name 'lamp is' status;

         status=stove.lampGetStatus();

         name=stove.lampGetName();

         put 'The' name 'lamp is' status;

         status=task.lampGetStatus();

         name=task.lampGetName();

         put 'The' name 'lamp is' status;

      end;

      method init();

         /* Instantiate the lamps */

         put;

         put '********************************************';

         /* Instantiated with name and status */

         desk=_NEW_ work.lamp('Desk', 'on');

         /* Instantiated with only name */

         floor=_NEW_ work.lamp('Floor');

         /* Instantiated with neither name nor status */

         stove=_NEW_ work.lamp();

         /* Instantiated with name and incorrect status */

         task=_NEW_ work.lamp('Task', 'new');

         put '********************************************';

      end;

      method run();

         dcl varchar(15) lampName;

         put 'Manipulating state with lampClick(''on/off''):';

         put;

         stove.lampSetName('Stove');

         desk.lampClick('OFF');

         floor.lampClick('ON');

         stove.lampClick('CLICK');

         task.lampClick('OFF');

         put '********************************************';

         put 'After manipulating with lampClick(''on/off''):';

         statusCheck();

         put '********************************************';

      end;

      method term();

         put 'Manipulating state with lampClick():';

         put;

         desk.lampClick();

         floor.lampClick();

         stove.lampClick();

         task.lampClick();

         put '********************************************';

         put 'After manipulating with lampClick():';

         statusCheck();

         put '********************************************';

      end;

 

   enddata;

   run;

quit;

SAS Log:

********************************************

*** Instantiating lamp Desk  ***

Desk lamp is on.

 

*** Instantiating lamp Floor  ***

Floor lamp status not set. It is off (default).

 

*** Instantiating an unnamed lamp ***

Unnamed lamp status not set. It is off (default).

 

*** Instantiating lamp Task  ***

The specified state must be 'off' or 'on'.

Lamp  Task cannot be  "new".

Task lamp is off (default).

 

********************************************

Manipulating state with lampClick('on/off'):

 

Unnamed lamp name set to Stove

Desk lamp was turned on.

Floor lamp was turned off.

Attempt to set Stove to click not successful.

   The specified state must be 'off' or 'on'.

   Stove lamp remains off.

Task lamp was turned on.

********************************************

After manipulating with lampClick('on/off'):

 

The Desk lamp is on

The Floor lamp is off

The Stove lamp is off

The Task lamp is on

********************************************

Manipulating state with lampClick():

 

Desk lamp was toggled off.

Floor lamp was toggled on.

Stove lamp was toggled on.

Task lamp was toggled off.

********************************************

After manipulating with lampClick():

 

The Desk lamp is off

The Floor lamp is on

The Stove lamp is on

The Task lamp is off

********************************************

As you can see, the lamp object is working just as we intended. It emulates a real lamp—we can move it to a new location (give it a new name), turn it on or off directly or toggle the status, and check to see whether the lamp is on or off. Our QA testing is complete and successful.

4.4.3 Using Objects as Building Blocks

The beauty of using objects in programming is that, once defined and tested, we can reuse them over and over as building blocks for new objects or new applications without having to repeat QA testing on the established objects. We can now use the lamp package as an object for building a more complex house package. Let’s build a house object that contains four lamps and control the house from another DATA program. In order to keep our package code organized, I’d like to define all of the private methods first. Unfortunately, some of the private methods call other methods that will be defined later, which will cause a syntax error when compiled. DS2 provides the FORWARD statement for just this occasion. The FORWARD statement directs the compiler to delay checking for undefined methods until the entire package code has been read:

/* The new HOUSE object uses some instances of LAMP */

proc ds2;

   package work.house /overwrite=yes;

   dcl varchar(15) houseName;

   dcl package work.lamp l1('Desk','On');

   dcl package work.lamp l2('Floor','Off');

   dcl package work.lamp l3('Oven','On');

   dcl package work.lamp l4('Task','Off');

   /****************************************************************

   PRIVATE methods are defined first. Because houseGetLampList will  

   be defined later, the FORWARD statement is required.

   ****************************************************************/

   forward houseGetLampList;

   /* PRIVATE overloaded constructor method. */

   private method house();

      houseName='Unnamed';

      put '*** Instantiating house' houseName $quote. '***';

   end;

   private method house(varchar(15) name);

      dcl varchar(15) lampNo[4];

      houseName=propcase(name);

      put '***' houseName $quote.

          ' instantiated with the following lamps ***';

      houseGetLampList(lampNo);

      put lampNo[*]=;

   end;

   /* PRIVATE method. */

   private method moveLampTo(package work.lamp thisLamp

                           , varchar(15) newName);

      thisLamp.lampSetName(propcase(newName));

   end;

   /* IN_OUT array parameter gets all lamp names */

   method houseGetLampList(in_out varchar l[4]);

      l[1]=propcase(l1.lampGetName());

      l[2]=propcase(l2.lampGetName());

      l[3]=propcase(l3.lampGetName());

      l[4]=propcase(l4.lampGetName());

   end;

   /* IN_OUT array parameter gets all lamp statuses */

   method houseGetLampStatus(in_out varchar s[4]);

      s[1]=l1.lampGetStatus();

      s[2]=l2.lampGetStatus();

      s[3]=l3.lampGetStatus();

      s[4]=l4.lampGetStatus();

   end;

   method houseLampMoveFromTo(varchar(15) oldName

                            , varchar(15) newName);

      dcl varchar(15) lamp[4];

      dcl int i dupe;

      oldName=propcase(oldName);

      newName=propcase(newName);

      houseGetLampList(lamp);

      do i=1 to dim(lamp);

         if newName=lamp[i] then dupe=1;

      end;

      if dupe then put newName

           'already has a lamp. Can''t move' oldName 'lamp there.';

         else if oldName=lamp[1] then moveLampTo(l1,newName);

         else if oldName=lamp[2] then moveLampTo(l2,newName);

         else if oldName=lamp[3] then moveLampTo(l3,newName);

         else if oldName=lamp[4] then moveLampTo(l4,newName);

         else put oldName 'lamp does not exist!';

   end;

   method houseLampCheck();

      dcl varchar(15) l[4];

      dcl varchar(3)  s[4];

      dcl int i;

      houseGetLampList(l);

      houseGetLampStatus(s);

      put;

      put 'The status of the lamps in' houseName 'house:';

      do i=1 to dim(l);

         put l[i] 'is currently's[i] ;

      end;

   end;

   method houseLampClick();

     put 'You can''t click an unnamed lamp!';

   end;

   method houseLampClick(varchar(15) lampName);

      dcl varchar(15) lamp[4];

      lampName=Propcase(lampName);

      houseGetLampList(lamp);

      if lampName=lamp[1] then l1.lampClick();

      else if lampName=lamp[2] then l2.lampClick();

      else if lampName=lamp[3] then l3.lampClick();

      else if lampName=lamp[4] then l4.lampClick();

      else put lampName 'lamp not recognized.';

   end;

   endpackage;

   run;

quit;

 

proc ds2;

   data _null_;

      dcl package work.house Hideout('Hideout');

      method init();

         dcl varchar(15) lampNo[4];

         dcl int i j;

         Hideout.houseGetLampList(lampNo);

         put '*** Toggling lamps ***';

         do j=1 to dim(lampNo);

            Hideout.houseLampClick(lampNo[j]);

         end;

         put;

         put '*** After Toggling lamps ***';

         Hideout.houseLampCheck();

         put '*******************************************';

         put;

         put '*** Moving Lamps ***';

         Hideout.houseLampMoveFromTo('Attic','Garage');

         Hideout.houseLampMoveFromTo('Task','Oven');

         Hideout.houseLampMoveFromTo('Task','Garage');

         put;

      end;

   enddata;

   run;

quit;

SAS Log:

*** Instantiating lamp Desk  ***

Desk lamp is on.

 

*** Instantiating lamp Floor  ***

Floor lamp is off.

 

*** Instantiating lamp Oven  ***

Oven lamp is on.

 

*** Instantiating lamp Task  ***

Task lamp is off.

 

*** "Hideout" instantiated with the following lamps ***

lampNo[1]=Desk lampNo[2]=Floor lampNo[3]=Oven lampNo[4]=Task

*** Toggling lamps ***

Desk lamp was toggled off.

Floor lamp was toggled on.

Oven lamp was toggled off.

Task lamp was toggled on.

 

*** After Toggling lamps ***

 

The status of the lamps in Hideout house:

Desk is currently off

Floor is currently on

Oven is currently off

Task is currently on

*******************************************

 

*** Moving Lamps ***

Attic lamp does not exist!

Oven already has a lamp. Can't move Task lamp there.

Task lamp moved to Garage

We could now start building more objects with which to populate our house, or even build community objects that include a collection of houses.

One last point about documenting packages created for use as objects: these types of packages frequently include methods that do not require parameters, and these methods do not lend themselves well to self-documenting by overloading. In this case, consider including complete documentation for all methods in the CONTENTS method. In any case, using DS2 packages as objects opens a whole new realm of possibilities for programming with DS2!

4.4 Review of Key Concepts

   Methods are named blocks of code that are written within DS2 data, thread, and package program blocks.

   In DS2 programs, all executable code must be part of a method.

   Methods come in two types:

   System methods:

̶   Execute automatically, and cannot be explicitly called by name

̶   Do not accept parameters

̶   Do not return values

   User-defined methods:

̶   Execute only when called by name

̶   Can accept parameters

̶   Can either return a value or modify one or more IN_OUT parameter values at the call site.

   DS2 packages are the objects of the DS2 programming language. Here are some characteristics for your consideration:

   Stored in SAS libraries.

   Consist of a collection of user-defined methods and variables.

   Private methods and variables are accessible only from other methods within the package and cannot be referenced directly via dot notation.

   Simplify creating and sharing extensions to the DS2 language.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.104.95