Phantasmal MUD Lib for DGD
|
Phantasmal Site > DGD > DGD LPC Reference > DGD Object Subtypes Subtypes of DGD's 'object' TypeFrom: 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 : |