Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Using multiple dispatch to write text adventures

8 views
Skip to first unread message

John Wiegley

unread,
Feb 20, 2002, 2:21:15 AM2/20/02
to
Comments requested..

I've tried object-oriented languages, and procedural languages, for
writing text adventures. I've used event-based frameworks,
action-based frameworks, and combinations of the two. But once a
world starts getting complex, all the current methodologies seem to
get exponentially more complicated. I want a system which will let me
focus on authoring text, and less on debugging complicated bugs.

OO looks like it would be a good model, since everything in a text
adventure is essentially an object. But it seems wrong to me now that
we associate methods with a particular object, when really they belong
to the *association between sets of objects*. With this in mind, I've
attempted to design a system that uses dynamic, multiple dispatch to
implement responses to object interactions.

I'm using Lisp as the host language for the first cut, and am
currently porting the "Alice" Inform script to it. Initial results
look promising. But I would like to ask for feedback from the
experienced people here before going too much further.


The system itself is extremely simple. There are essentially two
forms of declaration:

(defobject BASE NAME
:alias (NAMES...)
:contents (OBJECTS...)
FORMS...)

This defines an object with the given NAME, as being derived from BASE
(an object). BASE may also be a list. :alias and :contents are
optional, with the latter only applying to locations and containers
(although this is not enforced). The `move' function is responsible
for doing the necessary house-keeping.

FORMS must yield a string value. This is used to describe the object.

Verbs are also objects. They descend from "verb" which descends from
"object", the base type:

(defobject object "verb")
(defobject verb "drop")
(defobject object "sword")

Now we have a drop verb, and a sword object. So let's define what
happens when you drop the sword, using defmethod:

(defmethod VERB-OBJECT DIRECT-OBJECT [INDIRECT-OBJECT]
:transparent
FORMS...)

This defines an association between the imperative verb object, any
direct object, and any involved indirect object. All of the objects
specified may be base types, or derived types. The higherarchy is
searched using a customized breadth-first traversal in order to find
the most applicable dispatch method. The :transparent optional
keyword can be used to allow the search to resume, even after a set of
FORMS has been executed.

FORMS, again, must yield a string. This is what gets displayed to the
user after performing the interaction. Back to the example:

(defmethod drop sword
"The sword drops, clattering onto the ground.")

This would be a specific definition. A more generic one would be:

(defmethod drop object
(format "You drop %s on the ground." second)


That's all there really is to the system. Since I'm using Lisp, the
FORMS code is free to get and set arbitrary properties on any of the
objects involved. This allows for complicated interactions in the
relevant dispatch methods. Also, Lisp allows for new objects/methods
to be created at run-time, permitting a self-enriching system if so
desired.

Here is a tiny example world I wrote:

(defobject object "place")

(defobject place "Front Lawn"
:alias lawn
:contains
((defobject object sprinkler
(if (get this 'running)
"There is a sprinkler here sending water everywhere."
"There is a sprinkler, off, in the middle of the lawn."))
(defobject object grass))
"You see a large, green lawn.")

;; glancing is used to generate "one line descriptions". See
;; adventure.el for the definition of the glance method.

(defmethod glance sprinkler
(invoke second)) ; just use the "look" description

;; Using a string as the first arg to defmethod, implicitly creates
;; a verb object of that name.

(defmethod "turn on" sprinkler
(let ((now (get second 'running)))
(put second 'running (not now))
(if now
"You turn off the sprinkler."
"With a slow squeak, the sprinkler comes to life!")))

(defmethod "turn off" sprinkler
(invoke "turn on" second))

(adventure (object "lawn")) ; start us on the lawn!

Here is a preliminary implementation of the above described system in
Emacs Lisp:

----------------------------------------------------------------------
;;; adventure.el --- a multiple dispatch-based text adventure system

;; Copyright (C) 1999, 2000 Free Sofware Foundation

;; Author: John Wiegley <jo...@gnu.org>
;; Keywords: languages lisp games fun

;; This file is not yet a part of GNU Emacs.

;; GNU Emacs is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;;; Commentary:

;; There is only one defined basic type: object. Types are not
;; static, but dynamic, meaning that inheritance is determined among
;; objects at runtime, not among types at compile time. With that in
;; mind, this is the syntax:
;;
;; (defobject OBJECT-OR-LIST STRING
;; [:alias LIST-OR-SYMBOL]
;; [:contents LIST]
;; FORMS...)
;;
;; Defines a new object named STRING. It derives from OBJECT (or the
;; list of objects given). The :alias tags specifies other aliases
;; known for this object. These aliases may be strings, symbols
;; (single word symbols are identical to their string representation),
;; or recursive object declarations in the case that the object was
;; never declared previously.
;;
;; The :contents tag specifies objects that this object contains.
;; Otherwise, objects are contained in a universal space.
;;
;; FORMS are invoked if the user's input ever names just this object
;; alone.
;;
;; (defmethod OBJECT1 OBJECT2 [OBJECT3]
;; [:transparent]
;; FORMS...)
;;
;; `defmethod' defines a dispatch for a conjunction of objects. For
;; example, if a user types:
;;
;; hit table with hammer
;;
;; This is parsed into three object references: the "hit" verb object,
;; the "table" container object, and the "hammer" object. The
;; interpretor will look for a dispatch that defines the association
;; "hit table hammer". If it cannot find this, it will consider the
;; bases of each object involved, and continue the search. If no
;; match is found at all, it will display an error.
;;
;; If a match IS found, it will invoke the FORMS defined for that
;; dispatch. If :transparent is specified, then after invoking those
;; FORMS, the search will resume for any other matching dispatches. If
;; no more are found, it is not an error.
;;
;; The return value of FORMS, if it is a normal string, is printed.
;; If the declaration is :transparent, it is still printed, and any
;; other dispatch methods that are invoked will append their text
;; after it.
;;
;; Other useful utility functions:
;;
;; (object NAME-OR-SYMBOL) ; returns the object symbol
;; (action TEXT) ; parses an invokes TEXT
;; (invoke OBJECT...) ; invokes a dispatch among OBJECT(S)
;; (move OBJECT OBJECT) ; move an OBJECT to another
;; (display TEXT) ; display TEXT to the user
;; ; NOTE: almost never needed
;; (get OBJECT ATTR) ; get an object attribute
;; (put OBJECT ATTR VALUE) ; set an object attribute
;;
;; Some special attributes:
;;
;; name* ; full name of the object
;; location* ; object it's located in (or nil)
;; contents* ; objects it contains (or nil)
;; bases* ; base objects
;; seen* ; t if we've described this place
;; <dir>* ; object lying in different directions
;;
;; You must define an object before you can reference it, but there is
;; nothing to prevent defining and modifying objects on the fly.

