8  Packages

It is time to get serious about code organization separating Specifications from Implementation. Though we did not dwell upon it, we encountered a package hex in the dump projectlet. In addition, the predefined language environment is made available to us primarily as packages.

In this chapter, a package to support CSV or Comma Separated Variable files is developed. The package is then applied to load and utilize a foods database leveraging the work done in an earlier chapter.

8.1 Learning Objectives

  • Code Organization by packages

  • Information hiding

  • Richer Data Structures

  • Container support in the Predefined Library

8.2 Projectlet - csv package

The separation of the specification of a feature from the details of implementation is central to the notion of packages’s. By stating the interface specifications, the user or application is restricted in the interaction with the facility. This affords the freedom to change the implementation e.g. more optimally or handling edge cases without imposing any changes on the user. In the following, a facility to support CSV files is specified:

~/bin/codemd ../../toolkit/adalib/src/csv.ads -x Spec -l
0007 | package Csv is
0008 |    Debug : Boolean := True;
0009 |    Duplicate_Column : exception;
0010 |    type File_Object_Type is limited private;
0011 |    type File_Type is access File_Object_Type;
0012 | 
0013 |    subtype Field_Count_Type is Integer range 1 .. 255;
0014 |    package String_Vectors_Pkg is new Ada.Containers.Indefinite_Vectors
0015 |      (Index_Type => Field_Count_Type, Element_Type => String);
0016 | 

...

0034 | private
0035 |    type File_Object_Type is limited record
0036 |       Session      : GNAT.AWK.Session_Type;
0037 |       No_Columns   : Integer := 0;
0038 |       Current_Line : Integer := -1;
0039 |       Field_Names  : String_Vectors_Pkg.Vector;
0040 |    end record;
0041 | end Csv;

In the above, the variable Debug is public and is thus can be read or even written by the user. Presumably the implementation uses this variable in some way but not specified.

An exception, a data type and a pointer to this data type are also declared. The application can then access these data types. However note that the File_Object_Type is designated private implying the details ie elements of this record are not accessible by the user.

The type File_Type is akin to a pointer though in Ada unlike C, this pointer can only point an object of tye type File_Object_Type. By default variables of this type will contain the value NULL which does not point to any object.

We also create a package String_Vectors_Pkg based on the template package Indefinite_Vectors one of quite a few container data structures supported by the language. The key data type from this package Vector is used to store the column names of the file.

Encountering for the first time a record data structure is similar to C. Of the members the Session draws from a library package GNAT.awk which is a programmatic interface supporting the awk semantics.

The interface specification or the contract provides the means to interact with the library:

~/bin/codemd ../../toolkit/adalib/src/csv.ads -x Interface -l
0019 |    function Open
0020 |      (Name : String; Separator : String; FieldNames : Boolean := True)
0021 |       return File_Type;
0022 |    procedure Close (File : in File_Type);
0023 |    function No_Columns (File : File_Type) return Integer;
0024 |    function Field_Name (File : File_Type; Column : Integer) return String;
0025 |    procedure Get_Line (File : in File_Type);
0026 |    function Line_No (File : File_Type) return Integer;
0027 |    function End_Of_File (File : File_Type) return Boolean;
0028 |    function Field (File : File_Type; Column : Integer) return String;
0029 |    function Field (File : File_Type; Name : String) return String;
0030 |    procedure Set_Names (file : File_Type; names : String_Vectors_Pkg.Vector);
0031 |    function Names (file : File_Type) return String_Vectors_Pkg.Vector;

The segments above should be sufficient to write an application using the facility as shown in the following fragment.

