Programming question

As one who learned programming through TADS3, and is starting to try to learn some “real-world” languages as well, I’m wondering do other languages have the ‘property pointer’ functionality (or equivalent) that TADS uses? Say, Python or Java? I assume C++ would be able to, given you have direct access to pointers…

Could you explain what this is, for those of us who are not TADS users?

I’m not quite sure what a property pointer is: from what I can tell, TADS exposes two kinds of pointer, a function pointer and a property pointer. Judging by my admittedly superficial reading, they look like references. These are prevalent in a great many languages, because they offer a restricted subset of what raw pointers allow you to do. This is often a good thing, because direct memory manipulation and addressing control can wreck your shit in a hurry.

C, C++ and a host of other languages allow you full control over pointers: not just to use them to refer to other variables or functions or objects or memory, but to redefine any piece of data as if it were any type you want, or even (if you know what you’re doing) modify your own executable at runtime.

Could you give a concrete example of something you would use property pointers for?

In TADS (I don’t know if this is a widespread thing) properties and parameterless methods seem to be a little blurred together because there is syntax to support expressing the same thing in a few different ways depending on which is most convenient for your situation.
You can call a (parameterless) method without any explicit call operator just as if you were evaluating a property, and properties can be defined with a ternary operator so they act like a simple method (if this return that; else return the other). A property such as ‘isListed’ of a simulation object can be defined

isListed = true     // or
isListed = panel.isOpen    //or 
isListed = isIn(compartment) ? panel.isOpen : true   //or
isListed() { if(cond || cond2) return true; else return nil; }

A property pointer is basically like a function pointer… you can store a property/method name in a variable or a property of another object. In this form, the property is not attached to any specific object, therefore it doesn’t evaluate to anything until it has been applied to an object in the code. In TADS you preface the property name with an ampersand: &isListed, and to use it you must enclose it in parentheses after a dot: if(cond) obj.(&prop);
Simplified example from the adv3 library:

class Direction
    dirProp = nil
;
northDirection: Direction
    dirProp = &north
;
southDirection: Direction....   ;

class Room: Thing
    north = nil
    south = nil...
;
hall: Room
    south = foyer ;
frontYard: Room
    north = foyer ;
foyer: Room
    south = frontYard
    north = hall
// then, in the action execution code if the player has typed a  travel-in-
// direction command, the parser has returned a Direction object :
    local dir = //(whatever direction object the parser found)
    local loc = gPlayerChar.location;
    local propPointer =  dir.dirProp     //this will be something like &north...
                                             // a reference to the property of an object without 
                                             // evaluating it
    local connector = loc.(propPointer)  // evaluate what is in the 'north' 
                                           // property of the room the PC is in: it should be
                                          // a room object or other TravelConnector object
   nestedAction(TravelVia, connector);

Or, to create a Fuse or Daemon event:

me: Person
    drown() { end('You were underwater just a little too long'); }
    afterAction() { if(justEnteredWater) new Fuse(me, &drown, 8);
            if(justSurfaced) /* cancel the fuse */ ; }

where the Fuse constructor’s first parameter asks for any object, and the second parameter asks for which property/method to call on that object when the fuse goes off. When the interval has been reached (in this case 8 turns), the Fuse event will get a turn to execute a statement like obj.(prop); which is the same as me.drown; in this case, because those were the values stored in ‘obj’ and ‘prop’ during construction.

Does that make any sense? Do languages like Python and Java (what I’ve mostly been exploring so far) have similar functionality?

I won’t speak to Java, which I can sort of read but wouldn’t presume to write (nor to talk about the grammatical structures of). But Python supports the basic notion of what you’re asking about in several different ways, though not quite with the same underlying assumptions. (By “the basic notion of what you’re talking about” I mean that you can store the me.drown action in an abstract way in, say, a list of functions to execute. Apologies if that’s not what you’re getting at. Also apologies if, as someone who knows little about TADS, I’m missing subtleties in what you’re asking.)

Probably the most obvious analogue would be Python’s notion (and version) of bound methods, the basic notion of which is that you can refer to a method of an object in a way that keeps a reference to an object in addition to the object’s method, so that you could store a reference to, say, the_player.drown in a queue of methods to be executed. This is quite a common coding pattern in some areas of Python development–GUI’s built with Python’s tkinter GUI-building toolkit are often built around such things, so that if you’re writing, say, a drawing program, and you have a floating toolbox with a “clear canvas” button, then you might create the button with Button(text='clear canvas', callback=the_canvas.clear).

Maybe a more direct analogue to what you’re asking would be Python’s notion of unbound methods: roughly the same thing, but not already bound to an instance of a class, and for which you need to supply an instance of that class (or a subclass) when dispatching them. So say you have a class Character (classes in Python are by convention, though not grammatical necessity, capitalized), and that class has defined drown as a method. Say further that you don’t want to have to decide at the time the method is stored in the queue which character is going to do the drowning, perhaps because some logic is going to make that decision at the time the call is dispatched (the loser of some game of musical chairs, presumably). You can then refer to the class’s drown method instead of the drown method of an instance, but then have to additionally supply an instance of the object when dispatch time arrives, perhaps by doing something like

