CS 51 Week 6, L2:

Scheme --> C --> C++


Today's topics: Announcements


Week 6 Pithy Design Quotes

"I broke my arm trying to fold a bed. It wasn't the kind that folds." --- Steven Wright on type errors

A programming language is low level when its programs require attention to the irrelevant.

Life after Scheme

Some nice features of Scheme



"Lisp isn't a language, it's a building material." - Alan Kay


These properties make it wonderful for building new abstractions and exploring/using different programming models. However, as the scale of a software project grows, a whole new set of problems appear.

The Issue Of Scale: What happens when?


A programming language is low level when its programs require attention to the irrelevant.


Some (maybe) not so nice features of Scheme

As we build programs at this new scale, it becomes far more important that we are able to encourage, and enforce, conventions and good design practices. One way to do this is to build them into the language itself. In this next section we will see that some of the data abstractions we built into Scheme (i.e. Objects) will now be part and parcel of the language we use.

New Tools for our Toolchest

The new language we will use is C++, a language that has been used extensively in large-scale software. Conceptually it embodies some new features, and represents some new tradeoffs.

For the next few week, we will learn how objects can be used to construct interesting higher-level abstractions, that simply design and reuse for many large problems. A rough outline of upcoming weeks

What is an Object?

We will start with a very simple observation about data abstraction. When one creates a new type of data (by composing primitive data types), one almost always creates a set of functions to operate on it. Thus data abstraction is both the "data" and the "interface".

For example: Lists (cons, car, cdr, list?), Dictionaries (create, extend, lookup), Sudoku Board (create, fill-entry, lookup-entry). Lets build our own example.

The DOG Show!

We are organizing a dog how at Harvard, for all those students and faculty who happen to bring their dogs into work. In order to keep track of it all, we need to create a new dog data structure. The data structure should contain:

  • Name
  • Owner
  • Address
  • Age
  • Breed
  • Category

Some of the things we expect to be able to do:

= Create a new dog structure, given a name and breed (at least)
= Look at the variables in a dog structure
= Allow some variables to be changed (owner, address, age, category)
= But disallow changing others (name, breed)

We will now design a dog object in three languages: Scheme, C and C++.
Some things to think about are:



DOG In Scheme

