Ada 2012 Tutorial #1

Ada 2012 Tutorial

Ada Lovelace, the namesake of the Ada programming language, considered the world’s first computer programmer

    Welcome to the tutorial! I will be making some assumptions which are fairly safe: first, that you are unfamiliar with the Ada language; second, you have at least some interest in discovering what it is about; third, that you have some programming experience; and last, that you have an Ada Compiler. (There’s a free one available from AdaCore here, and the GCC has one as well.)
    Ada is probably different than what programming languages you are likely to be familiar with, this is a result of Ada’s design goals — two of which are safety and maintainability. The first means that Ada tries to do a lot of checking up-front, in compilation if possible, which reduces the time spent debugging at the cost of the compiler rejecting erroneous source. That can be frustrating at times, but it is better than having to spend three days tracking down a bug. This leads us to the second difference, Ada was designed to be highly maintainable, even across large teams, which is evident in its package system.
    To introduce the language I will use a small and simple (read as ‘toy’) LISP-like interpreter. To begin with, we need to realize that LISP runs on a loop of read-input, evaluate, and print.

Important note: Due to WordPress’s “helpful” reformatting the comments, which are two single en-dashes (minus-key), may be reformatted. Thus, if you copy the code, you will have to perform a search and replace.
Testbed.adb
— ‘With’ introduces a compilation unit dependency to this compilation unit;
— it’s roughly analogous to the ‘using’ of C#… except that it doesn’t alter visibility.
— Note: a compilation unit is a procedure, function, or package.

With
Ada.
Text_IO;— Ada.Text_IO: exactly what it says.

— This names the current procedure “testbed”.
Procedure Testbed is

  — Stubs for Read, Eval, and Print.

  Procedure Read is
  begin
    — The reserved-word ‘null’ can be used to indicate a null-statement.
    — This explicit naming of the null-statement means that the compiler
    — can detect stray semi-colons.
    null;
  end Read;

  Procedure Eval is
begin
    null;
  end
Eval;

  Procedure Print is
begin
    null
;
  end
Print;

  — Hack to terminate the loop; we will replace this later, with an
  — orderly shutdown-mechanism.
  Loop_Count :
Positive:= 1;

Begin

— We can name loops, and declaration blocks, this allows for stability when
— adding or deleting them, as is common when altering algorithms. This can
— help because instead of numbering the constructs to ‘break’, the exit is
— tied to the loop that it is associated with.

— EXAMPLE: Below we name the Read-Eval-Print Loop as “REPL”.

REPL:
loop
— Here we use an attribute, “image”, to obtain the string-value of the
— counter. All discrete types, including enumerations, have this.

Ada.Text_IO.Put_Line(“[Count:” & PositiveImage(Loop_Count) & ‘]’);

— Call read, eval, and print: this is the lungs, heart and blood of our interpreter.
Read
;
Eval;
Print;

— Set up the exit-condition; the name is optional but useful when dealing
— with nested loops & declare-blocks.
Exit REPL when Loop_Count =
10;

— Update the loop-counter.
Loop_Count:=
Loop_Count + 1;

end loop REPL;

— Print some text to indicate an orderly shutdown was achieved.
Ada.
Text_IO.Put_Line( “Goodbye.” );

End Testbed;

Save the above as Testbed.adb and we will continue.
    Notice that Ada allows us to define procedures within procedures; this nesting extends to subprograms and packages, it allows you to group things together, thereby reducing the clutter in visibility (read “the namespace”) and keep things logically together.
    Noting that LISP deals with lists which it defines recursively as: an element, or an element followed by a list we need to decide how we will implement them. The previous definition lends itself well to a linked-list approach, but Ada provides a set of attributes for arrays that provide much of the functionality we would have to implement on a roll-your-own linked-list approach… finally, we have the contents of the predefined container-packages.
    While there are several linked list packages in the containers, we’ll use a vector: this gives us much of the array-like functionality that is attractive, some of the list-manipulation functions we’ll need right out of the box, and will be simple to work with. Lastly, we need to consider what the elements of the list should be; there’s no particular reason for anything fancy so we’ll use a simple record. Ada allows for considerable organization with its packaging-system, and so we’ll use that to help organize our system; while it could be put into a single package, it is perhaps adventurous to introduce the concept of child packages early on.
    Now we can write the following three specification files:
lisp.ads

Package LISP is
Pragma Pure;

— Parse_Error is used to signal when there has been a problem parsing input; such as unbalanced parentheses.
Parse_Error,
— Undefined_Element is used to signal when there is a reference to an undefined entity; usually a function.
Undefined_Element :Exception;
End LISP;

