Interfaces are an OOP concept that BYOND has no notion of. If you're unaware of what an interface is, there are plenty of Google-able resources on the subject (Or for the lazy, see here).
The problem that interfaces are often there to solve, is this one:
What if you have a number of classes which carry no resemblance to eachother, but a different class wants to use any one of them in the same fashion.
For a specific example, in my game I have a /menu object and a /form object. A menu can link to another /menu, but it can also link to a /form. And yet, the /form has absolutely nothing to do with a /menu, and can exist just fine on its own. The /form is simply a series of questions the user is asked, where-as a menu is a static object that allows the user to walk through the menu, its children, and so on.
So how do you solve a dilemma like this? As with most things within programming, there are numerous methods to achieve the above. Some of which are pretty sketchy and not very robust, some of which are.
Lets simplify things and say we have the following scenario:
apple
proc/describe()
world << "Apples are yummy!"
car
proc/describe()
world << "Car goes wrooom!"
describe_item
var/datum/D
proc/setItem(datum/D)
src.D = D
proc/Go()
D.describe() // Uh oh! We can't do this
Ignore for the sake of this example that the above could use a variable to describe the object - The example is kept simple in order to be easier to grasp.
The above obviously isn't possible, as /datum has no describe() procedure. And if we don't want /apple and /car to descend from the same parent, we've got an issue we need to solve.
The smart reader might go, 'You can use istype() for this!', and they'd be right. But lets see how that looks, for a moment:
apple
proc/describe()
world << "Apples are yummy!"
car
proc/describe()
world << "Car goes wrooom!"
describe_item
var/datum/D
proc/setItem(datum/D)
if(istype(D, /apple) || istype(D, /car))
src.D = D
proc/Go()
if(istype(D, /apple))
var/apple/A = D
A.describe()
else if(istype(D, /car))
var/car/C = D
C.describe()
Woohoo! Home safe! Except.. the above really looks like we've duplicated a lot of code. And what if we want to add another object? More istype()'s. If we want to remove an object, we have to adjust both procedures too.
The smart reader might now go, 'You can use the : operator, and write a separate procedure to check the type of the datum!', and they'd be correct again. And this IS one solution. However, it still requires you to maintain the procedure doing the type-checking, and still feels pretty ugly to me.
I have a different suggestion: Emulate the spirit of interfaces, and joy will be had by all.
DM has a call()() procedure, which allows you to call a proc, or a proc belonging to an object. We could wrap this functionality in an object and maintain a list of legal functions to call. This way we get around having to check types, instead we can check whether the function we want exists with hascall() - And this can be done dynamically and all nice.
For example:
datum/proc/publicFunctions()
return new/list()
callWrapper
var
datum/__obj
list/__func=new()
New(datum/__obj, list/__func)
if(!istype(__obj)) del src
if(!istype(__func))
__func = __obj.publicFunctions()
for(var/a in __func)
if(!hascall(__obj, a))
__func -= a
if(!length(__func)) del src // No functions to call
src.__obj = __obj
src.__func = __func
proc
Call(f)
if(!f || !(f in __func)) return
if(length(args) > 1)
var/list/L=new()
L = args.Copy(2)
call(__obj, f)(arglist(L))
else
call(__obj, f)()
So that was a lot of code. What does it do?
The idea behind the callWrapper object is more or less what it sounds like. It wraps calls in an object. You supply it with an object, and optionally a list of functions that can be called, and it does the rest. In order to actually call that function, you use callWrapper.Call(function_name).
The callWrapper object will make sure that any function specified in the __func list in New() actually can be called, and that if there are no 'legal' functions left, or you didn't supply a datum as __obj, it'll delete itself.
Lets try to apply the callWrapper object to our initial case:
apple
proc/describe()
world << "Apples are yummy!"
car
publicFunctions()
return new/list("describe")
proc/describe()
world << "Car goes wrooom!"
describe_item
var/callWrapper/W
proc/setItem(callWrapper/W)
src.W = W
proc/Go()
W.Call("describe")
var/apple/A = new()
var/car/C = new()
var/describe_item/describe_apple = new()
var/describe_item/describe_car = new()
describe_apple.setItem(new/callWrapper(A,list("describe"))
describe_car.setItem(new/callWrapper(C))
The good thing about this, is that if you wanted to be able to describe trees, or a menu, or some internal object, or something else totally unrelated to all the rest, all you'd have to do was give it a describe() procedure. The above also shows the two different ways to deal with the callWrapper object - By overwriting publicFunctions(), or passing any functions that you want to call as a list to its constructor.
This is in fact closer to automatically inherited interfaces, which Google Go implements. And automatically inherited interfaces are awesome, for a variety of different reasons.
This callWrapper is also great any time you want to export some functionality in a library, and you want the API to be very simple. Lets say you have an object and you want to let the user define how it is displayed in a chat panel. You might in that objects constructor give the user the ability to pass it a callWrapper, and then in the descriptive function for the object, you'd use the callWrapper if it existed. That might look like this:
menu
var/name = "Some Menu"
proc/describeMenu()
if(__wrapper) return __wrapper.Call("describeMenu")
else return __defaultDescribe()
proc/__defaultDescribe()
return src.name
I may release a small library with this functionality (and some extra functionality), if I can convince myself its not just a small snippet but actually a library ;)
Beyond what is discussed in this article, the library provides functionality where it automagically figures out what procedures a given object type has. It will filter out functions considered private (By default, any proc that is prefixed by __), allowing you to use this to handily wrap API as well. Its a bit of a gimmick compared to an actual 'private' keyword, but its better than nothing.
As the demo.dm file shows, there is very little overhead in all of this. I haven't been able to run this on Windows yet, so I don't have any actual DS profiler stats - I'd appreciate if someone else could provide a profile of demo.dm, specifically the 10000 call test.
The library is currently in a state where the API can potentially change with updates, nothing has been locked yet. As things are tested and the library is used, I will begin locking features.
Remember! If you like DevTalk articles, follow us :)