unlucky_person = choose_musical_chairs_loser()   # where unlucky_person is an instance of Character
Character.drown(unlucky_person)                  # as opposed to just unlucky_person.drown()

at dispatch time. (That’s an awkwardly terse way to phrase it; in real life, of course, you’d have a queue and probably more scaffolding, but it illustrates the basic concept.)

If what you really want is to specify an object plus the text name of the method to call, you could use the getattr function and call the result:

getattr(the_person, 'drown')()

… though this is pretty hacky for a method name specified as a string constant: it would be easier to just write the_person.drown() if you know in advance that that’s the name of the method you’re going to want to call.

There are other options that involve deeper magic, such as mucking about with “dunder” (double-underscore) “magic methods,” which control the basic inner workings of how the objects function at a low level. One of these possibilities would involve trapping all attribute dispatch for a class by writing a __getattribute__ method. (Though this gets hairy pretty quickly: if you’re trapping all attribute dispatching for a class, then your method can’t refer to any of its own attributes in any way that triggers a lookup, because it will then call itself recursively. Workarounds for this get pretty Byzantine pretty quickly.)

In general, the second option (unbound methods) is closer to what I think you’re actually asking, but I suspect that the first (bound methods) are likely to be the idiom you’d be most likely to wind up using in the larger situation you’ve described.

Does that make sense? Does it help?

1 Like

I feel like this sort of thing comes from an older generation of functional languages, like Smalltalk, that framed everything in terms of “messages”. You could (in theory) pass any message to any object.

These got superseded in the 80s by OO languages, which preferred to talk about “methods”. An object provides specific methods for you to call. But since everything is determined by the object type, you can’t separate the method from the object.

Of course, as I said in a different thread, programming paradigms are genres and genres hybridize over time. Plenty of modern languages support something like methods. Patrick’s summary of Python demonstrates that.

Objective C is another example. ObjC was directly inspired by Smalltalk but it grew up in the late 80s. So it’s mostly OO, but you can separate out a method with @selector and then send it to any object you like. But this is rarely done. (I had to go hunting through the documentation because I forgot what the syntax was!)

Interestingly, Inform 6 also talks about messages and provides a form of this. That’s because the Z-machine was designed as an extremely simplified MDL machine, and MDL is a functional language! The Z_machine (v5) has a stock of 48 properties which can be applied to any object. A property can contain a value or a function – if it’s a function, you can think of it as a message. But they’re just numbers from 1 to 48 underneath, so you can perfectly well store a property in a variable and then apply it to an object.

2 Likes