lisp-lists.ads
lisp-elements.ads
— We are importing two items: the ‘elements’ child of LISP, which we
— haven’t yet shown (it is on the right; this would normally create a
— circular-dependency, and I will show you Ada’s way of handling it),
— and one of Ada’s language-defined containers.

With
LISP
.elements,
Ada
.Containers.Indefinite_Vectors;
— ‘Use Type’ was a visibility modifier introduced in Ada 95 to allow
— a type’s operators (+, -. *, /, etc) to be visible without having
— to use a ‘use’ clause, which would “pollute the namespace.”
— Ada 2012 extends this with ‘All’, indicating that all the type’s
— primitive operations are to be made visible.
Use All Type LISP
.Elements.Element;

Package
LISP
.Lists is
— The following package, “implementation”, is the instantiation of
— a generic package; a generic must be explicitly instantiated
— before its use in your program text.
Package Implementation is new Ada
.Containers.Indefinite_Vectors
(Index_Type => Positive, Element_Type => LISP.Elements.Element);
–(PS: The above is using named association, which can be helpful.)
— The following line declares inheritance, with no extension to the
— data of the base object/class; there are, however, ‘methods’
— which are added below.
Type LIST is new Implementation
.Vector with null record;

— Primitive operations are those subprograms which interact with
  — a type and are declared before the type is frozen. (Read as:
  — in the same declaring unit, before some point which the compiler
  — must commit to a type.)
  —
  — The following are primitive operations for LIST.
  Function Evaluate
(Input : List) Return List;
Function Head
(Input : List) Return LISP.Elements.Element;
Function Tail(Input : List) Return List;
  Function To_String (Input : List) Return String;
  Function Homoginized_List( Input : List ) Return Boolean;

End Lisp.Lists;

Limited with LISP.Lists;

Package LISP.Elements is

  Type Data_Type is
(Empty_Type,
                   Integer_Type,
                   String_Type, Name_Type,
                   List_Type);

  Type Element(<>) is tagged private;

  Function Create Return Element;
  Function Create( Item : Integer ) Return Element;
  Function Create( Item : String ) Return Element;
  Function Create( Item : Lists.List ) Return Element;

  Function To_String(Item: Element) return String;
  Function Get_Type (Item: Element) return Data_Type;

  Function To_List (Item: Element) return Lists.LIST;
  Function As_List (Item: Element) return Lists.LIST;

Private

  Type Element( Data : Data_Type ) is tagged record
    Case Data
is
    when Empty_Type
=>Null ;
    when Integer_Type => Int_Val : Integer ;
    when Name_Type |
         String_Type => Str_val : not null access
Standard
.
String;
    when List_Type => List_Val : Not Null Access
LISP
.
Lists.ListClass;
    End case;
  End Record;

  Function Create Return Element is ( Data => Empty_Type );
  Function Get_Type (Item: Element) return Data_Type is ( Item.Data );

End Lisp.Elements;

 

    These specification files are valid but not complete, partially because this is a tutorial and I don’t want to bog you down with too much at once. So let’s get down to explaining what’s going on in these three files, first a short explanation of the files themselves, then a bit more detail as to what they are doing.
  1. LISP.ads
    This is a pure package, this is Ada-terminology for declaring that there is no state-variable in the unit.
    It also declares two exceptions we will not be using right now, but at a later date.
  2. LISP-lists.ads
    This defines a type “List”*, which is the extension of a language-defined vector-object; to the parameter Element_Type we assign the element defined in LISP-Elements.ads; we then derive a type from its own Vector type, and extend its methods.
    (* In Ada the only casing that matters is the language’s name, it’s a proper name, and the internals of strings.)
  3. LISP-elements.ads
    • This unit depends on the LIST type defined in LISP-lists.ads… which depends on LISP-elements.ads
      • To break the circularity the Limited With is used to allow the reference to lists in a non-circular manner by exposing a more limited view — this recycling of keywords into a sort of phrase is the result of the ARG .
    • The enumeration Data_Type is defined in the public section of the specification, where Element is introduced but not defined.
      • Element is introduced with (<>) which indicates that it is indefinite, and probably discriminated.
      • Tagged is the keyword that indicates that the type being declared is for an object (C++/Java “class”) in Ada class indicates “that tagged-type or any type inheriting from it”, and is indicated as an attribute of tagged types.
    • The keyword private appears twice, each instance respectively indicates:
      • the type is fully defined in the private section, so that the clients using that type are prevented from depending on the internal implementation.
      • the start of the private section; there are implementation details which must be made available to the compiler to support separate compilation like calling conventions [caller v. callee cleanup and parameter order would each alone demand this] or items which might be changed and should not be depended on, like whether you’re using arrays or a linked-list internally.
    • Note the full-declaration of Element has what appears to be a variable-declaration in its definition, this indicates the type is discriminated, and allows the underlying record to have separate data-elements (layouts, if you will) dependent on that variable. In this example, when Data_Type is Empty_Type there are no other available fields, but when it is Integer_Type the integer field “int_val” is available for use.
    • Finally, note the expression functions, located after the Element type’s full-definition, they are a new feature to Ada 2012 which allows for naming an expression and/or compact declaration of relatively simple functions. (i.e. they are equivalent to a function with a single return statement, and therefore have no declarative region.)
    In order to get an executable we need either the object-files cooresponding to the implementations of these specifications, or the bodies for these specification-files (which will be used to generate the object-files).
