Programming benefits of Whisker's design

Top  Previous  Next

Keeping it simple


The main operant control system used by our lab at the time Whisker was first written was Arachnid (Paul Fray Ltd / Cenes, Cambridge). This is an extension of BBC BASIC. In designing Whisker, I [RNC] was particularly keen to avoid the sources of programming error that were common in Arachnid programs. Among the most common problems are typing errors, misunderstandings (often due to illegible code), inappropriate use of global variables, failure to design tasks carefully, and cross-talk between boxes.


Some of these problems are due to the choice of language. Replacing an interpreted language like BBC BASIC with a compiled, strongly type-checked language like C++ vastly reduces the number of typing errors that find their way into programs – the compiler picks them up, so you can guarantee that a program that compiles successfully is at least syntactically correct. Logical errors can still occur, but C++ encourages you to write small, easily testable chunks of code that hide information from each other, so many logical problems (including over-reliance on global variables) disappear.


On the other hand, some programming problems we experienced with Arachnid were due to weaknesses in the design of the system. When a program must control six operant boxes, the potential for errors increases; it is the programmer's responsibility to make sure that every switch and timer event is tagged to the correct box, to create a system whereby different boxes can run asynchronously, and so on.


Whisker follows the Unix philosophy instead: rather than one huge program, create lots of small ones. If one program only controls one box, errors of cross-talk are simply not possible.


Most of the programming examples I have supplied only control a single box. The exception is the SecondOrder task, which can control other boxes for the purpose of yoking. In this case, all the potential for cross-talk exists. The program calls its events things like Yoke_4_Active_Lever, and passes the Active_Lever part of this message on to a data structure representing the fourth yoked box. (SecondOrder follows the C++ principle of representing each operant chamber as an independent object that stores its own data.) Theoretically, it would have been possible to implement yoking by running two kinds of clients simultaneously, one controlling all the boxes using Whisker's line-grouping facility (discussed earlier, in Part V) and the other reading inputs from the yoked boxes, but then there's a problem of synchronizing the two programs; it would really have been impractical.


Naming your events


You could name your events after Russian films, but that would be silly. More useful would be to define events like NOSEPOKE_ON, which is what I tend to do. But there's no reason why you can't change the event name during the task to make it simpler to process events – for example, you could set things up so that the nosepoke detector generates INITIATE_TRIAL sometimes and REWARD_COLLECTED at others.


Events: a comparison to Arachnid


For those of you that are familiar with Arachnid, a direct comparison may be helpful. In Arachnid, you execute PROCpipe_switch() and PROCpipe_timer() calls, passing the name of a BASIC function of your own. When the event occurs, Arachnid calls your function.


Whisker gives you control at one level higher up. You can receive all the messages coming from the server, and decide what to do with them – which might involve calling other functions, just as Arachnid would have handled it, but it doesn't have to. It's a more powerful system and it makes for simpler code in some situations.


A word about ClearEvent commands: these commands stop any more messages being sent from the Server – but it doesn't send back any that are 'on the way'. This is slightly different to Arachnid, when a event could never be detected after it had been 'Killed'. This is especially important when we have to deal with touchscreens, which might send a lot of messages quite quickly, or when we're doing slow processing in the client. If you have to only respond to whichever of two messages comes in first, make sure the code can never respond to both: don't just assume that 'it'll never happen'.


Technical note


Of course, it is nice at times to be able to set up a complex sequence of events and have it happen without further interference; the Whisker C++ client library provides this kind of functionality. One can, for example, ask for a message to be sent to the server after a specified delay, and the library can do this for you. (Internally, it asks the server for a timer of its own, whose name begins with _sys, and it remembers what you wanted done when that timer elapses. The point is that this detail is hidden from you.) Visual Basic provides similar functionality by supporting re-entrant functions via the DoEvents command.