Hey Patrick, thanks for the in-depth reply. With my limited programming knowledge, I can’t be fully certain if any of those options you listed are quite exactly the same thing, although I expect if you know what you’re doing you could probably accomplish the same thing. A TADS property pointer is simply a property (or method) name that isn’t attached to any object or any class specifically, and can in theory be applied to anything of type object (though in practice you’ll need to know that you’re using it with classes that define the property, or you can make use of a builtin function: if obj.propDefined(&propInQuestion) obj.propInQuestion.
Maybe I’ll just try to give a contrived example, and you can tell me how you would go about the same thing in Python…

class WeatherEvent: object
    allEvents = [ /* during init this vector will be filled with all statically 
                            declared WeatherEvent objects */ ]
    reactProp = nil
;
rainEvent: WeatherEvent
    reactProp = &reactToRain
    // other props/meths   ;
hotSunEvent: WeatherEvent
    reactProp = &reactToSun    ;

class Flower: Thing
    height = 4
    reactToRain() { ++height;  "The flower perks up from the watering. "; }
    reactToSun() { "The hot sun withers the flower and kills it. ";
        moveInto(nil); /* TADS lingo for removing a sim obj from the game*/ }
class Person: Thing
    reactToRain() { if(notWearingRaincoat) /* set state to wet and grumpy etc.*/;
        else "<<name>> is really glad they have a raincoat right now. "; }
    reactToSun() { if(isWearingCoat) { "<<name>> can't bear to keep the coat on in this brutal heat. "; 
        nestedAction(Remove, coat); } }

// then, in some code that will get called when the PC enters a certain room:

... local event = rand(WeatherEvent.allEvents)  // choose a random event
    foreach(/*obj that's in scope*/) {
        obj.(event.reactProp); }  // if a rain event was chosen, we're now calling
                                                  reactToRain on each object in scope. If sun 
                                                  event, we're calling reactToSun...

In this example it’s pretty obvious that the property pointer will always be applied to an object of class WeatherEvent, therefore the process could probably be worked another way. But there are other situations in the adv3 library that more complex…

1 Like

The search term you want is duck typing. You don’t worry about whether something’s a member of a particular class, you’re only concerned about whether it has a particular method, i.e., “if it walks like a duck and it talks like a duck, it’s a duck.” Adhering to this is a strong cultural norm in Ruby. You can do the same in Python or dynamically typed object-oriented languages in general (many details elided).

1 Like

I’m surprised that javascript hasn’t been mentioned since it’s pretty popular on this forum thanks to Twine and such.

var direction = "north";

var room = new function() {
	this.north = function() {
		// do something
	};
};

room[direction]();

You can also use classes or create new object instances similar to C++ instead of one-off objects. This was just an example using super basic stuff.

3 Likes

So, where in TADS you’d store a property pointer like: reactProp = &reactToRain, JavaScript and Python would just store string values, and that string value could be used to call an unspecified object’s method later?

Yeah. JS is weird (and useful) in that everything is an object, even functions. That’s why my example works.

You could also write it like this, but it’s uglier and less manageable for more complex objects:

var direction = "north";

var room = {
	north: function() {
		// do something
	}
};

room[direction]();

This way it’s literally an object (or a map/hash/dict or whatever term you want to use).

IOW, in JS you access object members the same way you do array entries. It’s convenient because you can do shameful code like this:

var action = "take";

var door_key = new function() {
	this.before_take = function() { };
	this.after_take = function() { };
};

function doAction(obj, action) {
	if ("before_" + action in obj) obj["before_" + action]();
	if ("after_" + action in obj) obj["after_" + action]();
}

doAction(door_key, "take");
2 Likes

This is probably going to get long and I apologize for that in advance.

I guess what I was really saying is that you can do it the way you’re asking in Python … that’d be the unbound methods I was talking about earlier, with some system of matching them with individual objects at execution time. But I suspect that it would be more natural in many common circumstances to write the code in an idiom using bound methods: if your situation is, say, that someone has slipped into the dangerously fast stream, and has five turns to get out before drowning, then you know in advance who to schedule the check on in five turns: it’s easier to write unlucky_npc.drown_in_five_turns, say, or unlucky_npc_drown_in(5) or to use a helper function like schedule_event(unlucky_npc.drown, 5) then it is to store the object and the event separately and then pair them at dispatch time. Again, you can do it that way if you want to, and there are probably situations where it makes sense, but it’s probably not the most natural way to code the more common situations in Python.

To answer part of your direct question, it’s easy to determine whether an object has a named attribute in Python with a test like if hasattr(the_thing, 'switch_on'): [...], but arguably it more natural to try to do what you want to do on the assumption that you will succeed, then deal with failures when they arise:

try:
    the_thing.switch_on(actor)
except AttributeError:     # the_thing doesn't have a switch_on method?
    print("You are mysteriously unable to switch on {1}.".format(the_thing))    # Other syntaxes are possible and have partisans up to and including the level of hill-to-die-on advocates. I choose this syntax because it's terse.

Though arguably, since you’re really talking implicitly about writing the whole world model from scratch, maybe switch_on is something that you should implement in an abstract base class and for which you could provide a default rejection, then allow overriding in subclasses to customize (printing, or other) behavior:

class Noun (object):
    def switch_on(self, actor):
        print("You cannot switch on a %s." % self)

class Thing(Noun):
    [... many methods ...]

class Device(Thing):
    def __init__(self, initially_on):
        self.is_on = initially_on

    def switch_on(self, actor):
        if self.is_on:
            print("%s is already on!" % self)
        else:
            print("With a satisfying CLICK!, %s turns on." % self)

class Hammer(Thing):
    def switch_on(self, actor):
        # purely a custom response
        print("%s cannot switch on a gol durn HAMMER. Yeesh." % actor)

The problem with talking about writing parser IF in Python is that there really is no IF-type standard library that provides a world model, parsing, saving, undoing, checking to make sure that if A is contained by B then you’re not also trying to put B into A, etc. etc. etc. Anything you need has to be built from scratch, which is a lot of tedious work. But hand-waving that and imagining that there’s already an abstract set of routines that provide such things, event scheduling along the lines you suggested might look something like:

import random   # from the Python standard library
import globs    # global data, including all world-state data.

class Noun(object):
    def _where(self):       # subclasses must override to track containment.
        return None

    [... various methods that apply to everything, mostly default rejection messages ... ]

class Thing(Noun):
    def _where(self):       # subclasses must override to track containment.
        # Some abstract function tracking and returning, or else calculating, containment, details of which would depend on the world model

    def eat(self, actor):
        print(f"{self} doesn't look all that appetizing.")      # f-strings are great but only available in Python 3.6+

    def _react_to_sun(self):
        pass        # By default, this thing silently does nothing when in the sun.

    def _react_to_rain(self):
        pass        # By default, this thing silently does nothing when left out in the rain.

    [... plenty more methods, many of which do nothing but provide different default-rejection methods than Noun would ...]

class Room(Noun):
    def __init__(self):
        self.contents = list()

    def _where(self):
        return None     # Or maybe a region that this room is in, if we're thinking about containment that way. Depends on how the world model works.

    def _player_entry_trigger(self):
        # assume this is called by some code that handles player movement somewhere.
        pass            # By default, do nothing when the player enters.

    def eat(self, actor):
        print(f"{str(self).strip()} absolutely cannot eat a ROOM. I mean, come ON.")

    def _react_to_sun(self):
        for what in self.things_in_location:
            what._react_to_sun()

    def _react_to_rain(self):
        for what in self.things_in_location:
            what._react_to_rain()

    [... plenty more methods, many of which override default rejection messages in their ancestor classes ...]

class Raincoat(Noun):
    [ ... many clothing-related methods, presumably ... ]

class Person(Noun):
    def __init__(self):
        self.holding, self.wearing = list(), list()
        self.dead, self.wet, self.grumpy = False, False, False

    def _die(self):
        print(f"'Gack!' says {self} suddenly, slumping to the ground.")
        self.dead = True

    def _where(self):
        # some abstract function determining containment of this Person and returning it.

    def eat(self, actor):
        print(f"{actor} is not hungry enough to eat {self}. Yet.")

    def _react_to_sun(self):
        if [obj for obj in self.wearing if isinstance(self, Raincoat):    # Are there any Raincoats in the list of things SELF is wearing?
            print(f"{self} can't bear to keep the raincoat on any more.")
            for raincoat in [obj for obj in self.wearing if isinstance(obj, Raincoat)]:    # Iterate over all currently worn Raincoats, removing each from the .worn list and adding it to the .holding list.
                self.wearing.remove(raincoat)
                self.holding.append(raincoat)

    def _react_to_rain(self):
        if [obj for obj in self.wearing if isinstance(obj, Raincoat)]:
            print(f"{self} is really glad to have a raincoat right now.")
        else:
            self.wet, self.grumpy = True, True

    [... many more function definitions ...]

class Flower(Noun):
    def __init__(self, initial_height=4):
        self.height = initial_height

    def _react_to_sun(self):
        print("The hot sun withers the flower and kills it.")
        self._where.contents.remove(self)

    def _react_to_rain(self):
        print("The flower perks up from the watering.")
        self.height += 1

class WeatheryRoom(Room):
    # override the method in the superclass do actually do something.
    def _player_entry_trigger(self):
        which_event = random.choice([self._react_to_sun, self._react_to_rain, self._some_other_third_thing])
        which_event()

    def _some_other_third_thing(self):
        queue_event(random.choice([self._react_to_sun, self._react_to_rain])        # queue an event for next turn
    
event_list = dict()        # int -> list[func]: that is, index, by turn count, a list of functions scheduled to be called on that turn to implement events. Probably would realistically be in `globs`, or maybe in a separate module of event-handling code, but it's conceptually easier to put it here in this example.

def queue_event(action_func, in_how_many_turns=1):
    if (in_how_many_turns + globs.turn_count) in event_list:
        event_list[in_how_many_turns + globs.turn_count].append(action_func)
    else:
         event_list[in_how_many_turns + globs.turn_count] = [action_func]

def top_level_every_turn_handler():
    # let's assume this gets called somewhere in the main parsing loop
    globs.turn_count += 1
    for which_event in event_list[globs.turn_count]:
        which_event._event_action()
    # clean out past events to prevent it from growing too larger
    for index in [i for i in events_list.keys() if i <= globs.turn_count]:
        del event_list[index]

So there’s a plausible sketch that kind of hints at a basic world model and sketches out how events might be triggered and scheduled. Notably it doesn’t have a parser, object names or descriptions or synonyms or any scaffolding at the object level for the parser to locate information about in-world things, a model of support or containment, any error handling, much precondition checking, output text wrapping, or a whole bunch of other things that you might expect with a real parser-IF language. It also hand-waves the question of scope by just delegating to every object in the location without doing any other scope calculations at all. And there’s plenty of assumptions that could be made differently: there’s no need to make the assumption that verbs are callable methods on objects, because there’s plenty of other ways you could handle things, and it’s arguably hacky to use isinstance() in this way. Some of this could be tightened up by assigning names to expressions that appear more than once or by using collections.DefaultDict for event_list to automatically produce an empty list for a nonexistent key or by not accounting for the possibility that the character is wearing multiple raincoats or by sacrificing code legibility in various ways.

Notice that in this particular case events are dispatched in response to _player_entry_trigger() on a room, and that function picks a random event-handling function from a list and then calls it. (There’s no reason why this list couldn’t be pre-computed by the class, as your TADS example is, but neither is it really necessary to set that up in this example, and it would prevent me from making a point in a bit if I set it up that way.) But it’s flexible enough to have other possible ways of triggering and scheduling events, too.

In any case, the _player_entry_trigger function in WeatheryRoom picks an option from the list [self._react_to_sun, self._react_to_rain, self._some_other_third_thing], which are three methods of an instance of a WeatheryRoom object. That is, they’re all bound methods: the method, when called, automatically has access to all the attributes of self, which is the instance of the room in question. But the items in that list don’t have to be of the same ontological “kind”: they’re don’t need to be methods of WeatheryRoom instances at all. Python doesn’t care about their object type as long as they have the appropriate calling signature. (In this case, this just means “takes no parameters on call,” thought that can be fudged insofar as bound methods automatically get access to self, and there are ways of supplying defaults for methods that expect parameters, as a couple of functions show.) So you could have a helper function, earthquake(), say, in that same list, that’s not a method on an in-game object at all, but could alter global data. Or you could even have a bare class name in the list, in which case calling it would produce a new instance of that class when it’s called (though that would probably have to do something interesting in its __init__ method to be useful, like insert itself into the world model somehow: still, there’s the option to have a class, TerrifyingBatWithLotsOfTeeth, that automatically puts the new instance in the player’s location when it’s called. Or somewhere else, for that matter). Or you could use an unbound method with a function that makes a decision when it’s called, and defer the decision to associate the action with its object until the event occurs: maybe something like queue_event(lambda: Person._die(random_visible_person()), 5), which uses the class name together with a helper function and uses the lambda keyword to defer evaluating the whole expression until the event occurs. (EDIT. Or, you could have a helper function that has associated data stored as part of the function object, as JavaScript also allows. Python functions are also full-class objects that can store arbitrary data in their object attributes.)

The bound methods used in the last example are methods of a Room object (well, its subclass. Anyway). But there’s no reason why this setup has to dispatch only to methods of Rooms; you could just as well use an event-handling function that’s an attribute of a Person: the list could also contain Bob._die for that event to be triggered, and there’s nothing preventing that from being in that same list if that method has a compatible calling signature. Or you could define an abstract Event class that doesn’t model any in-game parser-accessible object at all but that interacts with the world model in some other way, and that would be just fine in that same list.

Does that make sense? Does it answer your question? That’s way more than I expected to write.

Hey! Thanks for the really involved reply. It’s illuminating for someone like me starting to experiment with Python. You might be a little ahead of me in some parts with all the terminology, but I think I was getting the drift. I think my question was probably twofold: does Python have an exact correspondence to TADS property pointers (because it’s a construct I’m familiar working with), and if not, how does Python accomplish the same thing. The second question was definitely answered, and for the first you said unbound methods … could you give me just a short snippet of syntax for how/where you define and then use unbound methods? Maybe you did in all your examples, but I wasn’t certain which one was the unbound method version…
Also, just to note, I know that my react-to-weather was an uber-simple example that could easily be approached with other tools. The assumption was that WeatherEvent objects had a bunch of other info to encapsulate, therefore it would still be necessary to randomly choose a WeatherEvent object and then use its reactProp on the objects, rather than simply choosing a random react-to-weather function from a list that each Thing instance possesses…

Let’s go back to the example of compass directions, which was mentioned at the top of the thread (from the T3 library). This is also the most obvious use of “unbound methods” in the Inform 6 library. So we can probably call it the natural use case in IF land.

Here’s the relevant I6 code (simplified):

! Define some properties:
Property door_to;
Property n_to;
Property s_to;
! ...etc

! This is library code which defines the direction objects:
CompassDirection n_obj "north"
    with door_dir n_to;
CompassDirection s_obj "south"
    with door_dir s_to;
! ...etc

! These are room objects defined in the game:
Object Kitchen "Kitchen"
    with n_to Pantry,
    with s_to DiningRoom;

Object Pantry "Pantry"
    with s_to Kitchen;

The parser takes the input GO NORTH and determines the action (Go) and the noun (n_obj). Note that the directions n_obj, s_obj, etc are regular objects. They have a special property door_dir whose value is another property – that is, an unbound method in the sense that we’ve been discussing.

Then the Go action does this:

    ! Set the local variable thedir to one of the properties n_to, s_to, etc.
    thedir = noun.door_dir;
    ! Set the local variable dest to a room (or zero).
    dest = location.thedir;

In this example, thedir is set to the (unbound) property n_to. Then dest is set to Pantry; that’s the destination of the move.

The reason we need an unbound method is that, on that last line, both location and thedir are variables. The parser must be able to look up any direction on any room.

(If the room has no n_to property, the expression location.thedir returns zero, and then the Go action knows it should print “You can’t go that way.”)

2 Likes

Here’s the exact equivalent in Python:


class CompassDirection:
    def __init__(self, dir):
        self.dir = dir

n_obj = CompassDirection('n_to')
s_obj = CompassDirection('s_to')

class Room:
    n_to = None
    s_to = None
    # etc
    def __init__(self, name):
        self.name = name

# Define the rooms
Kitchen = Room('Kitchen')
Pantry = Room('Pantry')

# Set up the room exits
Kitchen.n_to = Pantry
Pantry.s_to = Kitchen

location = Kitchen

def GoAction(noun):
    dir = noun.dir
    dest = getattr(location, dir)
    if dest is None:
        print('You cannot go that way.')
    else:
        print('Going to', dest.name)

Here, unbound properties are represented by strings, and we use the getattr() trick that Patrick described earlier.

Now, this isn’t the most natural way to do this in Python. It would be more comfortable to use a regular dictionary for the room directions:

class Room:
    def __init__(self, name):
        self.name = name
        self.exits = {}

# Define the rooms
Kitchen = Room('Kitchen')
Pantry = Room('Pantry')

# Set up the room exits
Kitchen.exits['n_to'] = Pantry
Pantry.exits['s_to'] = Kitchen

location = Kitchen

def GoAction(noun):
    dir = noun.dir
    dest = location.exits.get(dir)
    if dest is None:
        print('You cannot go that way.')
    else:
        print('Going to', dest.name)    

But Inform doesn’t have the dictionary data structure, so it has to use properties for everything.

2 Likes

I think I’ve got it now. Python can just use a string where TADS uses the &propertypointer syntax… Thanks!

The actual answer got buried in all that code, didn’t it? And it’s kind of a subtle distinction. Let me give you a briefer answer, taking all of the above as a given.

Say you do this:

bob = Person()
juanita = Person()
r = Raincoat()
f = Flower()

juanita.wearing.append(r)

w = WeatheryRoom()
w.contents = [bob, juanita, f]

queue_event(bob._die)
queue_event(lambda: Person._die(juanita), 5)

… the distinction between bound methods and unbound methods is in those last two lines. (Ignore the lambda keyword for a moment.) When you queue bob._die (note the absence of parentheses: you’re referring to the bob._die function, but you’re not calling the function at that point), the function pointer, if you want to call it that, includes a reference to the specific object, bob. (That’s a bound method: the function pointer you’re talking about is bound to the specific bob instance of the Person class.)

In contrast, when you’re talking about the Person._die function, you’re talking about a function that applies to a specific class of objects (and, presumably, its subclasses), but doesn’t (yet) include a reference to a specific Person: the method is unbound in this sense, i.e. not tied to a specific instance of the class. In this particular case, it’s immediately applied to the Person in the juanita variable (and applying that parameter with parentheses means that you need to use the lambda keyword to defer evaluation of the expression until it’s time to call it; otherwise, the parentheses in Person._die(Juanita) would result in the function call being made immediately when the action was queued). There’s no reason why you would need to immediately pair it with its object, though: you could instead pair it with a function that makes the decision at the time the code is called, perhaps by doing something like queue_event(lambda: Person._die(random_visible_person())), which would defer calling the random_visible_person function until it was time to execute that code.

Which means that having lambda: Person._die(juanita) is kind of weird and hacky, a contrived example for showing how you might shoehorn an unbound method into the event-scheduling mechanism I sketched out earlier. But it’ll work, if you want to do things that way. Or, if you’re going to be doing a lot of things that require pairings that can’t be made at the time events are queued, maybe you want to use two separate queues – one of verb functions, and a separate queue of functions to pick the objects of those verbs – and then keep the two queues in sync.

All of which is to say that – assuming already that your fundamental world model for the game works the way I’ve sketched out here, though of course there are plenty of other ways it could work – you can in fact refer just to the action (by providing a “function pointer,” though Python doesn’t use that exact terminology, to Person._die) rather than queuing the whole event, both verb and direct object, at once. You’ll just need to also write some logic to determine what the direct object of the verb is before you can call the function that carries out the verb that requires a direct object.

Or, to give what is perhaps a more direct answer to your question: Python doesn’t distinguish between pointers to properties (i.e., object attributes: “property” has a specific meaning in Python that, I think, is different from what it means in TADS, but let’s not get into that here) and pointers to integers, strings, metaclasses, functions, or anything else. In a sense, any name-to-object binding in Python already is a (more or less C-style) pointer, a reference to that thing. (Again, in Python, everything is an object, and every name you give to a variable, or a function, or a method of a class, or a method of an instance of a class, is just a pointer to that particular object.) But an unbound method – a reference to a class’s method, which needs to be paired with an instance of that class when you call it – seems to be closer to what you were originally asking, which I took to be along the lines of “can I encapsulate a reference to an action without having to tie that action to a specific object at the time I create the reference?”

Again, there’s no particular reason why the world model has to be set up the way I’ve sketched it out: unlike with TADS, you’re not dealing with a world model that’s been pre-built, so you have to (but also get to. But mostly have to) build the whole world mechanism from scratch. So you could in fact set up your event-dispatching code in any way that you want, and could in theory build Event objects and a WeatherEvent subclass, and could set up your event-scheduling mechanism to expect, or even to enforce, that model:

event_list = dict()        # int -> list[Event]: that is, index, by turn count, a list of Events scheduled for that turn. Probably would realistically be in `globs`, or maybe in a separate module of event-handling code, but it's conceptually easier to put it here in this example.

class Event(object):
    def __init__(event_name, location, duration):
        self.name = event_name
        self.location = location
        self.duration = duration

    def _begin_event(self):
        pass     # subclasses must override this to do anything useful

    def _every_turn(self):
        pass     # subclasses must override this to do anything useful

    def _end_event(self):
        pass     # subclasses must override this to do anything useful

class WeatherEvent(Event):
    pass     # There's no obvious reason to be defining an intermediate WeatherEvent class here, but in real life there would probably be *some* common methods and/or data for weather-related events and it would make sense to store them as attributes of this class.

class Rainstorm(WeatherEvent):
    def _begin_event(self):
        if globs.the_player in location.contents:     # Don't report offscreen events
            print("An enormous crack of thunder breaks overhead, and the skies open up, dumping rain on you.")

    def _every_turn(self):
        for what in location.contents:    # Run the react-to-rain trigger for each object in the location
            what.react_to_rain()

    def _end_event(self):
        if globs.the_player in location.contents:    # Don't report offscreen events
            print("The last of the rain grudgingly drizzles out of the clouds, and a small swath of blue sky peeks out from behind the clouds.")

def queue_event(the_event, in_how_many_turns=1):
    assert isinstance(the_event, Event)
    if (in_how_many_turns + globs.turn_count) in event_list:
        event_list[in_how_many_turns + globs.turn_count].append(the_event)
    else:
         event_list[in_how_many_turns + globs.turn_count] = [the_event]

You would then need to write some code to check every turn which events are supposed to be happening, start and stop them, and run their every-turn code, which wouldn’t be that much work (but it’s enough that I’m going to skip it here).

But, again, there’s no particular reason (aside, perhaps, from a love of conceptual purity or balance or something) to require that queue_event can only take an Event object; and using the previous model, queuing only functions in the event queue, doesn’t prevent you from working with an Event object if you want to:

def queue_event(action_func, in_how_many_turns=1):
    if (in_how_many_turns + globs.turn_count) in event_list:
        event_list[in_how_many_turns + globs.turn_count].append(action_func)
    else:
         event_list[in_how_many_turns + globs.turn_count] = [action_func]

def top_level_every_turn_handler():
    # let's assume this gets called somewhere in the main parsing loop
    globs.turn_count += 1
    for which_event in event_list[globs.turn_count]:
        which_event._event_action()
    # clean out past events to prevent it from growing too larger
    for index in [i for i in events_list.keys() if i <= globs.turn_count]:
        del event_list[index]

class Event(object):
    def __init__(event_name, location, duration):
        self.name = event_name
        self.location = location
        self.duration = duration

    def _begin_event(self):
        pass     # subclasses must override this to do anything useful

    def _every_turn(self):
        pass     # subclasses must override this to do anything useful

    def _end_event(self):
        pass     # subclasses must override this to do anything useful

class WeatherEvent(Event):
    pass

class Rainstorm(WeatherEvent):
    def _begin_event(self):
        if globs.the_player in location.contents:     # Don't report offscreen events
            print("An enormous crack of thunder breaks overhead, and the skies open up, dumping rain onto you.")
        self.every_turn()    # run the every-turn trigger once, immediately
        for t_num in range(self.duration):     # Queue each every-turn event in advance
            queue_event(self._every_turn, 1+t_num)
        queue_event(self._end_event, self.duration)    # Queue the end-of-rainstorm event in advance, too

    def _every_turn(self):
        for what in location.contents:    # Run the react-to-rain trigger for each object in the location
            what.react_to_rain()

    def _end_event(self):
        if globs.the_player in location.contents:    # Don't report offscreen events
            print("The last of the rain grudgingly drizzles out of the clouds, and a small swath of blue sky peeks out from behind the clouds.")

This gives you the flexibility to write event-handling code in different ways without worrying much about the interface to the queuing mechanism, so that you can write an Event for more complex things but use simple utility functions for the simpler events. That is, you could do this:

def earthquake():
    print("There is a great tremor from within the earth.")
    globs.aqueduct.break()
    globs.great_door.break()
    globs.key_room.disconnect_exit('down')

class TerrifyingBatWithLotsOfTeeth(Noun):
    def __init__(self):
        globs.the_player._where.contents.append(self)
        print("From nowhere, a terrifying giant bat appears! It has lots of teeth!")
    
r = Rainstorm('a spring shower', w, 4)
queue_event(r._begin_event, in_how_many_turns=3)
queue_event(TerrifyingBatWithLotsOfTeeth, in_how_many_turns=10)
queue_event(earthquake, 110 + random.randint(40))

… and the event-initialization code for Rainstorm, as written in its most recent iteration, would itself take care of scheduling the rest of the event’s components. Again, what you’re passing to a structure that expects simply a callable function would be the _begin_event function that’s bound to the Rainstorm object you’ve created, and the data and methods associated with that object (and its class, and its superclass(es), and their superclasses) are all available to that bound method. If you’re building your event-scheduling mechanism from scratch, there’s something to be said for keeping it as simple and generic as possible, and being able to pass in any kind of callable written to work in any way imaginable.

But maybe you really do want to have a scheduling system that requires that individual events be of type Event – maybe it’s easier to handle the event-dispatching every-turn logic that way, for instance: by wrapping the tests and the relevant data in an object, and having the code that manages those objects expect that they be full instances of a class that necessarily has all of the relevant data. In that case, you could certainly rewrite earthquake as an Event, along the lines sketched out above; or you could leave the earthquake function as-is and write an Event that calls it when the earthquake happens. Similarly, you could wrap the bat-creation code in an Event object too, if you wanted. It all depends on how you want your world model to work.

By the way, you certainly can find all subclasses of WeatherEvent, and this is itself possibly a good reason to have an intermediate WeatherEvent class–maybe you have a hundred and fifty different subtypes of WeatherEvents, and you don’t want to have to maintain a hard-coded list of them because (a) it’s tedious to have to add an entry manually to a separate list for MediumBlusteryWindWithModerateSleetFollowedByBrightSunshineWeatherEvent every time you define a new atmospheric condition, and/or (b) you’re worried you’ll forget to hard-code one (or a few) and will then be tearing your hair out over why some events never seem to get generated. If you have this:

class Event(object):
     pass
 
class WeatherEvent(Event):
     pass
 
class Rainstorm(WeatherEvent):
     pass
 
class Blizzard(WeatherEvent):
     pass
 
class SunnyDay(WeatherEvent):
     pass

class HotSunnyDay(SunnyDay):
     pass

… then it’s easy to inspect the WeatherEvent class (which is of course also an object of class type, because everything in Python is an object):

>>> WeatherEvent.__subclasses__()
[<class 'Rainstorm'>, <class Blizzard'>, <class 'SunnyDay'>]

… and this gives you references to the objects representing direct subclasses of WeatherEvent. Notice that HotSunnyDay is missing from that list: it’s an indirect descendant of WeatherEvent, because its immediate ancestor is SunnyDay, not WeatherEvent. But you can iterate over the list you get, recursively looking for subclasses of those classes until you’ve found everything beneath the starting point, which isn’t hard.

You could then create a random WeatherEvent and queue it:

def all_subclasses_of(what_class):
    # including subclasses of subclasses
    assert isinstance(what_class, type)
    ret = set()
    for sub in what_class.__subclasses__():
        ret.add(sub)
        for subc in all_subclasses_of(sub):
            ret.add(subc)
    return ret

def random_weather_event():
    event_type = random.choice(list(all_subclasses_of(WeatherEvent)))
    event_instance = event_type()  # Doesn't use the same call signature defined above, which would lead to an error in real life, but this is just a conceptual example and that's one of the details that would have to be worked out if this were actually to be implemented.
    queue_event(event_instance, 1)

… and that would choose a WeatherEvent subclass (or more distant descendant class) at random, create an Event of the chosen subtype, and then pass it to a routine that queues future events and expects instances of type Event. (Then again, there’s no reason why you couldn’t write your framework to expect that an Event subclass, rather than an instance, be passed in, and then creates an instance of the specified type on its own, and that might be a more convenient way to build your framework. It all depends on how you want to set things up.) (I use a set rather than a list in all_subclasses_of, above, because it’s the easiest way to avoid duplicates; but random.choice doesn’t like sets, so it has to be converted into a list before picking an item from it. Easy enough.)

Does that make sense? I’ve once again written much more than I expected to.

1 Like

Again, Patrick, very informative, thanks a lot. So I guess that unbound methods aren’t exactly identical to the TADS property pointer, since they are specific to a class, but between using those, and the string value/getattr approach that you and Andrew illustrated, you get the same things done. I’m familiar with the lambdas because it’s almost the exact same thing as a TADS3 anonymous function.
Are you in the middle of writing IF in Python right now?
Also, fun fact: my one-year-old daughter loves smoothies right now, and whenever she asks for one, she says: “Mooney!” :slight_smile:

Haha, I’m curious where that particular linguistic association comes from. (But of course parents don’t always know how their offspring comes up with those associations, either.) My own family’s joke (“you can always find a relative at the airport”) hasn’t aged well over the last few decades, as the referent of the joke has kind of dropped out of popular awareness.

I’m glad to be helpful. I do in fact have a half-written piece of parser IF in Python, and would like to pick it back up sometime and finish it, although I’ve shelved it while I work on a different project in Inform. (My TADS is in fact pretty minimal: I’ve skimmed through the manual and played around with the workbench while initially trying to pick a “real IF language” after writing big chunks of my own standard library in Python, which looks only sort of like what I’ve sketched out here. Inform wound up drawing me over TADS partly because I wanted something using a different language paradigm than any of the other computer languages I’m familiar with, and partly because non-Windows people are not supported in that ecosystem as well as Windows people. These days I’m curious what’s going to happen with Dialog, though, and maybe that’ll be the language of my third release, if the first two ever come out.) But that half-written work on my hard drive still keeps reminding me that it wants to be finished, and there are things about the story I like a lot, so maybe someday you’ll see a release version of Zombie Apocalypse: A Love Story, if I first get this piece of dystopian SF done in Inform.

I suppose one other approach you could take in Python, if you were writing everything from scratch, would be to have the parser encapsulate verbs (or actions) as objects, and then write code to match them to in-game things and dispatch appropriately. I’ll refrain from sketching out a code example on that one, though: I think it would be complex even though it may pay off in the long term.

1 Like