lisp-elements.adb

With
Ada.Strings.Fixed,
LISP.Lists;

Package body LISP.Elements is
  Use LISP
.Lists;

  ————–
  — CREATE —
  ————–
  Function Create( Item : Integer ) Return Element is
    ( Data => Integer_Type, int_val => Item );

  Function Create( Item : Standard.String ) Return Element is
    ( Data => String_Type, str_val => New Standard.String‘(Item));

  Function Create( Item : List ) Return Element is
    ( Data => List_Type, List_val => new List‘(Item));

  — To_String converts the element to a string representation.
  Function To_String(Item: Element) return String is
    Use Ada
.Strings, Ada.Strings.Fixed;

    — The Integer’Image attribute outputs an integer as a string;
— however, it puts a single space if it is positive or ‘-‘
— in the case of a negative numbers… the Trim function from
— Strings.Fixed removes that white-space.

    Function Image(Item : Integer) Return String is
    ( Trim(IntegerImage(Item), Side => Left));

  begin
    Case Item
.Data is
    when Empty_Type
=> Return “NULL”;
    when Integer_Type => Return Image(Item.Int_Val);
    when Name_Type |
         String_Type => Return Item.Str_val.All;
    when List_Type => Return To_String(Item.List_Val.All);
    End case;
  end To_String;

  — To_List places the given element into a new list.
Function To_List (Item: Element) return Lists.LIST is
begin
  Return Result
: Lists.LIST do
    Result
.Append( Item );
  End return;
end To_List;

— As_List returns the given element if it is a list
— otherwise it calls To_List.
Function As_List (Item: Element) return Lists.LIST is
   
( if Item.Data = List_Type then Item.List_Val.Copy else Item.To_List );

— Valid_Name returns true if the given string is a valid function-name.
Function Valid_Name( S : Not Null Access String ) Return Boolean is
  Use Ada
.Strings, Ada.Strings.Fixed;
  Temp : String renames Trim(Source => S.All, Side => Both);
begin
  Return Temp
= S.All and
        (for all Ch of S.All=> Ch in ‘A’..‘Z’|‘0’..‘9’|‘_’);
end Valid_Name;

End LISP.Elements;

lisp-lists.adb
Package body LISP.Lists is
use Implementation
;
use type Ada.Containers.Count_Type; — Returns the first element of the list, if the list is empty
— it returns
an Empty_Type element.
Function Head(Input : List) Return LISP.Elements.Element is
(if Input.Length = 0 then Create else Input.First_Element); — Tail returns a copy of the list, less the first element.
Function Tail(
Input : List) Return List is
begin
  Return Result
: List:= Input.Copy do
    Result
.Delete_First;
  End return;
end Tail; — To_String converts a list to its string representation.
Function To_String (
Input : List) Return String is
  — We define something to hold the string-representations
  — of the individual elements, so we can construct the
  — list’s string.
  Type String_Set is Array(Positive Range <>)of Access Standard.String;  — Here we provide the function to convert the above
  — into a single string, it’s nested, overloaded,
  — and recursive.
  Function To_String( S: String_Set ) Return String is
  begin
    if S
Length = 0 then
      Return
“”;
    else
      — Below we create a subtype of the range we
      — want to use in our array-slice; as you can
      — see, the attributes used are:
      — First, Last, and Succ.
      — “Succ”
is short for the successive element
      — of a discrete type.
      declare
        Subtype Tail is Positive Range
PositiveSucc(SFirst)..SLast;
      begin
      — Here we return the first element’s value, followed
      — conditionally by a separator, and the recursive call to
      — the function. The conditional expression is new to
      — Ada 2012 and comes in the (if/then[else])-flavor, and the
      — (case)-flavor; both must be in parentheses, this is to
      — distinguish them from normal conditional statements.
        Return S(
SFirst).All&
               (if SLength > 1 then SEPARATOR else “”)&
               To_String( S(Tail));
      end;
    end if;
  end To_String;  — Internal String level 2 (is2) performs the transformation of List’s
  — elements to a String_Set, and uses To_String to return the result.
  Function is2(
Input : list) return Standard.string is
    — Strings declares a string_set of the appropriate size, and
    — temporarily fills it with ‘pointers’ to the empty string,
    — further, note the use of ‘others’ to denote an arbitrary range.
    Strings :
