Newsgroups: comp.lang.lisp
Path: cantaloupe.srv.cs.cmu.edu!rochester!cornellcs!newsstand.cit.cornell.edu!portc01.blue.aol.com!news-peer.gsl.net!news.gsl.net!howland.erols.net!netcom.com!wnewman
From: wnewman@netcom.com (Bill Newman)
Subject: Re: How to do this in Lisp?
Message-ID: <wnewmanDz17Ds.45J@netcom.com>
Organization: NETCOM On-line Communication Services (408 261-4700 guest)
X-Newsreader: TIN [version 1.2 PL1]
References: <wnewmanDyzL1p.BFJ@netcom.com> <x54tk4ti4n.fsf@rsi.jhuapl.edu>
Date: Wed, 9 Oct 1996 23:17:52 GMT
Lines: 181
Sender: wnewman@netcom10.netcom.com

I started this thread by writing about a programming approach which I
might like to port to Lisp.  (a way of implementing reversible search
steps in a program to play the game of Go)
I have received several responses.

Another article in this thread suggests that I look into languages
like Prolog (possibly implemented in Lisp) and other programs with
backtracking support.  That sounds interesting, and I will look into it.
For now, I don't understand it well enough to comment on it.

Marty Hall's article, and some e-mail that I received, can't really
be responded to without clarifying what I was talking about.  I will
apologize in advance for polluting the hallowed halls of c.l.l with
icky C++ source code..

Marty Hall (hall@apl.jhu.edu) wrote:
: wnewman@netcom.com (Bill Newman) writes:
: > I have a program to play the game of Go.  

: Are you willing to share it? I've never seen any that play above the
: beginner level, although I've never tried the commercial ones from
: Ishi Press.

There are lots of neat ideas floating around on the computer
go mailing list.  I don't have the address handy, but I'll look it
up if someone sends me mail.  Someone on that list is planning to 
release a new program, but I don't know how good it is.

As for my current program, I have dreams of licensing something like
this program for money someday, so I am not currently sharing it,
sorry.  I did write the C version of one of those simple public-domain
beginner-level programs you have seen ("wally").  The current program
is much fancier, but key parts (e.g. the approximate score function)
are so crudely implemented that it doesn't play much better than
wally does.

: > Basically, I have a function which is a
: > crude approximation to the goodness of the position, and the
: > approximation gets better as I search deeper and minimax the 
: > results.  

: I'm probably speaking to an expert here, but just in case, you're
: using alpha-beta pruning, not a brute-force minimax, right? And given
: the humongous branching factor, I take it you aren't searching
: full-ply?

I'm still trying to decide what search strategy to use.  Howeer, the
details don't much matter for the purposes of the discussion here,
which is only concerned with reversibility.  For the sake of argument,
consider alpha-beta.

: > Instead, I modify the board in place and update only those score
: > terms which are changed.
: > 
: > Now, if I do this when I am searching down various branches of the
: > tree, what do I do when I come back up the tree?  In C++, I define the
: > variables which change in the search to be of class Remembered<FOO>,
: > e.g. Remembered<int>, and I overload the assignment operator so that
: > it remembers the old value whenever the object is modified, and puts
: > it into a stack of changes to be undone.  When I come back up the
: > tree, I unwind the stack and undo the changes.  This works cleanly and
: > reasonably efficiently even when the Remembered<FOO> objects are used
: > as data members of other classes.

: I'm not positive I follow the problem. I'd imagine you'd do something
: like

: (Make-Move Board <args>)
: (setq Expected-Value (Alpha-Beta Board <more args>))
: (Unmake-Move Board <args>)
: (Do-Some-Pruning-With Expected-Value)

Yes, this is basically what I would do.  (It would probably
actually be
  (with-move move (do-something))
but the idea is the same.)

: You're saying that this presents difficulties?

The problem is not at this level but in the implementation of 
MAKE-MOVE and UNMAKE-MOVE.  In C++ I can write stuff like this..