(defvar objects (make-vector 1023 nil))
(defvar aliases (make-hash-table :test 'equal))

(defsubst object-name (obj)
(cond
((stringp obj) (downcase obj))
((symbolp obj) (downcase (symbol-name obj)))))

(defsubst object (ref)
(let ((name (object-name ref)))
(and name
(or (intern-soft name objects)
(gethash name aliases)))))

(defsubst name (obj) (get obj 'name*))
(defsubst location (obj) (get obj 'location*))
(defsubst contents (obj) (get obj 'contents*))

(defun define-object (bases name &optional args)
(let* ((sym (intern (object-name name) objects))
(arg args) (first arg))
(put sym 'name* name)
(put sym 'location* nil)
(put sym 'bases*
(if (listp bases)
(mapcar 'object bases)
(list (object bases))))
(while arg
(cond
((eq (car arg) ':alias)
(setq arg (cdr arg))
(let ((alias (car arg)))
(if (not (listp alias))
(puthash (object-name alias) sym aliases)
(while alias
(puthash (object-name (car alias)) sym aliases)
(setq alias (cdr alias)))))
(setq arg (cdr arg) first arg))
((eq (car arg) ':contains)
(setq arg (cdr arg))
(let ((contents (car arg)))
(while contents
(setcar contents (or (object (car contents))
(eval (car contents))))
(put (car contents) 'location* sym)
(setq contents (cdr contents))))
(put sym 'contents* (car arg))
(setq arg (cdr arg) first arg))
(t
(setq arg (cdr arg)))))
(put sym 'body* first)
sym))

(defmacro defobject (bases name &rest args)
`(define-object (quote ,bases) (quote ,name) (quote ,args)))

(defun define-method (obj1 obj2 args)
(let ((objs (list (or (object obj1)
(define-object "verb" (object-name obj1)))
(object obj2)))
(obj3 (object (car args)))
method transparent sym)
(when obj3
(nconc objs (list obj3))
(setq args (cdr args)))
(if (eq (car args) ':transparent)
(setq transparent t))
(put (intern (mapconcat 'object-name objs "-") objects) 'body* args)))

(defmacro defmethod (obj1 obj2 &rest args)
`(define-method (quote ,obj1) (quote ,obj2) (quote ,args)))

(defun build-connections (obj directions)
(while directions
(put obj (object-name (car directions))
(cadr directions))
(setq directions (cddr directions))))

(defmacro connect (obj &rest args)
`(build-connections (object (quote ,obj)) ,args))

(defun parse-command (text)
(let ((words (split-string text)) count max objs obj)
(while words
(setq count 1 max (length words))
(while (<= count max)
(let ((tmp (nthcdr count words)))
(if tmp
(setcdr (nthcdr (1- count) words) nil))
(setq obj (object (mapconcat 'downcase words " ")))
(if obj
(setq words tmp count (1+ max)
objs (cons obj objs))
(if tmp
(setcdr (last words) tmp))
(setq count (1+ count)))))
(if (= max (length words))
(setq words (cdr words))))
(nreverse objs)))

(defvar real-objects nil)

(defun invoke (&rest objs)
(if (stringp (car objs))
(setcar objs (object (car objs))))
(let ((cmd (intern-soft (mapconcat 'object-name objs "-") objects)))
(if cmd
(let* ((this (nth 0 (or real-objects objs)))
(second (nth 1 (or real-objects objs)))
(third (nth 2 (or real-objects objs)))
(str (eval (cons 'progn (get cmd 'body*)))))
(if (stringp str)
str))
(catch 'done
(let ((baseslist
(nreverse (mapcar (function
(lambda (obj)
(get obj 'bases*))) objs)))
(index (1- (length objs)))
objcopy)
(while baseslist
(setq objcopy (copy-alist objs))
(let ((bases (car baseslist)) value)
(while bases
(setcar (nthcdr index objcopy) (car bases))
(let ((real-objects objs))
(if (setq value (apply 'invoke objcopy))
(throw 'done value)))
(setq bases (cdr bases))))
(setq index (1- index)
baseslist (cdr baseslist))))))))

(defsubst action (text)
(apply 'invoke (parse-command text)))

(defvar adventure-mode-map nil
"A mode map for catching the RET key in ADVENTURE mode.")

(defun adventure-mode ()
"Major mode for running text adventures."
(interactive)
(text-mode)
(set (make-local-variable 'scroll-step) 2)
(setq adventure-mode-map (make-sparse-keymap))
(define-key adventure-mode-map "\r" 'send-input)
(use-local-map adventure-mode-map)
(setq major-mode 'adventure-mode)
(setq mode-name "ADVENT"))

(defun display (&rest args)
"Print out a message to the display."
(goto-char (point-max))
(let ((beg (point)))
(beginning-of-line)
(when (< (point) beg)
(goto-char beg)
(insert "\n"))
(setq beg (point))
(while args
(if (and (car args) (stringp (car args)))
(insert (car args)))
(setq args (cdr args)))
(insert "\n")
(fill-individual-paragraphs beg (point))
(unless (bolp)
(forward-char))))

(defun prompt ()
"Output a prompt to the sure, since we're expecting input."
(let ((here (point)))
(beginning-of-line)
(if (< (point) here)
(delete-region (point) here)))
(insert "> ")
(goto-char (point-max))
(recenter -1))

(defun send-input ()
"Get a command from the user after telling them what's going on."
(interactive "*")
(beginning-of-line)
(let ((beg (point)))
(end-of-line)
(let ((cmd (buffer-substring-no-properties beg (point))))
(while (string-match "^> " cmd)
(setq cmd (replace-match "" nil t cmd)))
(if (= (length cmd) 0)
(insert "\n")
(catch 'handled
(let ((text (or (action cmd) "Command not understood.")))
(display text))))
(prompt))))

(defun move (obj where)
"Move the object OBJ from its current place to the place connected to
that place via the direction DIRECTION."
(let ((previous (location obj)))
(put obj 'location* where)
(if (memq obj (contents previous))
(set previous 'contents*
(delq obj (contents previous))))))

(defvar adventure-version "0.1"
"First release of the ADVENTURE text adventure engine.")

(defun adventure (start &optional intro)
"Let the adventure begin! Leap into the past, and the world of text
adventures that beguiled the wits, and fired the imagination. Don't let
them fool you: such colorful thinking might leave you intractable to
ordinary life: so beware! And enter with such cautious frame of mind
as is begotten by our later generations..."
;; switch to the game buffer...
(switch-to-buffer "*adventure*")
(delete-region (point-min) (point-max))
(adventure-mode)
(if intro (funcall intro))

;; always display the current location in the mode line
(make-local-variable 'global-mode-string)
(setq global-mode-string
(append global-mode-string
'(" "
(:eval (name (location (object "me")))))))

;; if initialization succeeds, let the games begin!
(move (object "me") start)
(catch 'handled (action "look"))
(prompt))

;; Now for some basic objects and methods that are always available

(defobject nil "object") ; the most basic object
(defobject object "me")

(defobject object "place")
(defobject object "container")
(defobject object "verb")

(defobject verb "look"
:alias "examine"
(let* ((here (location (object "me")))
(items (contents here)))
(display (invoke here))
(while items
(display (invoke (object "glance") (car items)))
(setq items (cdr items)))
(throw 'handled t)))

(defobject verb "glance")
(defmethod glance object
(format "There is a %s here." (name second)))

(defmethod look object
:alias "examine"
(invoke second))

(defobject verb "inventory"
:alias ("inv" "i")
(let ((items (contents (object "me"))))
(if (null items)
"You are empty-handed."
(display "You are carrying:")
(while items
(display (concat " " (name (car items))))
(setq items (cdr items)))
(throw 'handled t))))

(defun move-direction (dir)
(let* ((me (object "me"))
(there (get (location me) dir)))
(if (null there)
"You cannot go in that direction."
(move me there)
(action "look"))))

(defobject verb "north" :alias "n" (move-direction "north"))
(defobject verb "south" :alias "s" (move-direction "south"))
(defobject verb "east" :alias "e" (move-direction "east"))
(defobject verb "west" :alias "w" (move-direction "west"))
(defobject verb "northwest" :alias "nw" (move-direction "northwest"))
(defobject verb "northeast" :alias "ne" (move-direction "northeast"))
(defobject verb "southwest" :alias "sw" (move-direction "southwest"))
(defobject verb "southeast" :alias "se" (move-direction "southeast"))
(defobject verb "up" :alias "u" (move-direction "up"))
(defobject verb "down" :alias "d" (move-direction "down"))

(provide 'adventure)

;; adventure.el ends here

Kodrik

unread,
Feb 20, 2002, 4:28:15 AM2/20/02
to
> But I would like to ask for feedback from the
> experienced people here before going too much further.

We took the same approch but with a difference in our implementations.
I find it indeed very intutive and allow for an easy creation of extremelly
complex stories.

On the approach, here are a few things that could help you.
* At first, I implemented an item as an object whose status changes when
taken into inventory. But when an item is taken, it has totally new
properties, so it would be better if when taken, a seperate object goes
into the author's possession. You probably already have a replace object
sstem, if you don't it's important.
* For movement, I didn't use a directional system, instead every passage
object you define has a target room that can be anywhere. When a command
activates the passage, you go to the target room. You can have as many
passages as you want to go to any room you want. Also, if you define
multiple rooms for a passage, it wil randomly transport you to one of the
target room. Whatever directional system you use, it's just an illusion for
the auhor. You just set a key with the command "north" to go to room #323.
The engine itself most of the time doesn't need the room locations, only
their relations.
It did implement a four coordinate system for rooms to make it easier for
the author to reference them and add some features such as the maze talked
about in a previous thread.
I also found a use for the "replace object" system with rooms. In some
situation, you want to have a room replace the one you are at instead of
moving to this room.
MY most important advice in this post: Redo your system to implement room
relations instead of directional relations.
* I didn't implement the transparent system. On processing of a user
command, only the first form (I think that's how you call it, I call it a
key) encountered gets executed.
Since a key (form) can modify any object in the game, there is no need to
execute multiple one from user input. I realized that allowing multiple
forms to be executed from a key input makes it easier for authors to have
bugs in their stories. It's just a detail and I might be wrong.
I do allow multiple forms to be used in environment triggers (when turn
hits 2, process forms 1, 4 and 43).

I have more to say but first tell me if I'm answering your question. It
would be a waste of both our times if I'm OT.

Robin Rawson-Tetley

unread,
Feb 20, 2002, 6:37:26 AM2/20/02
to
> I'm using Lisp as the host language for the first cut, and am
> currently porting the "Alice" Inform script to it. Initial results
> look promising. But I would like to ask for feedback from the
> experienced people here before going too much further.

I seem to recall that Infocom's original MDL (pronounced "muddle")
language was an implementation of Lisp for the Z-Machine (Inform was
of course not heard of in the Infocom days for very obvious reasons!).

Anyway - the language is only how you are looking at solving your
problem. OO solves problems by making everything an object, Lisp
solves problems by making everything a list (hence the name LisP -
List Processor).

I think you would probably find that it's a case of "horses for
courses" and your Lisp-like language may make certain tasks easier and
offer an edge over OO in some respects, but it will not be as
efficient in others.

Still, I'll be interested to see it when you are done.

Cheers,

Bob

Andrew Plotkin

unread,
Feb 20, 2002, 10:22:55 AM2/20/02
to
Robin Rawson-Tetley <robin.raw...@btinternet.com> wrote:
>> I'm using Lisp as the host language for the first cut, and am
>> currently porting the "Alice" Inform script to it. Initial results
>> look promising. But I would like to ask for feedback from the
>> experienced people here before going too much further.

> I seem to recall that Infocom's original MDL (pronounced "muddle")
> language was an implementation of Lisp for the Z-Machine (Inform was
> of course not heard of in the Infocom days for very obvious reasons!).

Not exactly -- it was a program that generated Z-code, using Lisp-like
data/source files. The final Z-code game did not incorporate a Lisp
engine.

> Anyway - the language is only how you are looking at solving your
> problem. OO solves problems by making everything an object, Lisp
> solves problems by making everything a list (hence the name LisP -
> List Processor).

I think you're comparing apples to fruit-seeds here. Different
categories. You can write OO programs in Lisp, and you can create list
classes in other OO languages. (Or, for that matter, you can write an
OO program in C and then create a list class in it... three different
categories there.)

None of which is relevant to the original thread. :)

--Z

"And Aholibamah bare Jeush, and Jaalam, and Korah: these were the borogoves..."
*
* Make your vote count. Get your vote counted.

David Betz

unread,
Feb 20, 2002, 10:55:19 AM2/20/02
to
> > I seem to recall that Infocom's original MDL (pronounced "muddle")
> > language was an implementation of Lisp for the Z-Machine (Inform was
> > of course not heard of in the Infocom days for very obvious reasons!).
>
> Not exactly -- it was a program that generated Z-code, using Lisp-like
> data/source files. The final Z-code game did not incorporate a Lisp
> engine.

Actually, MDL as a Lisp dialect predates Infocom. It was used to write the
original mainframe version of Zork. Infocom invented a subset of MDL called
ZIL (Zork Implementation Language) and a compiler for it that generated
Z-code to run on micros.


Adam Thornton

unread,
Feb 20, 2002, 11:37:50 AM2/20/02
to
In article <871yfgm...@alice.dynodns.net>,
John Wiegley <jo...@gnu.org> wrote:
>Comments requested..

Short comment: very cool, but I'd never use it. That is, however,
because I'm happy with TADS and Inform and I haven't touched Lisp since
a Scheme college course nearly a decade ago.

Adam

Frobozz

unread,
Feb 20, 2002, 1:47:58 PM2/20/02
to

Adam Thornton wrote:

Besides, hardly anyone where I live is using Lisp. Most of the people in my
area
don't even use computers! They're only a few programmers in the area, most
of them
my age (about 18 or less).

John Wiegley

unread,
Feb 20, 2002, 5:13:52 PM2/20/02
to
>>>>> On Wed Feb 20, Andrew writes:

> I think you're comparing apples to fruit-seeds here. Different
> categories. You can write OO programs in Lisp, and you can create
> list classes in other OO languages. (Or, for that matter, you can
> write an OO program in C and then create a list class in it... three
> different categories there.)

> None of which is relevant to the original thread. :)

Yes, perhaps my choice of Lisp obscured the underlying attempt at
innovation. Lisp was an arbitrary choice, merely because it was
handy, and made the idea easy to express. I also toyed with writing a
new syntax to reduce the keywordage further, but thought I should wait
until the idea itself was proven.

So, forget that I'm using Lisp. The end product may have nothing to
do with Lisp. It's the idea of multiple dispatch that I'm testing.
It sounds like what I need now is a genuine proof of a real problem,
like re-implementing Alice, to show whether the system actually offers
any real power over current methods.


One thing that multiple dispatch does allow you to do is to enrich
base types without modifying them. For example, OO can represent a
"sharpen" method on a sword pretty easily:

type sword : object {
method sharpen(object) {
I am sharpening the sword, use object to do so.
}
}

But what if you want to specialize "cut" on the base object, relative
to swords? It would have to be done like this:

type object {
method cut(sword) {
I am cutting an object, using the sword to do so.
}
}

Yet this pretty much erases the abstractions gained from OO, by
creating a system in which every object may or may not need to know
about the type of every other object. With multiple dispatch, the
specialization of "sword" can occur at any time, in reference to any
object:

defmethod sharpen sword object
Sharpening the sword, using object.

defmethod cut object sword
Cutting an object, using the sword.

Now, no _object_ knows about any other. It is the associations that
know everything about their possible conjunctions. And since base
type can be used (as with OO), it's only necessary to specialize in
the least abstract terms:

defmethod sharpen sword hard
You sharpen the sword against the hard thing (an object derived
from hard).

>>>>> On Wed Feb 20, Kodrik writes:

> MY most important advice in this post: Redo your system to implement
> room relations instead of directional relations.

I hadn't actually dealt with connections yet. I will consider your
advice. I have two different thoughts so far. My hope is to make it
EASY and CHEAP to build connections, not necessarily super-powerful.

1. Use a "connect" operator to bind locations directionally after they
have been defined:

(connect "Front Lawn"
n "White House"
s "Driveway"
e "Park")

2. Have the exit directions be objects themselves. When a user types
"north", it would invoke the dispatch method on the "north" object:

(defobject object "exit"
"You cannot go that way.")

(defobject "Front Lawn"
:contents ((defobject exit "north" :alias "n"
(move me "White House"))
(defobject exit "south" :transparent)
(defobject exit "east" :transparent)
(defobject exit "west" :transparent)))

This of course could be syntactically optimized in any number of
ways, for example:

;; setup the standard "north" object, making the verb and alias
;; globally available
(defobject verb exit)
(defobject exit north :alias "n")

(defobject "Front Lawn"
:define "north" (move me "White House"))

The ":define" directive temporarily overrides the definition of any
object, providing a local definition used within the scope of that
location.

Again, all this syntax is arbitrary. Once the core ideas are
correctly defined, it can modified in whatever manner desires, and
hoisted onto whatever underlying syntax people happen to favor.

mathew

unread,
Feb 20, 2002, 10:57:53 PM2/20/02
to
In article <87vgcrl...@alice.dynodns.net>,

John Wiegley <jo...@gnu.org> wrote:
> Yes, perhaps my choice of Lisp obscured the underlying attempt at
> innovation.

You might like to consider Objective-C. It allows multiple dispatch, as
well as run-time computation of messages and selectors. Plus the C
syntax won't frighten the horses.

It's supported by any current version of gcc/egcs, so it's pretty
portable too.

The ability to do message forwarding would also be really useful for IF
implementation, I think.


mathew

--
<me...@pobox.com> / journal at <URL:http://www.pobox.com/~meta/>
Sending me e-mail indicates that you consent to my ignoring any generic
legal disclaimers, copyrights or licenses attached to your e-mail.
Bulk unsolicited e-mail and resumes sent to me may be published.

Dan Shiovitz

unread,
Feb 20, 2002, 11:17:31 PM2/20/02
to
In article <87vgcrl...@alice.dynodns.net>,
John Wiegley <jo...@gnu.org> wrote:
[..]

>
>Yes, perhaps my choice of Lisp obscured the underlying attempt at
>innovation. Lisp was an arbitrary choice, merely because it was
>handy, and made the idea easy to express. I also toyed with writing a
>new syntax to reduce the keywordage further, but thought I should wait
>until the idea itself was proven.

Right. I suspect if you really want this to catch on you'll have to
implement it as an add-on to TADS or Inform or Hugo or something. Or
talk to the authors about adding it, if you can't do it as a library
module. (I suspect it'd be doable to implement multiple dispatch all
in user code in T3, at least, and it's got the macro support to make
it relatively seamless. You could probably do it in TADS 2 also,
although not as nicely, and I imagine it could be done in Inform with
compiler support, since AFAIK Inform's inheritance stuff is all in the
code too. Dunno about Hugo -- similar situation to TADS 2, I suspect).

>So, forget that I'm using Lisp. The end product may have nothing to
>do with Lisp. It's the idea of multiple dispatch that I'm testing.
>It sounds like what I need now is a genuine proof of a real problem,
>like re-implementing Alice, to show whether the system actually offers
>any real power over current methods.

Right, exactly.

>One thing that multiple dispatch does allow you to do is to enrich
>base types without modifying them. For example, OO can represent a
>"sharpen" method on a sword pretty easily:

[..]


>Yet this pretty much erases the abstractions gained from OO, by
>creating a system in which every object may or may not need to know
>about the type of every other object. With multiple dispatch, the
>specialization of "sword" can occur at any time, in reference to any
>object:

[..]


>
> defmethod sharpen sword object
> Sharpening the sword, using object.
>
> defmethod cut object sword
> Cutting an object, using the sword.

[..]

In current systems we can do this already, of course, writing a cut
handler for object and a sharpen handler for sword. We can further
simulate multiple dispatch by letting either object refuse to allow
the action to take place, with before rules in Inform or verify
methods in TADS. Both systems currently require that the actual action
take place in only one spot, yeah, and this potentially causes
problems, but even then, it's generally the case that only one of the
objects is interesting. So, say, for cut, you only care that the
indirect object is a valid cutter and then the action depends on the
material of the direct object. For sharpen, same sort of deal.

You're only going to get a benefit from multiple dispatch when you're
making lots of pairs of objects where the effect depends heavily on
both objects in the pair -- and that just doesn't come up much in
most bits of IF. Maybe in an alchemy simulation or something, but
generally the actual number of cases are small and only one object of
the pair really matters.

(And speaking of pairs, I assume your system will support triples and
so on if necessary)

--
Dan Shiovitz :: d...@cs.wisc.edu :: http://www.drizzle.com/~dans
"He settled down to dictate a letter to the Consolidated Nailfile and
Eyebrow Tweezer Corporation of Scranton, Pa., which would make them
realize that life is stern and earnest and Nailfile and Eyebrow Tweezer
Corporations are not put in this world for pleasure alone." -PGW

nils barth

unread,
Feb 21, 2002, 2:38:15 AM2/21/02
to
Thus wrote Dan Shiovitz <d...@cs.wisc.edu>:

>You're only going to get a benefit from multiple dispatch when you're
>making lots of pairs of objects where the effect depends heavily on
>both objects in the pair -- and that just doesn't come up much in
>most bits of IF. Maybe in an alchemy simulation or something, but
>generally the actual number of cases are small and only one object of
>the pair really matters.

OTOH, isn't this precisely the point?
That is, multiple dispatch offers a kind of solution (or at least
tool) for dealing with the combinatorial explosion.

Suppose you wanted to make any object be able to hit any other object,
and you have a rock, and a window, and a cupcake
(say, objects that are hard/soft/breakable) -- a reasonable enough goal.

Then implementing HIT with multiple dispatches would be pretty
straightforward, right? (or rather, the pain would be managable)
While doing it in standard OO style would require adding hit methods
to everything in sight, which gets ugly and spaghetti-code pretty
quickly.

I think the difference is that OO tends to focus on nouns (even more,
on Subjects or Objects), while what's being suggested is more
verb-focused, it seems.

(coming up with a response to
HIT WINDOW WITH CUPCAKE
is of course left to the reader ;-)

Sean T Barrett

unread,
Feb 21, 2002, 10:05:49 PM2/21/02
to
John Wiegley <jo...@gnu.org> wrote:
>So, forget that I'm using Lisp. The end product may have nothing to
>do with Lisp.

By the by, you generally shouldn't post huge source samples like
that. A snippet that fits on a single screen (24 lines) is probably
a reasonable rule-of-thumb limit.

Now, gentle readers, first I'm going to offer some language-design
language-thinking criticism (which, hey, I may be wrong about, we're
talking about something fairly wacky here); and then after that pedantry
I'll offer my opinion about the relevance of this idea to IF.

>It's the idea of multiple dispatch that I'm testing.
>It sounds like what I need now is a genuine proof of a real problem,
>like re-implementing Alice, to show whether the system actually offers
>any real power over current methods.

It's an interesting idea that I considered and rejected when I
was designing my MUD development system. That system used a C-like
syntax for message dispatch (compare Perl) where
message(obj, p1, p2, p3)
was a message to 'obj'. This is STILL object-oriented code; and
one could argue that a language where
message(p0, obj, p2, p3)
got dispatched to 'obj' was also object-oriented.

>One thing that multiple dispatch does allow you to do is to enrich
>base types without modifying them.

[snip]


>But what if you want to specialize "cut" on the base object, relative
>to swords? It would have to be done like this:

There's really no reason why
message(obj1, obj2, p2, p3)
(where that's a multi-dispatch) couldn't be considered object-oriented;
for example, the method might live on either obj1 or obj2 but use some
kind of type overloading to resolve.

No matter how you implement multiple dispatch, you really do have to
view it as "modifying" the base type in some sense.

>Yet this pretty much erases the abstractions gained from OO, by
>creating a system in which every object may or may not need to know
>about the type of every other object.

This is the nature of the *problem domain*, not the nature of OO.
IF already veers wildly from traditional OO by defining lots of
unique objects with custom code. (You can argue they're singletons,
but if you compare what's going on in practice with what singletons
are traditionall for, you'll see the difference--the IF objects
are all of a common 'simulation type', just with different behaviors.)

So either you consider IF-OO to be "real OO" or not; I don't think
multi-dispatch really changes that.

> defmethod sharpen sword object
> Sharpening the sword, using object.
>
> defmethod cut object sword
> Cutting an object, using the sword.
>
>Now, no _object_ knows about any other. It is the associations that
>know everything about their possible conjunctions.

To put what I was saying above more concretely: we tend to think of
methods and living on objects (or their classes), but this is an
implementation detail; if the above is an example of code that's
not "on" any particular class/object, then

defmethod take object
Picking up an object

wouldn't need to be on any particular class/object either. We
just traditionally consider it that way, since OO isn't traditionally
multi-dispatch.

Ok, enough pedantry.

Now, as to IF:

I don't think this is interesting because of the following issues:
1. N-way interactions are rare due to simulationism
2. rules for selecting best method are human-incomprehensible
3. N-way dispatch can be faked with simple obvious code that
is no less well-structured
4. N-way dispatch syntactic sugar removes the ability to apply some
programmatic resolution methods

I'm impatient with raif right now so I'm going to write these only briefly.

1. Simulationism

On the simulationist mud I worked on, we had a combat system.
HIT NPC WITH SWORD, right? The following objects wanted to contribute
to the outcome of the attack:
the attacker's skill-system object (attack skill)
the attacker's body object (strength, etc.)
the attacker's weapon (the SWORD)
the defender's shield[s] (including magic shields)
the defender's skill-system object (defending skill)
the defender's body object (e.g. dexterity, encumberance state)

The solution to this is not to define a 6-way multiway dispatch,
where ONE rule wins and determines the outcome, since how can that
reasonably cope with both a magic sword and a magic shield and
the player being really skillful? The solution is to write a
single "combat formula" which is some code with lots of hooks that
call into the other objects in a traditional single-dispatch fashion
and allow them to override it in various ways.

You can see the same things going on in IF for "CUT OBJECT WITH SWORD",
involving sophisticated multi-stage resolution systems where different
objects (including objects not even mentioned in the command, e.g. with
Inform's react_before) are allowed to participate. Platypus and Tads3
are the front-runners in sophistication here, I believe.

2. Incomprehensible rules

What happens if I

defmethod cut object sword
(blah blah blah)
defmethod cut gem object
(blah blah blah)

How do we decide which of these runs? If the defmethod formal
arguments can be arbitrary types from an OO tree (e.g. weapon,
etc.), then the way to resolve it is rather unclear. Existing
systems use a formal staged multi-step priority scheme IN CODE,
see above, so it's fully configurable with all the power of the
language.

If you don't use code, I think you'll end up with something like
C++ function overloading rules, which involve a bizarre multiway
match that few humans can correctly predict.

3. Can fake it equally structured

The "floating associations" you describe above, not part of
any object, are just as poorly structured; since they're not
associated with either object, how do I (as a coder/maintainer)
know about them? Where do I go to find them?

Current practice puts these on one object or the other, with an
if to test for the other object. That seems to me to be GOOD;
if there's a seeming custom interaction between X and Y, at least
I know it'll be found somewhere in either X's code or Y's code;
with multi-dispatch associations, I'd have a third place to look,
the pool of floating multi-dispatch associations.

4. programmatic solutions useful

In addition to the action-verification schemes I mentioned before,
Inform happens to be uniquely powerful at addressing these things,
IN A HUGELY KLUDGED FASHION admittedly, but I think any multiway
dispatch will be kludged.

Inform deals with action verification using an "implicit swich
on action" in "before" and "after" routines. In languages like
Tads, each of these before-verb actions becomes it's own routine.
I'm sure Tads has some mechanism to do something similar, but this
means in Inform you can do stuff like:

before [;
Insert, PutOn:
if (second == hollow_banana)
"Look, the same multi-way dispatch for TWO methods,
incredibly concise.";
],

which can get even more complicated with things like

react_before [;
... a bunch of common code that's run for ALL verbs ...
Insert, PutOn:
... common code for all of this verb ...
if (noun == banana && second == altar) {
... some special case code ...
if (action == #Insert) {
... some special case for only one verb ...
}
... more special case for both verbs again ...
}
],

Etc. etc. etc.

Finally, like DanS said, I just don't think it comes up often
enough to be worth it.

SeanB

0 new messages