Phantasmal MUD Lib for DGD

Phantasmal Site > DGD > DGD LPC Reference > DGD Object Subtypes

Subtypes of DGD's 'object' Type

From: dgd at list.imaginary.com (Felix A. Croes)
Date: Fri Jul  9 16:36:01 2004
Subject: [DGD] object types

Within the next few days, I hope to release an important new feature:
object types.  It is going to be possible to add typechecking to objects:

    /* normal object */
    object obj;

    /* objects that inherit /kernel/lib/user */
    object "/kernel/lib/user" user1, user2;

Object types can be used anywhere where you can normally use 'object':
for variables, function types, function parameters, and in casts.

    obj = user1;	/* always works because obj has no specific type */
    user1 = user2;	/* always works because user2 is of the same type */
    user2 = obj;	/* only works if obj is nil or inherits ... */
			/* ... /kernel/lib/user */

In the example above, the variable 'user1' can only hold an object which
inherits /kernel/lib/user.  To attempt to use a different type of object
results in a runtime error.  There is no compile-time checking for
object types.

This change will affect a great deal of code, so consider 1.2.89 to be
the last stable version for a while.

Regards,
Dworkin

From: dgd at list.imaginary.com (Felix A. Croes)
Date: Sun Jul 11 17:05:01 2004
Subject: [DGD] object types: examples from the kernel lib

Have you ever used function_object() as a way to make sure that an
object can perform the role you want it to perform?  Then object types
can simplify your work.  The kernel library presently uses object types
in a few cases, which I will describe below by way of example.


The kernel library has several objects intended to be inherited by other
objects that want to perform a certain function.  Two such objects are
/kernel/lib/connection and /kernel/lib/user; they perform the connection
and user roles.

The kernel library uses a connection object as a way to communicate with
a remote user.  The connection object also communicates with a
representation of the user in the mud, the user object:

    Internet  <->  connection object  <->  user object

The kernel library creates a connection object as soon as a new
connection from the network is accepted.  A user object is created
(or found, since it may already exist) later.  To make sure that
the communication between these two objects works properly, each
of them inherits the proper role object from /kernel/lib.

An object can perform both roles.  For example,

    Internet  <->  connection object  <->  ssh object  <->  user object

Here the ssh object, which decrypts input from the remote user and
encrypts output from the mud, sits in between the connection object
and the user object.  To the connection object, it performs the user
role.  To the user object, it performs the connection role.  It does
this by inheriting both /kernel/lib/connection and /kernel/lib/user.
There may in fact be several objects in between the outermost connection
and user objects, performing some sort of translation.

This is how the user object used to look:

    static void connection(object conn)
    {
        if (function_object("query_user", conn) == "/kernel/lib/connection") {
            disconnect();
            connection = conn;
        }
    }

That is, the user object has a function called 'connection', which can
be used to set the connection object communicating with that user object.
To make sure that the prospective connection object is really capable of
the role it is supposed to perform, function_object() is used to see if
a function in the object is inherited from /kernel/lib/connection.

Here is how the same function looks at present:

    static void connection(object "/kernel/lib/connection" conn)
    {
        if (conn) {
            disconnect();
            connection = conn;
        }
    }

Unless the connection object is able to do the job, it will not be
accepted as an argument.  The object type in the parameter declaration
ensures that when the function is called, a check is performed to see
that the object actually inherits the proper "role" object.  Note that
nil is still an acceptable value even for a typed object, so the
function should check for that (previously, a nil argument would
result in an error when calling function_object()).

All object types are checked at runtime only.  There is no compile-time
check; in fact there is not even a check that the object type specified
is an existing object.  Each object type path is automatically
translated at compile time by calling the function object_type() in the
driver object.  In the kernel library, this will take care of relative
paths and paths that start with ~.

Here is a one-line example, from /kernel/sys/userd (slightly modified):

    user = (object "/kernel/lib/user") port_manager->select(str);

The user daemon asks the port manager for an appropriate user object,
given a first line of input (which can be a user name, a ssh ident
string, a HTTP GET command, and so on).  To ensure that the port
manager does not accidentally return an improper object, the result
is cast to object "/kernel/lib/user".  If this is not inherited by
the object, a runtime error will result.

Note that in all cases, the object type must be a string constant.
You cannot use a variable which will hold a string value at runtime;
it must be a constant.  However, the constant may be composed of
several concatenated strings. (Yes, even for a function parameter!)

Last example, from the auto object:

    static object this_user()
    {
        object user;

        user = ::this_user();
        while (user && user <- "/kernel/lib/connection") {
            user = user->query_user();
        }
        return user;
    }

The auto object masks this_user(), which by default would return the
connection object.  Instead, it finds the object which performs the
user role (and not, like a ssh object sitting in between, also the
connection role).  To accomplish this it uses the <- operator, which
works like a cast, but which never results in an error.  Instead, it
evaluates to 1 if the role object is inherited, and 0 otherwise.

The <- operator has the same priority as the -> operator, so

    obj <- "/foo" + "/bar"

evaluates to

    (obj <- "/foo") + "/bar"

Use explicit parenthesis for string concatenation, as below:

    obj <- ("/foo" + "/bar")

