(Anti)Object-oriented reasoning

Posted on by Chris Warburton

Brain dump:

Object-oriented programming is a style of writing instructions for computers which was invented to make comprehending these instructions easier. Using a list of things to do, like a cooking recipe, works well for short tasks, but with a computer it is required to provide every step of every instruction. Imagine a recipe that has a step “break two eggs into a bowl, keeping the yolks separate”. This is a “high-level” instruction, since it doesn’t tell us what muscles to move, what senses to pay attention to, etc. Making high-level instructions can be done by declaring “functions”, such as “break_eggs_into_bowl(egg_number, separate_yolks)”, then specify in this function exactly how to do this. Once the function is declared we can simply say “break_eggs_into_bowl(2, true)” in our recipe, rather than having to specify them all over again. Of course, we can use functions inside functions, so we could even make the entire recipe a function like “make_cake(flavour)” which includes running the function “break_eggs_into_bowl(2, true)” as well as many others. The problem then becomes how to keep track of all these functions.

Using “libraries” is one way of doing this; you put everything to do with images in an “image library” and everything to do with music in a “music library”, and so on, then when you want to write a program which handles images you just say ‘I’m going to use the image library’ (eg. in C this would look something like “#include” and Python would look something like “import images”). The problem with libraries is that, since they’re meant to include (“encapsulate”) everything to do with their subject (so that nobody using it has to care what it does, only what it’s functions are called) by dragging a library into a program you end up “polluting the namespace”.

A “namespace” is the embodiment of everything a program knows about at a certain point in its execution. Before we declare the “break_eggs_into_bowl” function it is not in our namespace, by declaring it we put it in the namespace so that later on we can use its name and the computer will know what we’re talking about. The problem with libraries is that by asking to include them in our program, the namespace of our program gets filled with everything from the library. That might be good in that we can include the image library then say “display(my_image)”, but we have to be very careful that names don’t get overwritten. For example we might want to make a spreadsheet that uses a graph library and an image library, but they might both declare a “display” function. It would not be obvious to us that our command “display(my_image)” is broken since it’s actually trying to draw a graph because both functions have the same name. That’s why we use more than one namespace in our programs, so that a command “import image” in Python won’t make everything from the image library directly accessible, instead it makes the namespace of the image library accessible so that we can say “image.display(my_image)” and, if we like, “graph.display(my_graph)”. That makes it much more obvious to us which one we’re using, and the only names that get put in our namespace are those of the libraries, which we’ve explicitly asked for. There are no hidden surprises caused by unknown things overwriting parts of our program.

An extension of this is to use Objects. An object is like a library, in that it encapsulates everything about a given topic, but objects make it easier to understand our programs since we can treat them as the actual things themselves. Rather than saying “import image” then “image.display(my_picture)” we can say “my_picture.display()”, ie. we send a message to “my_picture” asking it to display itself. The reason this makes understanding code easier is that we make the assumption that whoever made the object has done it sensibly (the same assumption we must make about libraries in fact), so that when we ask our objects what to do, they react in intuitive ways. Another nice thing about objects is that they know all about themselves, so whereas before we had “graph.display(my_graph)” and “image.display(my_image)”, if the graph and the image are objects then we can say “my_graph.display()” and “my_image.display()” and they both know what to do, even though we’re asking them the same thing. In fact, we can just say “something.display()” and it will work whether “something” is a graph or an image, we don’t have to care. Later on we can make a Web page object and tell that what to do when asked to “display” and voila our “something.display()” now works for Web pages too.

Object oriented programming allows us to write programs as a bunch of interacting objects, a lot like the real world is. Since we can handle the real world pretty well, this allows us to transfer those skills to our programming. However, OO programming is not just limited to creating models of the real world. For example, the use of “anti objects” can make programs much easier to write and understand, even though they go against what we know of the world. Anti-objects are objects in a program which don’t represent anything that exists, but since we’re making a computer program there’s nothing to stop us making up such things and then making them do stuff for us. The classic example is the game Pacman. If you want to write a Pacman game, the hardest part is trying to make the ghosts intelligent. They have to chase Pacman (ie. head towards Pacman), but they also have to work around the walls of the maze, it’s no good just homing in on Pacman if there’s a wall in the way. By using the level as an anti-object we can make the job of the ghosts much easier: rather than having intelligent ghosts that try to think of the best way to get Pacman, we instead make each part of the level contain an amount of “pacman-ness”. Wherever Pacman is has 100% pacman-ness, every wall has 0% pacman-ness and the pacman-ness of every other square in the level is the average of its neighbours. This makes the ‘pacman-ness’ diffuse through the level like a ‘smell’, but since it doesn’t go through the walls the pacman-ness of each square becomes a good indicator of the distance from that square to pacman, following the maze. Now the ghosts can be really dumb, since they just have to choose their next move based on whichever direction has more pacman-ness and it will always be the best move. In fact, if the ghosts are made to act like walls (ie. they don’t let pacman-ness through) then once a ghost has blocked a path leading to pacman, it will also block the pacman-ness smell. The other ghosts will thus find alternative open routes to pacman and the player’s escape routes will be blocked off. This guarantees that the ghosts will beat pacman if the number of pacman’s escape routes is less than or equal to the number of ghosts. I wrote such a pacman game in an afternoon, but unfortunately it’s on my laptop which is bust at the moment so I can’t share it. Still, it was an interesting approach to the problem, even if the resulting game became too hard to ever beat even with 1 ghost :P

So, we can encapsulate domain-specific knowledge inside objects, we can ask many different kinds of objects the same thing and they can interpret it in their own specific way and we can invent objects that don’t necessarily exist in the system we’re modelling, and transfer our logic to them. How might we use this to make an AI?

Imagine that we generalise the pacman example: rather than ghosts, we have a more general concept of agents and rather than pacman we have a more general concept of a goal, of which there can be many. The space, rather than being a maze which we traverse with movement, becomes a more general space of every imaginable scenario which we traverse by taking actions (a generalisation of movement). In this space we have objects for everything we know about, rather than just pacman. Each type of object has a ‘smell’ in each scenario, which means “how likely are you?”; ie. given that scenario, what is the chance that an agent can obtain or access you? The objects can update their own likelihoods if they encapsulate enough domain-specific knowledge of themselves, which can be learned over time. The agents then just decide which goal/goals they want and take the action which leads to a scenario where the goal is more likely. The only major complication is that the scenario space isn’t fixed: every object may add contributions, but considering that the probabilities will be multiplied (eg. the probability of having an Internet connection via a computer is the probability of having a computer * the probability of it having an Internet connection), thus they fall off rapidly and simple heuristics can prune the scenario space quite quickly.

In such a system, our reasoning does not have to depend on what we know, since every object looks after itself. Thus a separate program can handle the creation, specialisation and connections between objects (for example through a hierarchy of Bernoulli trials performed on the sensory input). A problem in AI is often in choosing what to do. Given our probability system for sub-goals which extends to whatever granularity we like, we only need one top-level goal, which I propose is “Maximise your freedom”. This sounds very philosophical, but essentially it means that, without being given anything else to do, the program would try to improve its position in the world so that it can do more things in the future. This could involve performing tasks for payment, since the possession of money increases what it is able to do. It could of course rob a bank to obtain money, but there is a large probability of being caught and deactivated if that course of action is taken, and being deactivated stops the program from doing anything, and is thus avoided (ie. self-preservation). By maximising its freedom, a program will be more likely to be in a good position for doing tasks asked of it in the future.

This is just a vague, hand-wavey brain dump, but would probably be interesting to build…