String_Set(1..Natural(Input.Length)):=
                        ( others => new Standard.String‘(“”));
  begin
    — Here we get the strings of all the individual elements.
    for Index in StringsRange loop
      declare
        — Here we type-cast to “Vector”, to take advantage of
        — implicit-derefrencing , after indexing the proper element
        Item :
String:= Vector(Input)(Index).To_String;
      begin
        — …and put the result into its proper place.
        Strings(
Index):= new String‘( Item );
      end;
    end loop;
    — After which we have the proper list.
    Return To_String(
Strings );
  end is2; begin
  — Add the parentheses to the list’s internal string, and return.
  Return ‘(‘&
is2(Input)&‘)’;
end To_String; — Homoginized_list returns true when all elements of the given list are the
— same, false otherwise. This will be useful for arithmetic functions.
Function Homoginized_List(
Input : List ) Return Boolean is
  Use Type Elements
.Data_Type;
  T : Elements.Data_Type Renames Input.Head.Get_type;
begin
  — If the Length of the list is less than two, then it must be homogenous,
  — otherwise we have to test all elements of the list for the proper type.
  Return Input.
Length < 2
         or else
(For all E of Input
=> E.Get_Type = T);
End Homoginized_List;— Evaluate is the heart of the interpreter… too bad this is a stub.
Function Evaluate(
Input : List) Return List is
  Use Elements
;
begin
  Return Input
.
Copy;
end Evaluate;End LISP.Lists;
    Now we have something that we can compile. It’s not usable at all, for two reasons: 1) there’s no input [the Read-function in our REPL] and 2) the evaluation function. On the other hand, we already have everything we need to do the Print portion of our REPL; so we alter out testbed function as follows:

— We can also put use clauses on the inside of packages and subprograms.
Use LISP.Lists, LISP.Elements;

— Our working list.
Working : List;

Procedure Read is
begin
  — Artificial input, merely a placeholder until we get to parsing.
  Working.Append( Create( Loop_Count ));
end Read;

Procedure Eval is
begin
  — Eval replaces our working list with the result of the evaluation.
  Working:= Working.Evaluate;
end Eval;

Procedure Print is
  Use Ada.Text_IO;
begin
  — To print we simply pass to Put_Line the result of Working.To_String
  — Note: When working with tagged-types we may use an alternative to
  — the more common object-dot-method; it can be called just like
  — the subprogram it is.
  Put_Line( To_String(Working)); — Same as Put_Line(Working.To_String);
end Print;

    Now we will turn our attention to the evaluation function in the LISP-Lists.adb file. We will need a fey new types. One to specify a function (a callback), and one to associate a string (or another type) to that function. We can use the standard map package to satisfy the second type, and the callback type is straightforward.

— Function_Type is merely a callback, the one we will use to define
— (and “register”) functions in the interpreter.
Type Function_Type is
      Not Null Access Function
( Input : List ) Return Elements.Element;

— Function_List_Pkg defines a map (of String => Function_Type) which we
— will use to hold functions.
Package Function_List_Pkg is new Ada.Containers.Indefinite_Ordered_Maps
  (Key_Type => String,
    Element_Type => Function_Type
  );

— Dictionary is the variable, of the aforementioned type, which holds the
— string/function associations.
Dictionary : Function_List_Pkg.Map;

— Get_Function reterns the function associated with the given string; it
— has been renamed (1) for clarity in the program text when invoked, and
— (2) to provide a central place to edit should more error-checking or
— correction be needed than provided by Function_List_Pkg.Map.Element.
Function Get_Function (Name: String) Return Function_Type
    Renames Dictionary
.Element;

— Evaluate is the heart of the interpreter.
Function Evaluate(Input : List) Return List is
  Use Elements
;
begin
  — Check if Head is an executable name.
  — Right now a regular string is indistinguishable from a name, it will
  — be different later on, particularly when we come to car/cdr commands.
  if Input.Head.Get_Type in String_Type..Name_Type then
    Declare
      — Get the name, then the function from the name, and execute it.
      Function_Name : String renames Input.Head.To_String;
      F : Function_Type:= Get_Function( Function_Name );
      E : Elements.Element:= F(Input.Tail);
    Begin
      — If the result is a list, return it, if not convert it.
      Return (if E.Get_Type = List_Type then
              E
.As_List
              else
              E
.To_List);
    End;
  else
    — If the head is not executable, then we return the list itself.
    Return Input.Copy;
  end if;
end Evaluate;

Now for the definition of some functions:
LISP-elements-operations.ads

With LISP.Lists;

Package LISP.Elements.Operations is

