Friday, March 15, 2013

Ada access types, part III

Thus far we've seen how accessibility rules "imprison" a local pointer at the scope in which it was declared, but how access parameters and access discriminants provide a safe mechanism for passing them around when needed.  But there is trouble in paradise, and it involves a brief side trip into a seemingly unrelated topic: OOP.

(It should be noted that this post is mostly my own opinions, moreso than usual.  I was obviously not there when they designed the language, nor do I know all the reasons for the design that was chosen.  In fact, much of the 'why' below is different than the 'official why', so please make your own judgements and take everything with a grain of salt.  Or ask your friendly neighborhood ARG member.)

Aside from general access types (and many other things), Ada95 also added support for so-called "object-oriented programming".  I can't possibly go into all the details, suffice to say that it is a programming model that relies on dynamic dispatching to select the correct subprogram at runtime, thus making programs much less brittle.

For example, suppose we want to create an abstract "file stream" parent, that allows us to read a single byte from a file.  This will be implemented by two child types, Normal_File and ZIP_File, where the former will simply read bytes from disk as expected, but the latter will do the messy work of decompressing the bytes 'on the fly'.  The first one will likely be quite simple (and just ferry calls to whatever the underlying disk is), whereas the second will be much more complicated, involving all manner of caching, maintaining file pointers, and so on.  We might establish our types like so:

type File is abstract tagged null record.
function Read (This : in out File) return Byte;

But right off the bat, we find an issue: functions cannot have out parameters.  It's an almost certainty that the ZIP_File will need to modify its own state during a read, since it will likely have to decompress a large chunk of data, and then return each byte from inside the cache.  But by being restricted to 'in' parameters, this is a non-starter.

Before Ada2012 solved the problem once and for all, there were two main workarounds to this problem.  The first, which was so widely used that it still exists in almost all Ada code, is to replace the function with a procedure and use an 'out' parameter, like so:

procedure Read(This : in out File, That : out Byte);

Of course this is not a general solution, since there are cases where only a return value will do (unconstrained arrays, discriminated records, etc).  The other method is, of course, to use an access value.  The only requirement is that the parameter itself not be updated, so we can modify anything we wish simply by adding an '.all'.  That means our function now looks like so:

function Read (This : File_Ptr) return Byte;

But while this is a step forward, it's also two steps back.  Remember that dynamic dispatching only happens for object types, not access types.  Read is no longer a dispatching primitive operation, which was the whole point to begin with!

And you can't call your language "object oriented" if, in a practical sense, you can only have dispatching on your procedures.  You can't just add named access values to the list of primitive operations either, since it would be a nightmare deciphering what overrides what (not to mention restrict you from using those fancy new general access types).  So as a kludge to the kludge, access parameters were added to the list of primitive operations, since they have the nice property of being able to accept any named type.  They said this was a "convenience" to avoid having to write '.all' at every place, since OOP is pointer heavy, but I don't buy that.  This was just a way to save face and avoid admitting fault vis a vis function out parameters.  But for whatever the reason, dispatching could now occur for access types, so long as the function was declared with an access parameter:

function Read (This : access File) return Byte;

Of course, this has nothing to do with the original intent of the access parameter (for accessibility control), and everything to do with getting yourself an 'out' parameter for a function.  If you peruse some Ada95-style "OOP" frameworks (e.g. GtkAda), you will find a rash of access parameters used in this manner.  Again, it's about dispatching, not accessibility.

But again, this created more problems than it solved.  Access parameters allowed you to get the needed dispatching, but you were also forced to take an anonymous access type.  Remember, all the same accessibility rules from the previous post still apply, whether you want them or not.  That means within the function body, there is no copying of the 'This' pointer to anywhere, or passing it to other subprograms.  It's imprisoned just the same, like it or not.

This puts you in a bind if you actually want to copy the pointer (and obviously do your work on the heap to avoid dangling references).  Suppose some fancy version of the Read subprogram is done so that if a certain byte-sequence is read, it inserts itself onto a linked list somewhere, or registers itself as an observer, or some other pattern that involves saving an access value.  Now you are stuck, since your Faustian bargain signed away any possibility of copying the object to gain dispatching ability.

So, as a kludge to the kludge to the kludge, Ada95 put in a loophole so that you could "get your type back" after being forced to cast it aside.for dispatching: access parameters can be typecasted.  Doing so, however, incurs a runtime check that raises an exception if the accessibility rules are violated (i.e. if the actual pointer in question was a local, and you saved it off somewhere longer lived).

The bottom line, however, is just don't do this.  There were only isolated cases in Ada95 and Ada2005 where this would have been necessary, and Ada2012 solved the problem once and for all by adding out parameters for functions.  There is no reason to ever try and cast an access parameter, and you are foolish to try.  If you need to copy a pointer, use a named access type (actually, you are even more foolish to to use a named access type anywhere in a public interface, but that's another post).  Moreover, there is no reason to even dispatch on an access value anymore, so start converting your OOP frameworks to use nothing but normal types.  In fact, as we will see later, Ada has evolved to a point where even the use of access parameters in the 'normal' sense (i.e. accessibility) is mostly antiquated.

So that's a lot of history about a feature that was bad to begin with and should never be used.  But it's just as important, if not moreso, to know how not to use the language; especially when it's confusing and misleading in the first place.

No comments:

Post a Comment