-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #977 from gusthoff/content/ada_idioms/review/edito…
…rial/structure/20240105 Editorial changes to "Ada Idioms" course
- Loading branch information
Showing
10 changed files
with
962 additions
and
926 deletions.
There are no files selected for viewing
285 changes: 285 additions & 0 deletions
285
content/courses/ada-idioms/chapters/abstract_data_machines.rst
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,285 @@ | ||
.. _Ada_Idioms_Abstract_Data_Machines: | ||
|
||
Abstract Data Machines | ||
====================== | ||
|
||
.. include:: ../../global.txt | ||
|
||
Motivation | ||
---------- | ||
|
||
.. todo:: | ||
|
||
Complete section! | ||
|
||
|
||
Solution | ||
-------- | ||
|
||
The Abstract Data Machine (ADM) is similar to the | ||
:ref:`Abstract Data Type <Ada_Idioms_Abstract_Data_Types>` in that it | ||
presents an abstraction, something that doesn't already exist in the | ||
programming language. Furthermore, like the ADT, operations are provided to | ||
manipulate the abstraction data, state that is not otherwise compile-time | ||
visible to client code. These operations are thus enforced as the only | ||
manipulation possible, as per the designer's intent. (The Abstract Data | ||
Machine was introduced by Booch as the Abstract State Machine, but that name | ||
can be confused with another concept that, though somewhat related, is not the | ||
same thing.) | ||
|
||
Unlike the ADT, however, the ADM does not define the abstraction as a type. To | ||
understand this point, recall that type declarations are descriptions for | ||
objects that will contain data. For example, our earlier :ada:`Stack` type was | ||
defined as a record containing two components: an array to hold the values | ||
logically contained by the :ada:`Stack`, and an integer indicating the logical | ||
top of that array. No data actually exist, i.e., are allocated storage, until | ||
objects are declared. Clients can declare as many objects of type :ada:`Stack` | ||
as they require, and each object has a distinct, separate copy of those two | ||
components. | ||
|
||
Clients can, of course, choose to declare only one object of a given type, in | ||
which case only one instance of the data described by the type will exist. But | ||
in that case, other than convenience there is no functional difference from | ||
declaring objects of the component types directly, rather than indirectly via | ||
some enclosing type. Instead of using the :ada:`Stack` type to declare a | ||
single composite object, for example, the developer could have instead declared | ||
two objects, one for the array and one for the stack pointer: | ||
|
||
.. code-block:: ada | ||
|
||
Capacity : constant := 100; | ||
type Content is array (1 .. Capacity) of Integer; | ||
Values : Content; | ||
Top : Integer range 0 .. Capacity := 0; | ||
|
||
or even this, using an anonymously-typed array: | ||
|
||
.. code-block:: ada | ||
|
||
Capacity : constant := 100; | ||
Values : array (1 .. Capacity) of Integer; | ||
Top : Integer range 0 .. Capacity := 0; | ||
|
||
If there is only one *stack* these two objects will suffice. | ||
|
||
That's what the ADM does. The necessary state for a single abstraction instance | ||
is declared in a package, usually a library package. But as an abstraction, | ||
those data declarations must not be compile-time visible to clients. Therefore, | ||
the state is declared in either the package private part or the package body. | ||
Doing so requires that visible operations be made available to clients, as any | ||
abstraction would require. Hence the package is the abstraction instance, as | ||
opposed to one or more objects of a type. | ||
|
||
Therefore, the package declaration's visible section will contain only the | ||
following: | ||
|
||
- Constants (but almost certainly not variables) | ||
|
||
- Ancillary Types | ||
|
||
- Exceptions | ||
|
||
- Operations | ||
|
||
The package declaration's private part and the package body may contain all the | ||
above, but especially one or the other (or both) will contain object | ||
declarations representing the abstraction's state. | ||
|
||
Consider the following ADM version of the package :ada:`Integer_Stacks`, now | ||
renamed to :ada:`Integer_Stack` for reasons we will discuss shortly. In this | ||
version we declare the state in the package body. | ||
|
||
.. _Ada_Idioms_Abstract_Data_Machines_Code_Example: | ||
|
||
.. code-block:: ada | ||
|
||
package Integer_Stack is | ||
procedure Push (Item : in Integer); | ||
procedure Pop (Item : out Integer); | ||
function Empty return Boolean; | ||
Capacity : constant := 100; | ||
end Integer_Stack; | ||
|
||
package body Integer_Stack is | ||
Values : array (1 .. Capacity) of Integer; | ||
Top : Integer range 0 .. Capacity := 0; | ||
procedure Push (Item : in Integer) is | ||
begin | ||
-- ... | ||
Top := Top + 1; | ||
Values (Top) := Item; | ||
end Push; | ||
procedure Pop (Item : out Integer) is ... end Pop; | ||
function Empty return Boolean is (Top = 0); | ||
end Integer_Stack; | ||
|
||
Now there is no type presenting a :ada:`Stack` abstraction, and the operations | ||
do not take a stack parameter because the package and its data is the | ||
abstraction instance. There is only one stack of integers with this idiom. That | ||
is why the name of the package is changed from :ada:`Integer_Stacks`, i.e., | ||
from the plural form. | ||
|
||
As with the ADT idiom, clients of an ADM can only manipulate the encapsulated | ||
state indirectly, via the visible operations. The difference is that the state | ||
to be manipulated is no longer a formal parameter. For example: | ||
|
||
.. code-block:: ada | ||
|
||
Integer_Stack.Push (42); | ||
|
||
That call places the value 42 in the array :ada:`Integer_Stack.Values` located | ||
within the package body. Compare that to the ADT approach, in which objects of | ||
type :ada:`Stack` are manipulated: | ||
|
||
.. code-block:: ada | ||
|
||
Answers : Stack; | ||
-- ... | ||
Push (Answers, 42); | ||
|
||
That call places the value 42 in the array :ada:`Answers.Values`, i.e., within | ||
the :ada:`Answers` variable. Clients can declare as many :ada:`Stack` objects | ||
as they require, and each will contain a distinct copy of the state defined by | ||
the type. In the ADM version there is only one stack and therefore one instance | ||
of the state. | ||
|
||
Rather than declare the abstraction state in the package body, we could just as | ||
easily declare it in the package's private section: | ||
|
||
.. code-block:: ada | ||
|
||
package Integer_Stack is | ||
procedure Push (Item : in Integer); | ||
procedure Pop (Item : out Integer); | ||
function Empty return Boolean; | ||
Capacity : constant := 100; | ||
private | ||
Values : array (1 .. Capacity) of Integer; | ||
Top : Integer range 0 .. Capacity := 0; | ||
end Integer_Stack; | ||
|
||
Doing so doesn't change anything from the client code point of view. Just as | ||
clients have no compile-time visibility to declarations in the package body, | ||
they have no compile-time visibility to the items in the package private part. | ||
This placement also doesn't change the fact that there is only one instance of | ||
the data. We've only changed where the data are declared. (We will discuss the | ||
effect of child packages separately.) | ||
|
||
The private section wasn't required when the data were declared in the package | ||
body. That's typical when using this idiom but is not a necessary | ||
characteristic. | ||
|
||
The ADM idiom applies information hiding to the internal state, similar to the | ||
ADT idiom, except that the state is not in objects. As well, like the | ||
:ref:`Groups of Related Program Units <Ada_Idioms_Groups_Of_Related_Program_Units>`, | ||
the implementations of the visible subprograms are hidden by the package body, | ||
along with any non-visible entities required for their implementation. | ||
|
||
There will be no constructor functions returning a value of the abstraction | ||
type because there is no such type with the ADM. However, there could be one or | ||
more initialization procedures, operating directly on the hidden state in the | ||
package private part or package body. In the :ada:`Stack` ADM there is no need | ||
because of the reasonable initial state, as is true with the ADT version. | ||
|
||
The considerations regarding selectors/accessors are the same for the ADM as | ||
for the ADT idiom, so they are not provided by default. Also like the ADT, | ||
so-called *getters* and *setters* are highly suspect and not provided by the | ||
idiom by default. | ||
|
||
Pros | ||
---- | ||
|
||
In terms of abstraction and information hiding, the ADM provides the same | ||
advantages as the ADT idiom: clients have no representation details visible and | ||
must use the operations declared in the package to manipulate the state. The | ||
compiler enforces this abstract view. The ADM also has the ADT benefit of | ||
knowing where any bugs could possibly be located. If there is a bug in the | ||
manipulation, it must be in the one package defining the abstraction itself. No | ||
other code would have the compile-time visibility necessary. | ||
|
||
This idiom can be applied to any situation requiring abstraction, including | ||
hardware. For example, a particular microprocessor had an on-board rotary | ||
switch for arbitrary use by system designers. The switch value was available to | ||
the software via an 8-bit integer located at a dedicated memory address, mapped | ||
like so: | ||
|
||
.. code-block:: ada | ||
|
||
Switch : Unsigned_8 with | ||
Volatile, | ||
Address => System.Storage_Elements.To_Address (16#FFC0_0801#); | ||
|
||
Reading the value of the memory-mapped :ada:`Switch` variable provided the | ||
current switch value. | ||
|
||
However, the memory at that address was read-only, and rightly so because the | ||
only way to change the value was to physically rotate the switch. Writing to | ||
that address had no effect whatsoever. Although doing so was a logical error no | ||
indication was provided by the hardware. That silence was potentially confusing | ||
to developers. It certainly looked like a variable, after all. Declaring it as | ||
a constant wouldn't suffice because the user could rotate the switch during | ||
execution. | ||
|
||
Furthermore, although mapped as a byte, the physical switch has only 16 total | ||
positions, read as the values zero through fifteen. An unsigned byte has no | ||
such constraints. | ||
|
||
A good general rule is that if something shouldn't be done by clients, we | ||
should use the compiler to make it impossible. That's better than debugging, | ||
any day. Therefore, we will use the ADM idiom to represent the rotary switch. | ||
The compiler will enforce the read-only view and the operation can handle the | ||
range constraint. The ADM is a reasonable choice because there is only one such | ||
physical switch; a type doesn't bring any advantages in this case. The | ||
following illustrates the approach: | ||
|
||
.. code-block:: ada | ||
|
||
with Interfaces; use Interfaces; | ||
package Rotary_Switch is | ||
subtype Values is Unsigned_8 range 0 .. 15; | ||
function State return Values; | ||
end Rotary_Switch; | ||
|
||
Clients can then call the function :ada:`Rotary_Switch.State` to get the | ||
switch's current value, as a constrained subtype. The body will handle all the | ||
details. | ||
|
||
.. code-block:: ada | ||
|
||
with System.Storage_Elements; use System.Storage_Elements; | ||
package body Rotary_Switch is | ||
Switch : Unsigned_8 with Volatile, Address => To_Address (16#FFC0_0801#); | ||
function State return Values is | ||
begin | ||
if Switch in Values then | ||
return Switch; | ||
else | ||
raise Program_Error; | ||
end if; | ||
end State; | ||
end Rotary_Switch; | ||
|
||
The range check in the function body might be considered over-engineering | ||
because the switch is a physical device that cannot have more than 16 values, | ||
but physical devices have a habit of springing surprises. Note that | ||
:ref:`attribute Valid <Adv_Ada_Valid_Attribute>` would not be useful here | ||
because there are no invalid bit patterns for an unsigned integer. If, on the | ||
other hand, we were working with an enumeration type, for example, then | ||
:ada:`'Valid` would be useful. | ||
|
||
Cons | ||
---- | ||
|
||
An ADM defines only one abstraction instance. If more than one is required, the | ||
developer must copy-and-paste the entire package and then change the package | ||
unit name. | ||
|
||
Furthermore, the ADM cannot be used to compose other types, e.g., as the | ||
component type in an array or record type. The ADM cannot be used to define the | ||
formal parameter of a client-defined subprogram, cannot be dynamically | ||
allocated, and so on. | ||
|
||
But if one can know with certainty that only one thing is ever going to be | ||
represented, as in the hardware switch example, the ADM limitations are | ||
irrelevant. That said, certainty is usually not available |mdash| even the | ||
hardware changes. |
Oops, something went wrong.