— It would be more convenient to refer to “List” in parameters
— than LISP.Lists.List, though there is nothing preventing us from
— using the fully-qualified format.
Use LISP.Lists;

— Operable tells us that the list can indeed be operated on; every function
— in LISP is a list, but not every list is a function.
— Example: What should (1 2 3 4 6) do? On the other hand (“+” 1 2 3 4 5)
— is simple enough — we apply the function “+” to the tail,
— which returns 15.
Function Operable( Input : List; Element_Type : Data_Type ) Return Boolean;
Function Operable( Input : List ) Return Boolean;

— Addition.
Function Plus( Input : List ) Return Element
with Pre
=> Operable(Input);

— Subtraction.
Function Minus ( Input : List ) Return Element
with pre
=> Operable(Input);

— And so forth…

End LISP.Elements.Operations;

    So now we are presented with multiple subprograms which differ only in the function which is applied, a good place to use generic for the subprograms.
LISP-elements-operations.adb

With Ada.Containers;

Package body LISP.Elements.Operations is
Use Type Ada
.Containers.Count_Type;

— To be ‘operable’ the input must be homoginous if it has more than one element.
— If the element’s type has been specified, all elements must be of that type, if it
— is not, then the type will be that of the first element.
— NOTE: The input is the TAIL of the list that was passed in (i.e. only the parameters).
Function Operable
( Input : List; Element_Type : Data_Type ) Return Boolean is
( if Input.Length > 0 then
  Input
.Head.Get_Type = Element_Type and
  Input
.Homoginized_List
);

Function Operable( Input : List ) Return Boolean is
  (if Input.Length=0 then True else Operable(Input, Input.Head.Get_Type));

— Note that we can parametrize types, functions, and [though not shown] values.
— The functions [and values] can be given defaults; the “is <>” for functions denotes
— “the subprogram named [and visible] that matches the parameters [and return-type].”
Generic
  — ‘Component’ is the internal type that are concerned about: Integer, String, etc.
  Type Component
(<>) is Private;
  — Value returns the proper component of the LISP.Elements.Element Type.
  With Function Value ( Item: Elements.Element ) Return Component is <>;
  — Create parameters to the used functions.
  With Function Create( Item: Component ) Return Elements.Element is <>;
  With Function “+”( Right, Left : Component ) Return Component;
Function Operation( Input : List ) Return Elements.Element;

— Here we give the body to the generic, note that the specification of the subprogram
— must be repeated.
Function Operation
( Input : List ) Return Elements.Element is
  — Our initial value is the first parameter; this means (- 7 3) yields 4.
Temp
: Component:= Value( Input.Head );
Begin
  — For all of the following items we apply the operation [parametrized as “+”] to
— the temporary and the result of the function “Value”.

  For Item of Input
.Tail loop
    Temp
:= Temp + Value(Item);
  end loop;
  — Finally, we pass the Temporary.
  Return Create
( Temp );
End Operation;

——————————-
— Operator Instantiations —
——————————-

Function Plus_Op is New Operation( Element => Integer,“+”=>“+”);
Function Minus_Op is New Operation( Element => Integer,“+”=>“-“);

————————-
— Operation Renames —
————————-

Function Plus ( Input : List ) Return Element Renames Plus_Op;
Function Minus ( Input : List ) Return Element Renames Minus_Op;

End LISP.Elements.Operations;

    The instantiation of a generic cannot be the body of a subprogram; this is because the Ada reference manual specifies the instantiation as being equivalent to the specification followed immediately by the body that, in turn, prevents the instantiation from acting as the body for the specification.
    Now we need to alter LISP-elements.ads & adb to allow for the method “value” to return the proper record-field of our tagged record.
— Private specifications; in case we need to move the implementation to
— the body, these will allow us to keep everything else here unchanged as it
— preserves visibility to child packages.

Function Value( E: Element ) Return Integer;
Function Value( E: Element ) Return String;
— Expression-Functions as completions.
Function Value( E: Element ) Return Integer is ( E.Int_val );
Function Value( E: Element ) Return String is ( E.Str_val.all );
    As you can see, we have overloaded the accessor (method name) ‘Value’ to access the internal field of the element. This is not to defeat the type-system of Ada, but rather facilitate the generic Operation in the operations child-package of elements: by overloading the name and giving proper visibility, we allow the omition of the parameter Value (the function passed to the instantiation), as we do with Create.
Now we are ready to alter the LISP-Lists to regester the functions “+” and “-“.
The end of LISP.Lists.adb

— We want Operations to have the full-visibility for registering them.
Use Elements.Operations;
Begin
  Dictionary.Insert(Key =>“+”,
      New_Item => PlusAccess
    );
Dictionary.Insert(Key =>“-“,
      New_Item => MinusAccess
    );