We will implement Dog as a message-passing object. This means that dog is really a function that takes a "message" and based on that message, it calls the correct helper function.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Creating a DOG Class for a Dog Show    ;;
;; Message-passing Objects In Scheme      ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; The CONSTRUCTOR
(define (make-dog thename thebreed)

  ;; The INTERNAL STATE (concrete representation) of Dog
  (let ((name thename)
	(owner "Not Assigned")
	(address "Not Assigned")
	(age 0)
	(breed thebreed)
	(category "Not Assigned"))

    ;; The INTERFACE for accessing Dog
    (lambda (msg . args)                 ;; this syntax means optional arguments 
      (cond ;; SELECTORS
	     ((equal? msg 'getName) name)
	     ((equal? msg 'getOwner) owner)
	     ((equal? msg 'getAddress) address)
	     ((equal? msg 'getAge) age)
	     ((equal? msg 'getBreed) breed)
	     ((equal? msg 'getCategory) category)

	     ;; MUTATORS
	     ((equal? msg 'setName)     "ERROR: Dog 'setname not allowed")
	     ((equal? msg 'setBreed)    "ERROR: Dog 'setbreed not allowed")
	     ((equal? msg 'setOwner)    (set! owner (car args)))
	     ((equal? msg 'setAddress)  (set! address (car args)))
	     ((equal? msg 'setAge)      (set! age (car args)))
	     ((equal? msg 'setCategory) (set! category (car args)))

	     ;; METHODS
	     ((equal? msg 'printinfo) 'do-something-nicer)
	     
	     (else "Urecognized message"))
      )))

TESTING


(define mydog (make-dog "Alyssa" "BloodHound"))
(define yourdog (make-dog "BenBitdiddle" "BloodHound"))

;; Evaluating mydog should return a procedure
mydog

;; Selectors
(mydog 'getName)
(yourdog 'getName)
(mydog 'getBreed)
(mydog 'getCategory)

;; Mutators
(mydog 'setName "noname")      <--- returns an error!!! 

(mydog 'getCategory)           <--- this is ok
(mydog 'setCategory "small")
(mydog 'getCategory)


Some Observations

We could define how things are allowed to be accessed quite precisely. We could enforce our abstraction barriers. And the code is nice and small. (Note however that we ARE USING MUTABLE STATE, without which this is much less clean to implement)

However we had to write the whole message dispatch function ourselves which is a little tedious (and easy to make mistakes). In OOP languages, the message dispatch structure is created by the compiler. But this is essentially how it works -- the data determines what the message means

Also, the interface and implementation are mixed together

Why is this a problem? Well, for starters its not very easy to read and immediately understand what the programers intentions were (what the various message functions should be doing) without reading how the function is implemented. There is also very little error checking here. What are valid datatypes for the "owner" field? Adding in type checking would significantly expand the code, and slow down run-time behavior. Furthermore, the "contracts" (input/output data types) are all implicit and thus the user has to guess the programmers intentions.

DOG in C

We will implement Dog as a "struct" in C. Structs provide some of the data-directed programming because given a data, it maps field names to the correct location.

/******************************************/
/* Creating a DOG structure for a Dog Show */
/* Objects In C, using "struct"            */
/*******************************************/

// To compile gcc -o dogstruct dogstruct.c
// To run ./dogstruct

#include 
#include 

// STRUCTURE DECLARATION
//-------------------------
// (usually placed in the .h file)

struct dog {
  char *name;
  char *owner;
  char *address;
  int age;
  char *breed;
  char *category;
};

INTERFACE AND METHODS

// Note that both selectors and mutators come for free
// The compiler takes care of the "dispatch" from data to appropriate field
// The constructor also comes for free, but may not be what we want

void
test_dogstruct1() {

  // INTERFACE of CONSTRUCTORS, DESTRUCTORS, MUTATORS, SELECTORS

  // Three Methods for CONSTRUCTION
  //-------------------------------
  // 1. declare (allocated on the stack)
  // 2. declare and assign statically (same)
  // 3. declare by explicitly allocating memory (allocated on the heap)

  struct dog foo;

  struct dog foonice ={"Rascal", "Me", "Home", 8, "Samoyed", "big"};

  struct dog *fooref;
  fooref = (struct dog *)malloc(sizeof(struct dog));

  if (fooref == NULL) {
    printf("Dog test: Malloc returning null");
    return;
  }


  // SELECTORS
  //----------
  // Note that C provides us selectors for free! 
  // The data itself is responsible for its methods
  // Instead of writing a function DOG.NAME(FOO) 
  // I can do FOO.NAME or FOOREF->NAME
  // And the compiler will complain if I do FOO.WEIGHT (undefined field)

  // These selectors however DO NOT TEST if the data structure has anything assigned!
  // print garbage or segfault (maybe segfault is better!)

  printf("\n TESTING SELECTORS \n");
  //BAD printf("The dog named %s is %d years old \n", foo.name, foo.age);
  printf("The dog named %s is %d years old \n", foonice.name, foonice.age);
  //BAD printf("The dog named %s is %d years old \n", fooref->name, fooref->age);


  // MUTATORS
  //----------
  // Note that C provides us mutators for free! 
  // (regardless of whether we wanted something to be mutable..)

  fooref->name = "Roomba";
  fooref->owner = "You";
  fooref->address = "YourHome";
  fooref->age = 15;
  fooref->breed = "BloodHound";
  fooref->owner = "Medium";

  printf("\n TESTING MUTATORS \n");
  printf("The dog named %s is %d years old \n", fooref->name, fooref->age);

}

Some Observations

The nice part so far is that we are able to create a "real" data structure. Struct provides us many things for free.

However we face a couple of problems

DOG in C++

Now will we create our Dog structure in C++, a language designed to support the kinds of abstractions we built by hand in Scheme, and tried to recreate in C. As we will see, the language is designed to describe the kind of functionality we are looking for, and provides support to give our abstraction barriers real teeth.
/*******************************************/
/* Creating a DOG Class for a Dog Show     */
/* Objects In C++                          */
/* CS51, Nagpal, 2008	                   */
/*******************************************/

// To compile g++ -o dogclass dogclass.cxx
// To run ./dogclass

// IO LIBRARIES
//-------------
// In C++ we will use iostream instead of stdio
// we will also be able to use a nice string library

#include 
#include 
using namespace std;




CLASS DECLARATION

Typically, the Class Declaration should be in the header file (dogclass.h) The header file serves as a "interface specification" For now we will write this in place to see the whole spec together

class Dog {
public: 

  // CONSTRUCTORS
  Dog();                             // Default constructor
  Dog(string name, string breed);    // A more reasonable one

  // DESTRUCTORS
  ~Dog();                             // Default constructor

  // SELECTORS
  string getName();              // Since we made all data private
  string getOwner();             // we must declare explicit selectors
  string getAddress();
  int getAge();
  string getBreed();
  string getCategory();

  // MUTATORS
  void setOwner(string ownername);  // Note how easily we can protect
  void setAge(int years);           // name and breed from being changed
  void setAddress(string address);
  void setCategory(string category);

  // METHODS
  void printInfo();             // Always a nice touch

  // TESTING
  static void testsuite();     // This runs a set of tests for the Class

private:
  string name;
  string owner;
  string address;
  int age;
  string breed;
  string category;
};
// note that like all declarations, this must end with a semicolon


This class declaration declares our intentions quite clearly. This class declaration is called the "INTERFACE" for our dog data abstraction. Now we can "IMPLEMENT" our interface.

CLASS DEFINITION (IMPLEMENTATION)

Here we will implement each of the methods that we specified in our interface. Our implementations can further control how the data abstraction is used. For example, our constructors can explicitly assign default values and thus safely create objects.


// CONSTRUCTORS
//---------------
Dog::Dog(){                 // overriding the implicit "default" constructor
  name = "Not Assigned";
  owner = "Not Assigned";
  address = "Not Assigned";
  age = 0;
  breed = "Not Assigned";
  category = "Not Assigned";
}

Dog::Dog(string aname, string abreed){ // creating our own constructor
  name = aname;
  owner = "Not Assigned";
  address = "Not Assigned";
  age = 0;
  breed = abreed;
  category = "Not Assigned";
}

// DESTRUCTORS
//---------------
Dog::~Dog() { } // Default destructor, does nothing special



// SELECTORS
//---------------
string Dog::getName()    {return name;}
string Dog::getOwner()   {return owner;}
string Dog::getAddress() {return address;}
int Dog::getAge()        {return age;}
string Dog::getBreed()   {return breed;}
string Dog::getCategory(){return category;}

// MUTATORS
//---------------
void Dog::setOwner(string ownername)    {owner = ownername;}
void Dog::setAddress(string newaddress) {address = newaddress;}
void Dog::setAge(int years)             {age = years;}
void Dog::setCategory(string cat)       {category = cat;}

// METHODS
//---------------
void 
Dog::printInfo() {
  cout << "DOG NAME: " << getName() << " and Breed: " << getBreed() << " \n";
  cout << "Dog belongs to " << getOwner() << " at address " << getAddress() << " \n";
  // Note the printing is much nicer to express in C++
  // we send things to the cout stream, and they are converted to string implicitly
  // Also note that we could have used "name" instead of "getName()"
  // why is it better to use getName() ?
}


TESTING our Dog Class

In CS51, we will use the convention that for every class you must define a static function "testsuite" that runs a bunch of tests for that class.

// STATIC => that the function is bound to the class, 
//           and not to any particular instance (object) of this class
// Its a way of creating functions that should "logically" be part of the class 
// even though they are not associated with a piece of data.

void
Dog::testsuite() {

  // Three Methods for Construction
  //-------------------------------
  // 1. declare using simple constructor
  // 2. declare using our defines consructor
  // 3. declare by explicitly allocating memory on the heap
  // NOTE: in C++ use NEW and DELETE (not malloc/free)

  Dog foo;
  Dog foo2("Harvey", "Retriever");
  Dog *fooref = new Dog("Harold", "Retriever");

  // Now we can look at what is inside these structures
  // We have succesfully forced the data structure to have default values
  cout << "\n TESTING PRINTINFO AND SELECTORS \n \n";
  foo.printInfo();
  foo2.printInfo();
  fooref->printInfo();

  // We can test our ability to change values in the dog structures
  cout << "\n TESTING MUTATORS \n";
  cout << "Original Owner is " << foo2.getOwner() << "\n";
  foo2.setOwner("Someone else");
  cout << "New Owner is: " << foo2.getOwner() << "\n";

  // Note that attempting to mutate something protected, results in compiler errors
  //    foo2.setName("Should not do this");
  //    foo2.name = "hello";
  //
  // WHAT THE COMPILER SAYS if you do g++ -o dogclass dogclass.cxx
  // -- dogclass.cxx: In function 'void test_dogclass()':
  // -- dogclass.cxx:161: error: 'class Dog' has no member named 'setName'
  // -- dogclass.cxx:67: error: 'std::string Dog::name' is private
  // -- dogclass.cxx:162: error: within this context
  
  // Calling the destructor
  delete fooref;
}

int
main(int argc, char *argv[])
{
  Dog::testsuite();   // this is how you call a static method
  return 0;
}

Object-Oriented Programming (OOP)

A class is a data abstraction that encompasses both the state of an object and the operations that are permitted on that object. An instance of the class (something made by using the constructor) is an object. Functional interfaces are called methods.

Classes provide:



This is the "essence" of the idea. But this support has many interesting impacts. It allows the programmer to easily reason and write data abstractions, and then quickly move on to more complex things, like composing data objects in various ways. We will learn new techniques for modularity and design, that build on this simple idea of objects.






CS51, Spring 2008, Radhika Nagpal