Creating a custom scripting language
Goto page Previous  1, 2, 3
 
Post new topic   Reply to topic    mudlab.org Forum Index -> Coding
View previous topic :: View next topic  
Author Message
eiz



Joined: 11 May 2005
Posts: 152
Location: Florida

PostPosted: Thu Dec 15, 2005 5:34 pm    Post subject: Reply with quote

[Note: I've edited this post several times now to clarify the multiple light source issue.]

shasarak wrote:
Anything that wishes to invoke that method does so by checking for the existence of a #lightSource property and, if it's there, invoking the method on that.


I don't see how the namespace issue for methods is any different from the namespace issue for property names.

shasarak wrote:
This sort of thing results in the most unholy mess when you start trying to figure out which version of a method that is defined in more than one superclass you actually want to end up using.


Well, it's certainly a circus in C++ but most languages deal with this by ordering the inheritance graph: in Slate, for example, you can use addDelegate:before:valued: or addDelegate:after:valued: to add a delegate slot in a specific place.

shasarak wrote:
Instead, you need a two-level system: the sword needs a #lightManager property, and the manager manages a collection of light sources. LightManager probably implements a #brightness method which is the sum of all of individual LightSource brightnesses.


Phew, sounds complicated. Wouldn't it be a whole lot easier to just resend the message to the rest of the delegates? Smile In any case, the issues are the same for either system. And it's also a game design issue, not a language design issue: depending on your goals, it may be perfectly reasonable to not have stackable light sources.

Still, if you want this, it's straightforward - just make LightSource recursive:

Code:

addPrototype: #LightSource derivedFrom: {Cloneable}.
LightSource addSlot: #subLights.
LightSource addSlot: #brightness.

ls@(LightSource traits) lightLevel
[
    ls brightness + ls subLights inject: 0 into: [| :x :y | x brightness + y brightness]
].

addPrototype: #GlowingSword derivedFrom: {LightSource. Sword. Etc.}.
...


Then you can nest light sources as deeply and stupidly as you want, and since you're using delegation you can just directly add your fire or whatever as a light source, rather than going and screwing around looking up its 'lightSource' property, adding it, and then praying the original object doesn't somehow obtain a new LightSource object of its own, desynchronizing your lighting... So a 'Torch' could have a 'Flame' as a light source, which itself could also be responsible for setting things on fire.

Of course, you might want to do things like this, too:

Code:

r@(Room traits) subLights [ r contents ].


Now 'lightLevel' automatically does the right thing... Smile
Back to top
View user's profile Send private message Visit poster's website
Author Message
shasarak



Joined: 29 Jun 2005
Posts: 134
Location: Emily's Shop

PostPosted: Fri Dec 16, 2005 3:55 pm    Post subject: Reply with quote

eiz wrote:
I don't see how the namespace issue for methods is any different from the namespace issue for property names.

Simply a question of numbers. The number of different named properties that you want to check for is probably going to be a few hundred at most. The number of unique methods in the system will be in the tens of thousands. It requires less effort to ensure that a few hundred names are unique than it does to ensure that tens of thousands of names are unique.

eiz wrote:
shasarak wrote:
This sort of thing results in the most unholy mess when you start trying to figure out which version of a method that is defined in more than one superclass you actually want to end up using.

Well, it's certainly a circus in C++ but most languages deal with this by ordering the inheritance graph: in Slate, for example, you can use addDelegate:before:valued: or addDelegate:after:valued: to add a delegate slot in a specific place.

Well, yes, but that wasn't the point I was making. It's not a question of "can the language make this work?" it's a question of "how easy is it to actually program this?"

A lot of the thinking behind the approach I'm suggesting, here, is not to do with what's syntactically possible or even what performs well, but with what makes it easier to program and to debug. If you have an object that inherits from both "Sword" and "Torch", the compiler may be quite comfortable with which method gets called at which times, but the programmer may well get muddled, and it may take some time to track down a bug that is the result of the TorchSword behaving in a Torch-like fashion when you were expecting it to behave in a Sword-like fashion (or vice versa).

I advocate coding things at a very high level of abstraction, and keeping each class as small as possible: one class, one function. You might well have a common superclass for a torch and a lantern. But (for example) the class that knows how to deal with commands like "light" and "extinguish" ought not to be the same class that handles brightness level. If a light spell makes an object glow, that object needs to act as a source of light - but the player cannot light it or extinguish it in the same way as he can a lantern. So you don't want to oblige an object that inherits from a LightSource class also to automatically inherit a response to commands like "light" and "existinguish".

Your glowing sword (even if the player can switch the light on or off) should therefore NOT inherit from Sword and Torch - it should inherit from (or contain instances of) a number of classes such as Sword, BrightnessManager, BurningLightCommandHandler, etc. If you're going to use multiple inheritance, the superclasses should (IMO) overlap with each other as little as possible. The more there is a functional overlap between one superclass and another, the more difficult it is for the programmer to keep track of what's going on, regardless of how comfortable the compiler is.

eiz wrote:
shasarak wrote:
Instead, you need a two-level system: the sword needs a #lightManager property, and the manager manages a collection of light sources. LightManager probably implements a #brightness method which is the sum of all of individual LightSource brightnesses.


Phew, sounds complicated. Wouldn't it be a whole lot easier to just resend the message to the rest of the delegates? In any case, the issues are the same for either system. And it's also a game design issue, not a language design issue: depending on your goals, it may be perfectly reasonable to not have stackable light sources.

Yes but, again, that's not the point I was making. What I was saying was simply that, in this situation, there must a degree of aggregation going on. The LightManager class doesn't inherit from both TorchLightSource and SpellLightSource - it is holding references to instances of those classes. For behaviour to be cumulative across objects or classes, you can't depend purely on dynamic inheritance, there has to be some instantiation going on (or the subclass code has to be directly aware of the current state of its superclasses, which is nasty).
Back to top
View user's profile Send private message
Author Message
eiz



Joined: 11 May 2005
Posts: 152
Location: Florida

PostPosted: Fri Dec 16, 2005 4:44 pm    Post subject: Reply with quote

BrightnessManager, LightSourceManager, .... you seem to write very bureaucratic code. Wink

That aside:

shasarak wrote:
I advocate coding things at a very high level of abstraction, and keeping each class as small as possible: one class, one function. You might well have a common superclass for a torch and a lantern. But (for example) the class that knows how to deal with commands like "light" and "extinguish" ought not to be the same class that handles brightness level. If a light spell makes an object glow, that object needs to act as a source of light - but the player cannot light it or extinguish it in the same way as he can a lantern. So you don't want to oblige an object that inherits from a LightSource class also to automatically inherit a response to commands like "light" and "existinguish".


In the first place, allow me to stop speaking hypothetically for a moment and say that I would never, ever write a LightSource class in real life. Since any object can conceivably be a light source, it would be built in functionality. Obviously, attaching "light" and "extinguish" commands to such a class would be pure madness. On the other hand, I would certainly consider it for "Torch" ...

shasarak wrote:
If you're going to use multiple inheritance, the superclasses should (IMO) overlap with each other as little as possible.


I agree fully. Fortunately, this is not a major problem in practice.

shasarak wrote:
The more there is a functional overlap between one superclass and another, the more difficult it is for the programmer to keep track of what's going on, regardless of how comfortable the compiler is.


Well, I speak for myself only, but I'm quite comfortable with MI. It's not that hard.

shasarak wrote:
The LightManager class doesn't inherit from both TorchLightSource and SpellLightSource...


I don't recall stating any such thing in the first place. In fact I don't recall mentioning LightManagers, TorchLightSources, or SpellLightSources at all. If I wanted a spell to create a light effect on an object, I'd probably create a game object called 'Aura' or something and attach it as a component. This might sound odd, but it's actually a lot more flexible than tossing around random 'managers': now your funky effect is a first class member of the game world, and can be interacted with just like anything else.

shasarak wrote:
For behaviour to be cumulative across objects or classes, you can't depend purely on dynamic inheritance, there has to be some instantiation going on (or the subclass code has to be directly aware of the current state of its superclasses, which is nasty).


Obviously. I believe I addressed that. Inheriting LightSource provides the behavior of being able to emit light, nothing more, nothing less. How much light, in what direction, what color or how it got there is irrelevant. LightSource itself can be modified to suit the needs of the game design. In your mind LightSource seems to be an object which 'manages' light, but in mine it's a trait of a game object, not an entity per se. Dynamic inheritance comes into play when you want to actually change the fundamental capabilities of the game object after creating it - say, turning a human into a frog, or something like that. Not when you're merely changing its state (the fact that an object is now glowing does not affect its capability for light emission).

My remark about resending messages was probably confusing and unnecessary. It was just a random thought. Smile
Back to top
View user's profile Send private message Visit poster's website
Author Message
Lindahl



Joined: 29 May 2005
Posts: 56

PostPosted: Fri Dec 16, 2005 5:48 pm    Post subject: Reply with quote

BobTHJ wrote:
I'm using a property method for my Mud Engine in VB.Net similar to what shasarak describes, except it's all in compiled bytecode instead of a scripting language. The key factors I would like to point out about it are:

1. Each property can be attached to an object (object being Area, Room, Mob, Item, or Exit). The object can hold multiple properties of the same type (to solve the 2 light source problem).

2. Each property implements a common interface, which allows the engine to update/respond to a property without knowing what that property is or does. Properties provide 2 methods for receiving notice of messages (one before the action, and one after).

3. The properties are much more generalized than LightSource. For instance, if you want to make a glowing sword, you would add a 'ChangeStat' property to the sword. This would alter the "Luminosity" stat of the sword by increasing it as long as the property remained attached to the object. An object's stats are stored in a hashtable, and are only created if they are required for use. So a sword would not have a "Luminosity" stat until something changes it from the default of 0.

I have found that by adding combinations of properties to objects I can create almost any behavior I desire. So far, I've had to write about 90% less scripts for custom object behavior than I have on previous muds, all by just using combinations of a dozen or so property objects. I find the system to be most effective.


This is essentially the DIKU model, with added dynamic properties (instead of static properties). Effects in the DIKU model can be arbitrarily added to an object, the effects will modify a stat (or several) on an object for a specified amount of time (possibly indefinate). The only difference between this model and your model, is that the DIKU stats are static, while your stats are dynamically stored in a hash table. The trade-off, I suppose, being speed for memory (in your case).

After reading this whole discussion, I prefer a forwarding object to dynamically add properties to objects. Dynamic properties are added by chaining properties together and forwarding on unhandled methods until they reach the original object. This is best implemented in the generic form, below.

Obviously simplified, but:

Code:

class forwarder : public object
{
public:
  forwarder(object * forward) : m_forward(forward) {}

  void other_methods() {m_forward->other_methods();}
  ...

private:
  object * m_forward;
};

template<class Trait>
class property : public Trait, public forwarder
{
public:
  property(object * forward) : forwarder(forward) {}
};

// all accesses to a specific object pass through one and only one handle (like a smart pointer)
class handle
{
public:
  template<class Trait>
  add()
  {
    m_object = new property<Trait>(m_object);
  }
  ...
private:
  object * m_object
};

Game code:

class illuminated
{
public:
  unsigned lumens() {return 1;}
};

if (spell == illuminate)
  target.add<illuminated>();


In my object model, all objects have all methods, but they apply some sort of 'null' effect on methods they don't support. In the 'lumens' case, a regular object would just return zero, or no lumens, or no brightness.

This is a rather contrived example, however, an explaination of a more detailed example (for instance, a player attempting to perform an action that isn't supported requires feedback) requires delving into my model of game objects - essentially the seperation of the controller from the controlled - the brain/body idiom I've been using for the several years now. The brain chooses what to do, sends it's message to the body, the body tries to do what the message says, then sends whatever reponses (if any) back to the brain. The fact that an opportunity for a response always exists allows me to always give feedback for unsupported methods - even if the brain doesn't care about feedback (in which case, it just ignores the response).

On the topic of MI, I don't find anything drastically wrong with C++'s implementation of it. It could be better, but it's certainly useful in some situations - even in MUD development (as you see above).


Last edited by Lindahl on Fri Dec 16, 2005 6:17 pm; edited 7 times in total
Back to top
View user's profile Send private message
Author Message
eiz



Joined: 11 May 2005
Posts: 152
Location: Florida

PostPosted: Fri Dec 16, 2005 7:11 pm    Post subject: Reply with quote

[Just a notice: OT stuff moved to General]
Back to top
View user's profile Send private message Visit poster's website
Author Message
eiz



Joined: 11 May 2005
Posts: 152
Location: Florida

PostPosted: Fri Dec 16, 2005 11:09 pm    Post subject: Reply with quote

A side note on prototype based languages: anyone who is serious about implementing a high performance system based on prototypes must read the Self papers. Not that I expect anyone to go and implement polymorphic inline caches for a mud server, but at least the map optimization (see this paper for details) and possibly a few others.

Lindahl wrote:
The only difference between this model and your model, is that the DIKU stats are static, while your stats are dynamically stored in a hash table. The trade-off, I suppose, being speed for memory (in your case).


This is actually exactly what the map optimization addresses. Basically, in a naively implemented prototype system (I believe this may apply to ColdC - please correct me if I'm wrong!) the slots of an object are stored in a table of (symbol,value) pairs. Self replaces this with a pointer to a (symbol,offset) map shared by all objects cloned from the same prototype. Since most objects are clones that don't differ in shape from their parents (think $orc_1, $orc_2, $orc_3, ...), this results in a large space savings. It's also quite handy when compiling type-specialized method implementations, since you can just use the offsets directly. The map is immutable, so if you change a parent's layout after creating clones, the children will still point at the old map.
Back to top
View user's profile Send private message Visit poster's website
Author Message
Lindahl



Joined: 29 May 2005
Posts: 56

PostPosted: Mon Dec 19, 2005 7:05 pm    Post subject: Reply with quote

eiz wrote:
Not that I expect anyone to go and implement polymorphic inline caches for a mud server, but at least the map optimization (see this paper for details) and possibly a few others.


If anyone were to, it'd probably be me. Laughing

eiz wrote:
This is actually exactly what the map optimization addresses. Basically, in a naively implemented prototype system (I believe this may apply to ColdC - please correct me if I'm wrong!) the slots of an object are stored in a table of (symbol,value) pairs. Self replaces this with a pointer to a (symbol,offset) map shared by all objects cloned from the same prototype. Since most objects are clones that don't differ in shape from their parents (think $orc_1, $orc_2, $orc_3, ...), this results in a large space savings. It's also quite handy when compiling type-specialized method implementations, since you can just use the offsets directly. The map is immutable, so if you change a parent's layout after creating clones, the children will still point at the old map.


Right, I remember seeing Unifex's early implementations of Aetas using this model. Is this what you're referring to? Where a heirarchy of maps are examined to find a property (or perhaps just two levels)? Your mention of offsets however makes me think that there's something different to the methods described in the Self papers? Lacking a good postscript viewer and gzip utility at the moment, I can't read them (I find it disgusting that they don't cater to the average user - .pdf.zip).

Anyway, I find that the space savings from a few static properties would be wasted by the secondary mapping needed to accomodate the mutated properties. From what I've seen (in modern MUDs), most properties are mutated. Additionally, given a rich heirarchy, the static properties can usually be built-in as truely static variables, providing better space benefits - if necessary. Whereas, the speed you sacrifice is a bit harder to make up for, IMO. I'd anticipate that (in modern MUDs) you'd run out of processing speed long before space - given the trend to make everything smarter as opposed to space filling.

Though, I'd be interested in hearing what implementations you have in mind when you think "large space savings" - or even other benefits of such an architecture.

Is it fair to say that a big reason to look in that direction is that it folds into an SQL database better than a rich static type heirarchy? It seems that about the time Unifex started moving to SQL databases, and found the difficulty of mapping a rich type heirarchy, he adopted the property map architecture, but perhaps it's just a coincidence.

Another coincidence seems to be choosing this architecture when it comes time to implement (or add) a scripting language - which naturally precedes the need to access arbitrary properties by symbol (outside the raw programming environment).

As for topicality, scripts certainly can be optimized to only look up a property once per usage (and store the offset), however, in the raw programming environment, I wouldn't be surprised to see the implementations become quite a bit wordier to achieve this optimization (reducing readability and maintenance).

Note that I posted this in the context of modern MUDs, not DIKUs (and similar), which I agree would largely benefit from such an architecture.
Back to top
View user's profile Send private message
Author Message
eiz



Joined: 11 May 2005
Posts: 152
Location: Florida

PostPosted: Tue Dec 20, 2005 12:24 am    Post subject: Reply with quote

Lindahl wrote:
Right, I remember seeing Unifex's early implementations of Aetas using this model. Is this what you're referring to? Where a heirarchy of maps are examined to find a property (or perhaps just two levels)? Your mention of offsets however makes me think that there's something different to the methods described in the Self papers? Lacking a good postscript viewer and gzip utility at the moment, I can't read them (I find it disgusting that they don't cater to the average user - .pdf.zip).


Nope. What Aetas did was copy on write: if a property wasn't found in an object, the parents would be searched. This saves space, but is very inefficient. With MI it would be even worse. What Self does is change the property map to an object layout map, sharing this between all objects of the same shape (i.e. they have the same properties).

Lindahl wrote:
From what I've seen (in modern MUDs), most properties are mutated.


Exactly why this optimization works! Instead of having a map for each object's variables, you have a single map which the objects point to, followed by a bunch of slots. The map tells you the layout of the slots. This means you only need one hash table or whatever for all objects sharing the same ancestry (assuming they don't acquire new slots, which is the uncommon case).
Back to top
View user's profile Send private message Visit poster's website
Author Message
Tyche



Joined: 13 May 2005
Posts: 176
Location: Ohio, USA

PostPosted: Tue Dec 20, 2005 2:21 am    Post subject: Reply with quote

eiz wrote:

This is actually exactly what the map optimization addresses. Basically, in a naively implemented prototype system (I believe this may apply to ColdC - please correct me if I'm wrong!) the slots of an object are stored in a table of (symbol,value) pairs. Self replaces this with a pointer to a (symbol,offset) map shared by all objects cloned from the same prototype. Since most objects are clones that don't differ in shape from their parents (think $orc_1, $orc_2, $orc_3, ...), this results in a large space savings. It's also quite handy when compiling type-specialized method implementations, since you can just use the offsets directly. The map is immutable, so if you change a parent's layout after creating clones, the children will still point at the old map.


ColdC stores only the variables defined in an object in that object. Only when a parent object variable are assigned to does the variable migrate into the child. A read of a variable will fetch the default value from the parent. All data inheritance is private so all access for inherited variables regardless of where they reside must use methods defined on the parent.
Back to top
View user's profile Send private message Visit poster's website
Author Message
eiz



Joined: 11 May 2005
Posts: 152
Location: Florida

PostPosted: Tue Dec 20, 2005 3:14 am    Post subject: Reply with quote

Tyche wrote:

ColdC stores only the variables defined in an object in that object. Only when a parent object variable are assigned to does the variable migrate into the child. A read of a variable will fetch the default value from the parent. All data inheritance is private so all access for inherited variables regardless of where they reside must use methods defined on the parent.


That's correct - but when the variable is assigned, a hash table entry is created. When a slot is assigned in Self, it's stored in a fixed location obtained from the shared map. Thus there is no per-object variable overhead. In Genesis, I believe the overhead is 5 or so words per variable, plus whatever blank entries are present in the hash table.

Also, I thought parents were only accessed when default_var and inherited_var were used...? object_retrieve_var just returns 0 when a variable isn't found.

Since I don't seem to be getting the point across very successfully, let me give an example. Say you want some rectangles. So you create a prototype with 4 slots: x1, y1, x2, y2. Now you clone a modest number of them, say a hundred thousand or so, and set their slots.

Self: 6 words per object (2 words overhead: 1 header word, 1 map pointer). Nothing to worry about. This is accomplished without losing any of the generality of the prototype object model.
ColdC: Some impractically large number. Every object has a variable table, a method table, a string table, an identifier table, a list of children, a list of parents, various flags, and probably other stuff I'm not thinking of.

So in ColdC, you would probably use a list wrapped in a frob to represent your rectangles. In fact, $rect in ColdCore does exactly that. I happen to think frobs are stupid. If objects were lightweight like they should be, they would be totally unnecessary.
Back to top
View user's profile Send private message Visit poster's website
Author Message
Lindahl



Joined: 29 May 2005
Posts: 56

PostPosted: Tue Dec 20, 2005 1:24 pm    Post subject: Reply with quote

eiz wrote:
What Self does is change the property map to an object layout map, sharing this between all objects of the same shape (i.e. they have the same properties).


Thanks for the clarification - I can skip reading the paper now (especially considering the inconvenience of the format). The whole point to this is mutating the types of objects, correct? The only way it saves space is by sharing hash tables (which in a static typed language is only maintained during compilation), correct? An object is still sized by the amount of bytes needed to contain all data members for it's type, from what I can tell, offering no space savings beyond a statically typed language (but lacks dynamic typing).
Back to top
View user's profile Send private message
Author Message
eiz



Joined: 11 May 2005
Posts: 152
Location: Florida

PostPosted: Tue Dec 20, 2005 5:14 pm    Post subject: Reply with quote

No, there is no savings beyond that of a class-based language, even a dynamically typed one; the map pointer is roughly equivalent to a class pointer - the only difference is that it is created implicitly and managed transparently while maintaining the generality of prototypes. The idea is to eliminate the memory overhead of prototype OO, putting it on par with traditional systems. Many of the other techniques in Self are applicable to mainstream languages (and, indeed, modern JIT compilers such as Java's HotSpot and Microsoft's CLR are based largely on a combination of Self's techniques and traditional optimizations - I suggest Advanced Compiler Design and Implementation for those, under the assumption that you already understand the basics of compiler design).
Back to top
View user's profile Send private message Visit poster's website
Display posts from previous:   
Post new topic   Reply to topic    mudlab.org Forum Index -> Coding All times are GMT
Goto page Previous  1, 2, 3
Page 3 of 3

 
Jump to:  
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum

Powered by phpBB © 2001, 2002 phpBB Group
BBTech Template by © 2003-04 MDesign