End
LISP.Lists;

Finally, we alter our Testbed function so that we might test our interpreter.
Testbed.Read

Procedure Read is
begin
  — Artificial input, placeholder until we get to parsing.
  if Loop_Count /= 2 then
    Working
.Append( Create( Count ));
  else
    — We need the “+” at the head of the list, so prepend instead of append.
    Working.Prepend( Create(“+”) );
    Ada.Text_IO.Put_Line( “–DEBUG– “& Working.To_String );
  end if;
end Read
;

Output
[Count: 1]
(1)
[Count: 2]
(1, 2)
[Count: 3]
(1, 2, 3)
[Count: 4]
–DEBUG– (+, 1, 2, 3)
(6)
[Count: 5]
(6, 5)
[Count: 6]
(6, 5, 6)
[Count: 7]
(6, 5, 6, 7)
[Count: 8]
(6, 5, 6, 7, 8)
[Count: 9]
(6, 5, 6, 7, 8, 9)
[Count: 10]
(6, 5, 6, 7, 8, 9, 10)
Goodbye.
    Next, we take a look at LISP’s specification for list manipulation: car and cdr are used to specify, respectively, the head and tail of the list. If it were just that it would be easy: we’d just register “car” and cdr” as we did “+” and “-” — on page 4, the LISP 1.5 specification says:
It is convenient to abbreviate multiple car’s and cdr’s. This is done by forming function names that begin with c, end with r, and have several a’s and d’s between them.
Examples
cadr[(A B C)]=car[cdr(A B C)]=B
caddr[(A B C)]=C
cadadr[(A (B C) D)]=C
    The natural outcome of this is that there are an infinite number of functions for accessing list elements. Indeed, we could simply ignore the problem saying only car and cdr are supported, or we could make some arbitrary limit and say we’re only supporting that… or we could do things the right way and address the situation: simply applying the definition given. Thus we have the following modifications to LISP-Lists.adb:
Portions of LISP-lists.adb

— With the following Predicate, we are forcing names to be case insensitive
— by mandating that the result of To_Upper be equal to its own input.
— NOTE: This means that all function-names are, internally, UPPER-CASE.
subtype Name_String is String
with Dynamic_Predicate
=>
           
Name_String = Ada.Characters.Handling.To_Upper(Name_String);

— Function_Type is merely a callback, the one we will use to define
— (and “register”) functions in the interpreter.
Type Function_Type is
      Not Null Access Function
( Input : List ) Return Elements.Element;

— The “Function_List_Pkg” paackages define a mappping of String to the
— appropriate functions (precompiled/internal, or user/external); I would
— like to use the Name_String as the Key, however a compiler-error
— in my compiler [GNAT GPL 2012 (20120509)] prevents that.

— WORKAROUND: All key insertions & retrievals be of type Name_String.
Package Function_List_Pkg is new Ada.Containers.Indefinite_Ordered_Maps
  ( Key_Type => String,
    Element_Type => Function_Type
  );

Package User_Function_List_Pkg is new Ada.Containers.Indefinite_Ordered_Maps
  ( Key_Type => String,
    Element_Type => List
  );

— Dictionary is the variable, of the aforementioned type, which holds the
— string/function associations.
System_Dictionary : Function_List_Pkg.Map;
User_Dictionary : User_Function_List_Pkg.Map;

— Make Map & Cursor operations visible.
Use Type
  Function_List_Pkg
.Map,
  Function_List_Pkg.Cursor,
  User_Function_List_Pkg.Map,
  User_Function_List_Pkg.Cursor;

Function Sys_Key_Exists(Name : Name_String) Return Boolean is
  ( System_Dictionary.Find(Name)/= Function_List_Pkg.No_Element );

Function User_Key_Exists(Name : Name_String) Return Boolean is
  ( User_Dictionary.Find(Name)/= User_Function_List_Pkg.No_Element );

— Get_Function returns the function associated with the given string; it
— has been renamed (1) for clarity in the program text when invoked, and
— (2) to provide a central place to edit should more error-checking or
— correction be needed than provided by Function_List_Pkg.Map.Element.
Function Get_Function (Name: Name_String) Return Function_Type
  Renames System_Dictionary
.Element;

Function Get_Function (Name: Name_String) Return List
  Renames User_Dictionary
.Element;

— Get_Internals returns the given string less the first and last character.
Function Get_Internals(S : String) Return String is
begin
  Return S
( PositiveSucc(SFirst)..PositivePred(SLast));
end Get_Internals;

— Is Accessor returns true for functions accessiong list elemrnts;
— (CAR, CDR, and the abbreviations thereof: CAAR, CADR, CDAR, CDDR, etc.)
Function Is_Accessor(S : Name_String) Return Boolean is
begin
  Return S