/* mixin class for ephemeral objects, i.e. objects which are automatically
   deleted by undoing a move */
class Eph { 
protected:
  Eph() : next(s_list_head) { s_list_head = this; }
  virtual ~Eph();
private:
  Eph* const next;
  static Eph* s_list_head;	// list of objects to be deleted by $~Event()$
};

/* A $Memory<T>$ object is used to remember the state of some $T$ object before
   an ephemeral change, so that the ephemeral change can be undone by
   undoing a move. */
template<class T> class Memory : public Eph {
private: 
  const T v_what; 
  T* const v_where;
  STATIC_CLASS_STATS;
  /* Copy ctor and assignment operator don't exist. */
  Memory(const Memory&); 
  const Memory& operator=(const Memory&);
public: 
  Memory(T& x) : v_what(x), v_where(&x) {} // !! Can I do this in Lisp?
  ~Memory();
};

/* A $Remembered<T>$ object is a magic variable apparently of type $T$,
   whose old value is automatically remembered in an $Memory<T>$ (for
   later restoration when a move is undone). */
template<class T> class Remembered {
private:
  T v;
public:
  Remembered(); // sensible/desirable default value depends on $T$
  Remembered(T v_arg) : v(v_arg) {}
  Remembered(Remembered& that) : v(that) {}
  ~Remembered() {}
  operator T() const { return v; }
  void set_without_remembering(T v_arg) { v = v_arg; }
  void set_without_remembering(Remembered that) { v = T(that); }
  void operator=(T v_arg) {
    new Memory(*this);
    *this = v_arg;
  }
  void operator=(Remembered that) { operator=(T(that)); }
};

In attempting to express this in Lisp, the problem appears in the
implementation of Memory.  Basically, C++ allows me to create an
object which stores a reference to an object, and a copy of its
previous state, so that I can ask it to restore the object whenever I
choose.  (In my program, the restoration request is issued by
destroying the Memory object.)  I can't figure out how to create such
a reference in Lisp.  It may be that this can't be done, at least when
the object is a component of another object: I think I remember
someone slamming C/C++ for allowing references to components within
objects in one of the Garbage Collection Wars earlier this year.

When I use the code above, if I write e.g.
  class Square_Of_The_Board {
    Remembered<Color> v_current_color;
    Remembered<Square_Of_The_Board*> v_next_in_chain;
  };
then whatever changes I make to the v_current_color and v_next_in_chain
fields can automatically be undone by calling Unmake_Move.  Unmake_Move
walks the linked list descending from Eph::s_list_head calling the
dtor for each element, and the changes are undone.

I received e-mail suggesting -- if I understood correctly -- that I
can't overload assignment in Lisp, but the same effect could be
achieved by defining my own (REVERSIBLE-ASSIGN VAR VALUE) function or
macro in Lisp.  I can't figure out how to do even this.  Also, even if I
could, I'd still prefer to have the reversible-ness associated with
each variable, rather than with each assignment to each variable.  (I
used to do it the other way around in C before I switched to C++, and
it was a major maintenance hassle tracking down things which
accidentally changed something irreversibly.)

: Another alternative is to simply create a resource pool of boards at
: the beginning, and copy onto that board but don't reallocate the
: space. Ie suppose that you generate the board positions on the fly and
: search to depth 4. Then you only need 4 boards. You still have to copy
: the previous board positions onto the "new" board at each level, but
: not allocate the new array. So you won't cons. If, however, you are
: searching deep enough to make it worthwhile to apply your static
: evaluation function at earlier levels (so you can order the moves to
: get an increased number of alpha-beta cutoffs), then you'll need a lot
: more boards in the resource pool. Ie if you search to depth 4 and you
: consider 40 moves at each level, you'll need 160 boards. But you can
: still allocate them in advance and then reuse them.

I think the copying is probably too costly even without garbage
collection and reallocation.  (I haven't checked, but I do have timing
information comparing exhaustive calculation, which I use for
debugging purposes, to the incremental recalculation used for actual
analysis.)

  Bill Newman
