Interfaces in DM

Non-Robust programming for those who don't like the : opperator.

Contents


Introducing Interfaces

There are many times when it's helpful to be able to group together different types of objects based on something they have in common. Consider that the world is composed of objects derived from three basic ancestors: Circles, Squares, and Triangles. Now consider that in this world there is someone who collects blue objects; though all objects in fact derive from one of the ancestors, he finds it practicle to treat them as though they derive from the ancestors Blue, Yellow, and Grey (he just happens to be red-green color blind).

What the collector has done is created an 'interface', an assumption he makes about the nature of objects which have blue in common. Let's use another example, the handle interface. Objects which use handles will be said to "implement" the handle interface. Any sort of object can have a handle, some examples include doors, refrigerators, cabinets, coffee mugs, and briefcases. Though all of these objects derive from different types, we can still pull on them and know that something will happen. In most of the cases mentioned above, if you pull on the handle a door of some sort will open (or close); in the case of the coffee mug and briefcase, however, pulling on the handle just allows you to carry the item. What is important is that anything implementing handle can be pulled on.

In the previous example, handle was our interface, and "pull" is the method all objects implementing handle can be assumed to have, even though they all derive from different types. An example of what this might look like in a DM-esque language follows:

handle{
    // Interface
    proc/pull(){}
    }

refridgerator implements handle{
    proc/pull(){
        open_the_door()
        }
    }
coffee_mug implements handle{
    proc/pull(){
        carry_the_mug()
        }
    }
			

An important thing to notice in the above example is that the handle interface doesn't actually define what pull() does, it just lists it. Each object which implements handle must define the specifics of how pull() is executed. Even though the interface doesn't define the pull() proc itself, it still allows the program some nifty liberties, the one I'll be focusing on is how both refridgerator and coffee_mug can now be treated the same way by the compiler without it returning an "undefined proc" error.

It should be obvious that the code above will not compile in DM (or any future version of DM, thank goodness). However, there is a hackish work-around to achieve the same feel of "multiple inheritence". Now it's time to really bend DM to our will.

Top

Simulating Interfaces in DM

Simulating Interfaces in DM is a straight forward hack using the parent_type variable, a variable anyone familiar with the arcana should be well versed in. First we will define a new node which will act as our interface. Put the following in a new file called "interactable.dm"

interactable{
    // Interface
    var/unit/last_actor
    proc/interact(unit/who){}
    }
			

Now we decide on the basic types of objects which will be interactable; this list can be expanded at any time in the future. For this example we'll assume that there are three types of objects which will implement interactable: NPC (inherites from /unit), Tile (inherites from /turf), and Furniture (inherites from /scenary). These are defined in their own files, but the secret to the interface is that we pull their line of inheritence through our interface node, so we'll also define these types under interactable:

interactable{
    // Interface
    var/unit/last_actor
    proc/interact(unit/who){}
    }
interactable{
    // Types
    npc{
        parent_type = /unit
        var/unit/last_actor
        proc/interact(unit/who){}
        }
    tile{
        parent_type = /turf
        var/unit/last_actor
        proc/interact(unit/who){}
        }
    furniture{
        parent_type = /scenary
        var/unit/last_actor
        proc/interact(unit/who){}
        }
    }
			

Now that those nodes exist under interactable we can refer back to them in our definitions of NPC, Tile, and Furniture:

// In NPC.dm
npc{
    parent_type = /interactable/npc
    interact(unit/who){
        mindless_npc_talk(who)
        }
    }

// In tile.dm
tile{
    parent_type = /interactable/tile
    interact(unit/who){
        if(trap_in_src()){
            attempt_disarm_trap(who)
            }
        }
    }

// In furniture.dm
furniture{
    parent_type = /interactable/furniture
    interact(unit/who){
        do_stuff()
        last_actor = who
        inform(who, "You left fingerprint on [src].")
        //Had to use the last_actor variable somewhere, eh?
        }
    }
			

So what is the benefit to all this? We can now refer to anything derived from any of those three types as though it were it's type or as though it derived from interactable. So we can now do something like the following:

// In unit.dm
unit{
    parent_type = /mob
    proc/act(){
        var/tile/T = get_step(src,dir)
        // Assumed: there are no turfs that are not tiles
        ASSERT(istype(T,/tile))
        var/interactable/target
        target = (locate(/npc) in T)
        if(!target){
            target = (locate(/furniture) in T)
            }
        if(!target){
            target = T
            }
        target.interact(src)
        }
    }
			

Top

Shortcommings

A 'simulated' interface in DM is good for basically one thing: You can refer to variables and procs from the interface, regardless of the actual type of the object. Apart from this, the benefits are few and the work involved in setting up and maintaining such a convoluted inheritence tree usually outweigh the gains.

The interface is also notable for what you'd think it should be able to do, but really can't:

All of the above shortcommings are really just symptoms of one problem, which in turn is the very nature of this hack. Though we can safely (if we're careful about how we implement it) use the interface type to refer to objects which implement it, those objects do not technically derive from the interface; they are not of the same type.

All told, it is a useful technique if and only if:

As with everything in the arcana, proceed at your own risk.

Top


IainPeregrine