Length > 2 AND
         S
(SFirst)=‘C’ AND
         S
(SLast)=‘R’ AND
        (for all C of Get_Internals(S)=> C in ‘A’|‘D’);
end Is_Accessor;

Function Is_Definition( Name : Name_String ) Return Boolean is
  ( Name =“DEFINE”);

— Evaluate is the heart of the interpreter.
Function Evaluate(Input : List) Return List is
  Use Elements
, Ada.Characters.Handling;
  Function_Name : String renames To_Upper(Input.Head.To_String);
begin
  — Check if Head is an executable name.
  — Right now a regular string is indistinguishable from a name, it will
  — be different later on, particularly when we come to car/cdr commands.
  if Input.Head.Get_Type in String_Type..Name_Type then
    if Is_Accessor(Function_Name) then
      — Here we execute the accessor to return the proper [sub]list.
      Return Result : List := Input.Tail do
      — We need to reverse the loop in order to have the proper
      — order; the manual gives the example of “caddr”
      — referencing the third element of a list and in order to
      — do that we need to process the last character first so
      — that we get “the head of the tail of the tail”.
        for C of reverse Get_Internals(Function_Name) loop
          case C is
          — CAR returns the head.
          when ‘A’=> Result:= (if Result.Get_Type = List_Type then Result.As_List
                                else Result
.To_List);
          — CDR returns the tail.
          when ‘D’=> Result:= Result.Tail;
          when others => raise Program_Error;
          end case;
        end loop;
      End return;
    elsif Is_Definition(Function_Name) then
      — Here is where we handle the user-defined functions.
      Handle_Definition:
      declare
      — This Function_Name hides the previous Function_Name; if
      — we needed that, we could reference it with the subprogram
      — name (i.e. Evaluate.Function_Name).
        Function_Name : Name_String renames To_Upper(Input.Tail.Head.To_String);
        Function_Body : List renames Input.Tail.Tail;
      begin
        User_Dictionary
.Insert(
            Key => Function_Name,
            New_Item => Function_Body
          );

Return
Create
(Function_Name)& Create(“Defined”);
      End Handle_Definition;
    elsif
Sys_Key_Exists
(Function_Name) then
      Handle_System_Function
:
      begin
        declare
          — Get the function from the name, and execute it.
          F : Function_Type Renames Get_Function( Function_Name );
          E : Elements.Element Renames F(Input.Tail);
        begin
          — If the result is a list, return it, if not convert it.
          Return (if E.Get_Type = List_Type then
                     E
.As_List
                  else
                     E
.To_List);
        end;

      — A CONSTRAINT_ERROR is raised when an invalid key is
      — given to a Map, the following handles that by raising
      — the proper exception: UNDEFINED_ELEMENT.
      — NOTE: This is a development artifact that should be
      — removed, I am leaving it as an example of
      — ‘converting’ exceptions.
      Exception
When
CONSTRAINT_ERROR
=> Raise Undefined_Element;
      end Handle_System_Function;
    elsif User_Key_Exists(Function_Name) then
      Handle_User_Function
:
      begin
        — Get the function from the name, and execute it.
Return
Get_Function
( Function_Name ).Evaluate;
      end Handle_User_Function;
    else
      — The function wasn’t found in our dictionaries.
      Ada.Text_IO.Put_Line(“[[DEBUGGING]] Unknown Function: “& Function_Name );
Raise
Undefined_Element
;
    end if;
  else
    — If the head is not executable, then we return the list itself.
    Return Input.Copy;
  end if;
end
Evaluate
;

— Homoginized_list returns true when all elements of the given list are the
— same, false otherwise. This will be useful for aritimatic functions.
Function Homoginized_List( Input : List ) Return Boolean is
  Use Type Elements
.Data_Type;
  T : Elements.Data_Type Renames Input.Head.Get_type;
begin
  — If the Length of the list is less than two, then it must be homogenous,
  — otherwise we have to test all elements of the list for the proper type.
  Return Input.Length < 2
         or else
(For all E of Input => E.Get_Type = T);
End Homoginized_List;

— We want Operations to have the full-visibility for registering them.
Use Elements.Operations; use LISP.Elements;
Begin
  System_Dictionary.Insert(
      Key =>“+”,
      New_Item => PlusAccess
    );

  System_Dictionary.Insert(
      Key =>“-“,
      New_Item => MinusAccess
    );
End
LISP.Lists;

Now with a slight modification to our testbed and we should get the following output:

Portions of Testbed.adb
Output
Procedure Read is
begin
  case Loop_Count
is
    When 8 =>
Working.Prepend( Create(“+”));
    When 10 => Working:= Create(“define”) &