~/bin/codemd ../../toolkit/examples/meals/src/foods.adb -x CSVLoad -l
0088 |    function Load (mfn : String) return Meal_Type is
0089 |       use Ada.Strings.Unbounded;
0090 |       result   : Meal_TYpe;
0091 |       mealfile : csv.File_Type;
0092 |       dish     : Dish_Type;
0093 |    begin
0094 |       mealfile := csv.Open (mfn, ";");
0095 |       while not csv.End_Of_File (mealfile) loop
0096 |          csv.Get_Line (mealfile);
0097 |          dish.Name     := To_Unbounded_String (csv.Field (mealfile, 1));
0098 |          dish.Servings := Servings_Type'Value (csv.Field (mealfile, 2));
0099 |          result.Append (dish);
0100 |       end loop;
0101 |       csv.close (mealfile);
0102 |       return result;
0103 |    end Load;

The CSV file is opened, closed and lines Get_Lined very similar to any text file. Additional functions retrieve the fields from each line. Field values are retrieved by their column number and returned as Strings. The user of CSV performs appropriate data conversions. The meals projectlet below puts the rest of the operation in context.

8.3 Projectlet: meals

In this projectlet, a food database is maintained as a CSV file. The database contains name, Glycemic Index, Glycemic Load, Calories and Protein content of food items. Once we have this, we can compute the total Calories of a meal containing the list of items along with the number of servings included. This could potentially be useful to determine Insulin bolus dosage for example for diabetics.

8.3.1 Requirements

  • Primary Input is a CSV file detailing a meal. Each line indicates a food item and the number of servings of the item.

  • Goal is to compute the total number of calories. This is done by referring to a foods database.

  • Foods Database is a CSV file providing a list of food items, including their name, Serving Size, Calories per serving and other relevant information.

8.3.2 Example Usage

~/bin/meals ../../toolkit/examples/meals/breakfast.csv ../../toolkit/examples/meals/foods.csv
meals
Number of Fields  5
Field Name
Field Servings
Field Calories
Field Carbs
Field  
Number of Fields  2
Field Name 
Field  Servings 
The meal is
Item Eggs   2 servings
Item OJ   1 servings
Item Coffee   1 servings
Item Toast   2 servings
Total Calories is  970

8.3.3 Detailed Design

The meal contents are structured as:

~/bin/codemd ../../toolkit/examples/meals/src/foods.ads -x Dish -l
0070 |    type Servings_Type is range 0 .. 8;
0071 |    type Dish_Type is record
0072 |       Name     : Ada.Strings.Unbounded.Unbounded_String;
0073 |       Servings : Servings_Type;
0074 |    end record;

A meal then is a collection of these.

Similarly the Foods Database items are structured as:

~/bin/codemd ../../toolkit/examples/meals/src/foods.ads -x FoodItem -l
0033 |    type Food_Item_Type is record
0034 |       Name        : Ada.Strings.Unbounded.Unbounded_String;
0035 |       ServingSize : Ada.Strings.Unbounded.Unbounded_String :=
0036 |         Ada.Strings.Unbounded.Null_Unbounded_String;
0037 |       Calories    : CaloriesType := CaloriesType'First;
0038 |       Proteins    : ProteinsType := ProteinsType'First;
0039 |       gi          : Glycemic_Index;
0040 |       gl          : Glycemic_Load;
0041 |       carbs       : CarbohydratesPerServing;
0042 |    end record;
0043 |    function Equal( Left, Right : Food_Item_Type ) return boolean ;

The foods database is a collection of these food items.

Collections

For both the foods database and each meal, a simple data structure like an array will be a good starting point. However, with the strong typing constraints, it becomes a bit challenging to size the arrays appropriately. A dynamically growing datastructure might be more suitable. Ada incorporate a rich library of Containers to choose from. The needs of this projectlet can be met by Ada.Containers.Vectors.

The container library provides templates of data structures or in Ada terminology packages from which application specific facilities are created by instantiation - a technical term for marrying the library specification with a specific data structure like so:

~/bin/codemd ../../toolkit/examples/meals/src/foods.ads -x Database -l
0047 |    package FoodsDatabase_Pkg is new Ada.Containers.Vectors
0048 |      (Index_Type => Natural, Element_Type => Food_Item_Type , "=" => Equal);
0049 |    subtype FoodsDatabase_Type is FoodsDatabase_Pkg.Vector;
0050 |    function Load (filename : String) return FoodsDatabase_Type;
0051 |    procedure Show (fi : Food_Item_Type);
0052 |    procedure Show (db : FoodsDatabase_Type);
0053 |    function Find(db : FoodsDatabase_Type ; item : String ) return Food_Item_Type ;

In this instance, a custom package has been designed - FoodsDatabase_Pkg which in turn created a datatype FoodsDatabase_Pkg.Vector which serves as the database of foods. Further API can be specified around these specifications as illustrated. The key function is the Find which returns the full details from the database based on the item name.

Using the same generic package, a distinct package is created for the meal itself like so:

~/bin/codemd ../../toolkit/examples/meals/src/foods.ads -x Meals -l
0078 |    package Dishes_Pkg is new Ada.Containers.Vectors
0079 |      (Index_Type => Natural, Element_Type => Dish_Type);
0080 |    subtype Meal_Type is Dishes_Pkg.Vector;
0081 |    function Load (mfn : String) return Meal_Type;
0082 |    procedure Add
0083 |      (meal : in out Meal_Type; item : String; servings : Servings_Type);
0084 |    procedure Show (meal : Meal_Type);
0085 | 
0086 |    function Calories (db : FoodsDatabase_Type ; meal : Meal_Type) return CaloriesType;

The main requirement of the program namely the computation of the total caloric content of a meal is to be found in the body of the package foods. This body then is the home for the implementation of all the procedures and functions comprising the package.

~/bin/codemd ../../toolkit/examples/meals/src/foods.adb -x Calories -l
0135 |   function Calories (db : FoodsDatabase_Type ; dish : Dish_TYpe) return CaloriesType is
0136 |    use FoodsDatabase_Pkg;
0137 |    fi : Food_Item_Type ;
0138 |    found : FoodsDatabase_Pkg.Cursor ;
0139 |   begin
0140 |    fi.Name := dish.Name ;
0141 |    found := FoodsDatabase_Pkg.Find(db,fi);
0142 |    if found = FoodsDatabase_Pkg.No_Element
0143 |    then
0144 |       raise DatabaseError ;
0145 |    end if;
0146 |    return FoodsDatabase_Pkg.Element(found).Calories ;
0147 |   end Calories ;
0148 | 
0149 |    function Calories (db : FoodsDatabase_Type ; meal : Meal_TYpe) return CaloriesType is
0150 |       use Dishes_Pkg ;
0151 |       result : CaloriesType := 0 ;
0152 |       ptr  : Dishes_Pkg.Cursor;
0153 |    begin
0154 |       ptr := meal.First;
0155 |       while ptr /= Dishes_Pkg.No_Element loop
0156 |          result := result + CaloriesType( Float(Dishes_pkg.Element(ptr).Servings) * 
0157 |                                           Float(Calories(db,Dishes_Pkg.Element(ptr))));
0158 |          ptr := Dishes_Pkg.Next(ptr);
0159 |       end loop ;
0160 |       return result ;
0161 |    end Calories;

8.4 Refinement

The sample implementation provided is sufficient for pedagogic purposes but is not robust enough which is left as an exercise. Some potential improvements that will belong in a real application are:

  • Case insensitivity for food item names

  • Robustness in handling malformed, corrupted database files

  • Foods Database file location. Currently the default specification does not include any directory specification for the database file or the meals file itself

8.5 Stretch

  1. Design a Project activities notebook. Over the day, one might be working on several projects and related activities. A simple command to keep track of activies of the day (or week) and be able to summarize them can be built patterned after this project.

  2. Enhance CSV to support generation and create a table of computed formulae that can be fed into gnuplot for plotting. For example individual meals of the entire week may be summarized and the source of calories can be plotted as a histogram.

8.6 Sample Implementation

Repository: toolkit
Example: examples/foods