CS 396 |
OOPs World:
a basic OOP interpreter
|
Rules: Individual Effort, no collaboration | |
DUE DATES:
|
This is probably the coolest programming assignment ever imagined. When you finish it, you will have (a) a solid understanding of OOP language concepts and behavior and (b) an incredible sense of power and knowledge. It isn't everyday that you write your own private language!
More specifically, you will implement a simple interpreted object-oriented programming system. The system will read in a data file containing "class definitions"; the format for these definitions is given in detail below. You may assume that the definitions given to your system are syntactically correct (although I did build some basic error-checking into my implementation out of habit). After the definitions are loaded, the system will allow the user to interactively instantiate defined classes, and manipulate the resulting objects. Your system will provide graceful error-handling, class fields and methods, self-reference (i.e., the Java "this" keyword; the Python "self") and other standard features of OOP environments.
Objectives:
Your assignment is to implement an interpreted object-oriented programming environment. By "interpreted" I mean that, rather than compiling your source files (containing the class definitions), the user simply loads the class definitions into the interpreter environment. This "defines" the classes. The user may then instantiate classes to her heart's content and, of course, pass messages to the resulting instances.
Class definitions are stored in a text file. The file may have Scheme comments embedded in it as well (the built-in "read" method ignores these). The general format for class definitions is as follows:
(class <class-name> (parent: <parent>) (constructor_args: <constructor_args>) (ivars: (name1 val1) (name2 val2)...(namen valn)) (methods: (<meth-name> (<args>) (<body>) ) (<meth-name> (<args>) (<body>) ) <etc> ))
This definition format is quite straightforward; syntactic conventions are of course lisp-like. Details:
Here are a couple of sample class definitions (one plain one, one that shows inheritance at work) that illustrate the most important aspects of class functionality. Note that, in the definition above, a provision is made for specifying a "parent" class. For Part I of the assignment, this is just a placeholder; in PartII of the assignment, you'll implement the inheritance feature.
Your program should provide several important top-level functions:
(load-classes "filename") ---> loads the class definitions contained in the file called "filename". As in the sample file given above, a single file could contain many class definitions. Your load-classes function should cause these definitions to be loaded and "compiled" (processed), resulting in the definition of the new classes in the environment. Load-classes should print out the names of each class as it's loaded, i.e., something like "loaded class: the-class-name". This is just so that we can see that it's doing its job and know what classes were loaded. Hint: Here is a simple load-file function that you can use as a starting point.
(new <class-type> <args>) --> The new function takes in the name of a previously defined class, along with whatever constructor args it requires, and creates a new instance of the class.
After you create new objects you should, of course, allow passing of messages to them. So the general plan would be:
> (define obj1 (new myClass arg1 arg2)) ;; Use the loaded classdef to create a new instance of myClass
> (obj1 'method_name <method args>) ;; Invoking the new instance of the object using a method and its args.
Here is a more concrete sample sequence I produced loading the two sample classes given earlier.
Some details:
Turn in a 1-2 page sheet showing your basic object system loading and working on:
Your printout should show you loading the class file(s), creating a few objects, and passing them some messages to exercise their various methods. Your upload should consist a single PDF containing, IN THIS ORDER:
The easiest way to create the single pdf is probably to paste each of these things (with nice headings, and each section starting on new page) into a word processor file...and then export that file as a PDF for upload. Make sure you use a fixed-width font (Courrier is always good) so that the code indenting all works out as it should.
Turn this in as a single PDF file submitted to BBlearn by the date/time specified (see top of page).
As we discussed in lecture, inheritance is one of the most thorny, complex issues that must be dealt with in implementing an OOP system --- although, at the same time, inheritance functionality is also a central feature of the OOP paradigm. If you examine the research literature in OOP, you will find that dozens of experimental languages have been designed to explore variations of different inheritance models --- with much heated argumentation of pros and cons of each. In brief, the decisions made in designing the inheritance model for a language affect nearly every aspect of the language ranging from security of information-hiding, complexity, reliability, performance, and difficulty of implementation.
Implementing fully-functional inheritance is non-trivial. Fortunately, I am asking only for a basic inheritance mechanism. Still, it requires some thought and gaining a deep understanding of what inheritance really means. The goal is for you, in implementing even this rudimentary mechanism, to gain some insight into the challenges associated with design and implementation of an inheritance mechanism in object-oriented languages.
Your assignment is to implement a basic inheritance mechanism to extend the main OOP programming project. You will have noticed that the class definition format I gave for the core assignment included a "placeholder" for parent classes. In principle, implementing inheritance is simple: you simply put the name of a (single) parent class into this slot; all instances of the class thereby inherit all methods and instance variables from the parent class (and its parent class, if applicable). In short, it works just like most modern OOP languages. Some specific characteristics:
Here is a sample run showing some of the inheritance features at work with the simple Point and 3dpoint classes in the class definition sample linked above. When you have that running, here is a more challenging set of classes (more similar to what I'll test with) that explore the full capabilities of inheritance.
If you have further questions about specification details, please come ask me...
You should, as always, begin by thinking the problem through carefully!!! The central question, in particular, is "How can I model an object in Scheme?". This will lead you to consider, in more profound ways than you ever have before, the question: What is an object really? Once you boil your understanding down to the primal essence of what an object is, you will find that it is very simple to model in Scheme.
So here is my suggested implementation plan:
STEP 1: Think about what an object is, how to model one in Scheme. Then follow up and directly define an object for yourself --- just make an "instance" directly. Just one for now. When you have understood and managed this, you will have dealt with the central conceptual piece of this programming challenge! Practically speaking, this will allow you to work the kinks out of being able to pass messages (ie, run "methods") and so on.
Here's a hint to get your started: Scheme is a statically-scoped language, which means that all functions/variables are evaluated in their environment of definition, which is to say, the variables/names that existed at the time the lambda that defined the function was executed. Which means that, in order to support this stringent requirement, Scheme has to keep that environment around in some way --- hence the name "procedure object", ie, it contains not only the procedure, but its environment of definition. And THIS is exactly what we're going to leverage to implement objects -- we'll just make these procedure objects work as real objects! I'm probably giving way too much fun away here, but here is the basic concept:
Welcome to DrRacket, version 5.0.2 [3m].
Language: Pretty Big; memory limit: 128 MB.
> (define basics
(let ((x 2)
(y 3))
(lambda ()
(display (list 'xval: x 'yval: y)))))
> (basics)
(xval: 2 yval: 3)
> >
Is this is coolest thing, or what?! You see how it works? I enclose the call to lambda (which defines the function) within a LET statement, which defines an extended environment of definition for the function. Because the LAMBDA is defined within the LET, the variables defined in the LET are valid and exist at the moment the LAMBDA is defined. So --- to preserve static scoping --- Scheme wraps them up as part of the "environment of definition" for the LAMBDA, which is then tucked into the procedure object that's returned. (Wow, so cool...hmmmm....I wonder if we could leverage this same coolness to define ivars for our objects?!) Of course, the LET ceases to exist right after the lambda is called --- but the environment it defines must stick around because the body of the lambda MUST have access to its environment of definition. Thus, this is the ULTIMATE secure object; only "methods" defined in the enclosed lambda can "see" the ivars defined in the LET! There is no way to violate this hiding, even if you wanted to because the object's security is driven by the scoping mechanisms/guarantees of Scheme! Awesome! SO so cool!
Ok, I think you can take it from there. Obviously you'll need a more sophisticated mechanism for what we technically call "message dispatch" --- dealing with "messages" sent to the object to invoke various methods, deal with shadowing, accommodate the *this* concept, and so on. But this gives you the golden seed...
TAKE THE TIME TO UNDERSTAND everything in step 1. Make some basic objects that accept simple messages, then make the message processing capabilities fancier, deal with multiple signatures for same method name --- all that stuff. Only when you fully understand this are you ready for step2!
STEP 2: ask yourself: how could I *write a function* that would produce such instances (like you just wrote directly in step 1) in mechanistic fashion, i.e., an "object factory" --- so this thing basically has to put together the code you wrote by hand in Part I, then eval it. It is, of course, this factory that is invoked by the call to your new function. Follow up by writing yourself an object factory that produces a particular type of object. This will allow you to work out the kinks in passing in values to this factory function for it to use to initialize the "instance variables" for the new instances it produces.
STEP 3: once you've figured out how to write the "object-factory" manually and have it satisfactorily producing instances for you, then it's time to address the by-now rather ho-hum matter of automatically assembling and defining these object factory functions from the class definitions that you read in from the file. This will be a fairly simple matter of combining the list-munging skills and the Function-Maker effort you mastered in Program #1.
Here are some PRACTICAL POINTERS that introduce some Scheme features that you'll probably find useful for this assignment.
Required pre-submission self-testing: Here is a little "script" in Scheme that loads a given code file, then runs some tests. It's set up to load and test the two test class definition files CLASS1.TXT and inherit.txt that I gave you earlier. This little script is very very similar to what my testing program will do in testing your code. Just put all your files in the same directory and run it: it should load your program, load the test classes, and run some tests. Here is the output it produces. This is an EXCELLENT way to self-test your solution so that you know you didn't accidentally mis-name some functions or otherwise not match function signatures.