Create(
“bob”) &
                        Create(“car”)&
                                          Create(
                            Create(
“Fire”) &
Create(
“Ice”) &
Create(
“Air”)
);
    — Above: (DEFINE BOB CAR (“Fire” “Ice” “Air”))
— Result: BOB => (“Fire” “Ice” “Air”)

    When Others =>
Working.Append( Create( Loop_Count ));
  end case;
end Read;

— … Somewhere in the declaration area Testbed.

Sub_Loop : array (Positive Range <>)
of Not Null Access Constant String
:=
( New String‘(“car”), New String‘(“cdr”),
         New String
‘(“cadr”), New String‘(“cdar”),
New String
‘(“caddr”), New String‘(“BOB”)
);

— Testing functions.
— Between the call to print and the loop’s exit.
if Loop_Count = 10 then
Working
:= Create(“A”)& Create(“B”)& Create(“C”);
For E of Sub_Loop loop
declare
Use Type LISP
.Lists.Implementation.Vector;
Temp : LIST := Create(E.All)& Working.Copy;
Use Ada.Text_IO;
begin
Put_Line
( ASCII.HT & Temp.To_String );
Temp:= Temp.Evaluate;
Put_Line( E.All& ASCII.HT &“=>”& Temp.To_String );
end;
End loop;
end if;

[Count: 1]
(1)
[Count: 2]
(1, 2)
[Count: 3]
(1, 2, 3)
[Count: 4]
(1, 2, 3, 4)
[Count: 5]
(1, 2, 3, 4, 5)
[Count: 6]
(1, 2, 3, 4, 5, 6)
[Count: 7]
(1, 2, 3, 4, 5, 6, 7)
[Count: 8]
(28)
[Count: 9]
(28, 9)
[Count: 10]
(BOB, Defined)
(car, A, B, C)
car =>(A)
(cdr, A, B, C)
cdr =>(B, C)
(cadr, A, B, C)
cadr =>(B)
(cdar, A, B, C)
cdar =>()
(caddr, A, B, C)
caddr =>(C)
(BOB, A, B, C)
BOB =>(Fire, Ice, Air)
Goodbye.
    This tutorial is getting rather long, so we’ll call this it and perhaps continue with parsing in a later installment. I will close with a few notes on the Ada mentality, which will be especially useful if you are coming from a C or C++ background:
  1. ‘Never’ use access
    Aside from the callbacks most of the Access types here were not needed, even in this tutorial, but were used for expediency — the self-imposed “Don’t Use Accesses” rule should balance out the training to [over]use pointers you’ve had from your prior [C/C++] experience. (If you need to alter parameters, try the out or in out modes.)
  2. Strings are arrays
    This means that you have access to the ‘Range, ‘First, ‘Last, and ‘Length attributes — take advantage of that. (This means when passing arrays as parameters you don’t need a separate length parameter.)
  3. Use subtypes
    One of the biggest disservices Computer Science has done with its emphasis on Object Oriented Programming is emphasizing ‘extensibility’ to the point of neglecting the utility of restricting possible values to the point where many students (and young programmers) think everything must be extensible to be useful restriction is done so routinely in mathematics that I’m shocked that more computer languages don’t have subtyping; how many times did we hear such restricting? “Integers?” “Non-negative Integers?” “Positive Integers?” Those are all subtypes.
    Examples:

    1. Subtype Real is Float’Range; — Assuming Float is an IEEE’s 745 float.
      The above strips the non-normal values from the type, this means you don’t have to deal with NaN or INF in your functions [taking Real], trying to pass those invalid values results in a CONSTRAINT_ERROR exception.
    2. Subtype Safe_Handle is Not Null Handle; — Assuming Handle is some access-type, possibly dealing with the Windows-API.
      If your functions take parameters of Safe_Handle, then you don’t have to do that checking for Null; furthermore, you won’t forget to check when you should.
    3. Subtype Number is String with Dynamic_Predicate => (for all C of Number => C in ‘0’..‘9’);
      The above defines a string of numbers, say for serial- or ID-number; as experienced DB programmers say: if you’re not performing arithmetic on it, it’s a string.
  4. Use tasks
    Ada’s multithreading/concurrency mechanism is part of the language: that means it’s inherently more portable and maintainable than other [OS or library] options. Just remember during an accept block there is no concurrency, this is because you’re in the part [of the rendezvous] where the two [callee and caller] are synchronized.
In short, this will help you avoid trying to write C in Ada, which is much as SB from Comp.Lang.Ada said:
“I’ve had the displeasure of maintaining a lot of legacy code, ostensibly written in Ada, where Integer and Float are used for everything, access values are used instead of out parameters, tasks are avoided in favor of the underlying O/S model, System.Address is used instead of generics, and the list goes on.”
facebooktwittergoogle_plusredditpinterestlinkedinmail