Regards,
Dworkin

From: dgd at list.imaginary.com (Felix A. Croes)
Date: Sun Jul 11 17:31:01 2004
Subject: [DGD] object types from a Java perspective

In Java, almost all typechecking is done at compile time.  In LPC, object
types are checked at runtime only.  In fact, the object type does not even
have to exist as an object, though in that case the check will of course
always fail.

Knowledge of object (class) types in Java also involves a lot of other 
knowledge, such as the functions callable in that object, and so on.
In LPC such knowledge only exists if the object is inherited.  This is
both a bane and a boon, for in a persistent environment the ability to
arbitrarily change interfaces and functionality can be quite useful.  I
am currently considering implementing abstract objects for LPC
(declaration or inheritance of code-less prototypes in an object not
marked as "abstract" results in a compile-time error), but I am not yet        
sure if abstract functions & objects would "fit" in LPC.

In Java the object must be explicitly named.  In LPC it is named by a
string constant, which may be a concatenation of other string constants.

Since object type checks are performed at runtime, they take ticks at 
runtime.  However the implementation is quite efficient, and results
are cached.

Regards,
Dworkin
 

From: dgd at list.imaginary.com (Par Winzell)
Date: Mon Jul 12 04:56:01 2004
Subject: [DGD] object types: examples from the kernel lib

> Have you ever used function_object() as a way to make sure that an
> object can perform the role you want it to perform?  Then object types
> can simplify your work.  The kernel library presently uses object types
> in a few cases, which I will describe below by way of example.

An alternate perspective: for people who have not had much experience 
with more strongly typed languages (or who, like me, are merely subject 
to a lazy bent), using function_object() to check the precise type of a 
function's object parameters is too cumbersome to feel worthwhile.

In e.g. Java, an object that tries to call a function on an object will 
simply not compile unless the object is of a type undeniably known to 
implement F. It's not merely impossible to call a non-existent function, 
it is impossible for the code to even try.

LPC is much laxer. There is no compile-time check on call_other, of 
course, but there's also the LPC tradition that call_other() silently 
calls non-existent functions on another object without error. Thus we 
simply do not get any immediate visceral punishment when we screw up and 
call a function (or otherwise supply) an object of the wrong type.

We do, of course, get punished, because our code doesn't work. The whole 
point of type-checking is to catch errors at an earlier stage than mere 
faltering logic.

These new object types provide just that; better type checking. But they 
also provide incentive to arrange your code with much more structure. 
Where you're used to sending around blobs of code and data that are all 
just 'object' to the recipient parameters, start thinking of them as 
precisely defined data types.

Thus if a function accepts a parameter of type 'object "/lib/iterator"' 
this new DGD functionality allows you to rest more assured than ever 
that it will behave like (your idea of) a standard iterator. More than 
that, however, it will also compel you to go through your legacy code, 
finding all the iterators you've written over the years, and making them 
all inherit "/lib/iterator".

Next, in a passionate flurry of standardisation, you will find yourself 
going through all your data structures to give them the ability to 
return just such a standard iterator. And while doing that, you'll 
realize that if they are all iterator providers, they should inherit, 
say, "/lib/iterator_provider" or perhaps, Java-style, "/lib/collection", 
and suddenly you can write functions that accept "/lib/collection" 
objects which do not care a iota what the underlying implementation is 
and simply expect to be able to retrieve an iterator from whatever it is 
that was sent in.

Yes, of course, this is just basic object-oriented design, and we all do 
it more or less automatically. Some of you are undoubtedly even 
disciplined enough to do it without support from the language. Those of 
you who are more like me, however, and need good habits encouraged and 
bad discouraged, will find these object types simply thrilling. Aim them 
at your old monolithic monstrosities and see them dissolve into elegant 
type interactions.

Now, if /lib/collection could just be made abstract... :)

Zell

From: dgd at list.imaginary.com (Greg Lewis)
Date: Mon Jul 12 15:26:01 2004
Subject: [DGD] object types: examples from the kernel lib

On Mon, Jul 12, 2004 at 11:55:24AM +0200, Par Winzell wrote:
> In e.g. Java, an object that tries to call a function on an object will 
> simply not compile unless the object is of a type undeniably known to 
> implement F. It's not merely impossible to call a non-existent function, 
> it is impossible for the code to even try.

This isn't strictly true.  There are a number of ways to get around this
(i.e. to produce compilable code that will cause an exception at runtime).

Off the top of my head:

1. Pass the object to a function which takes a parameter of type Object
   (or any other common base class) and casts it to a specific type before
   calling the method.  This will produce a ClassCastException at runtime
   if you pass an object not of the correct type.

2. Use the reflection framework to get a Method object from one class and
   simply use its invoke() method with an instance of a class of a 
   different type.  This will produce an IllegalArgumentException at
   runtime.

Admittedly both of these require that there is _some_ class which
implements F.  This is, however, weaker than the assertion that the
object's class itself must implement F for the code to be compilable.

But back to our regularly scheduled DGD programming :).

-- 
Greg Lewis                          Email   :
Eyes Beyond                         Web     : http://www.eyesbeyond.com
Information Technology              FreeBSD :