Outputting special characters in Ada

How does one output special characters in Ada? It’s tricky and not as easy as in some languages, probably because output really never was Ada’s purpose. But it can be done using the library Ada.Wide_Text_IO. A small example below shows how to display a small polynomial.

with ada.Text_IO; use ada.Text_IO;
with ada.Integer_Text_IO; use ada.Integer_Text_IO;
with ada.wide_Text_IO; use ada.wide_Text_IO;

procedure z4 is

   xsq : wide_string := "x²";
   xcu : wide_string := "x³";
   plus : wide_string := " + ";
   x1 : integer;

begin
   x1 := 7;
   put(x1, width=>2);
   put(xsq);
   put(plus);
   put_line(xcu);
end z4;

Here is the output:

 7x² + x³

As an alternative (at least on my Mac), you can also do this:

put_line(Integer'Image(x1) & "x² + x³");

A more interesting linked list in Ada

Instead of making assumptions about the type of thing we are storing in the list, let’s instead deal with keeping track of connections in the list, and working with lists in such a way that the information is pre-loaded. Here we define a list as a record containing two fields: a nodeptr, and a variable, size, to keep track of the length of the list. Lists can then be created just by declaring variables of this type. Here are the relevant type definitions:

type node; 
type nodeptr is access node;

type node is record
   word: unbounded_string;
   next: nodeptr := null;
end record;

type list is record
   next: nodeptr := null;
   size: natural := 0;
end record;

Here are some variables the program will need:

nnd : nodeptr;
wrdlst : list;
aword: unbounded_string;

Below is a function make_node() which accepts an unbound_string object, and returns a node with that object stored in it.

function make_node(word: unbounded_string) return nodeptr is
   p: nodeptr;
begin
   p := new node;
   p.word := word;
   return p;
end make_node;

Here are the procedures to add a node to the linked list, insert_node(), and print the list, print_list() :

procedure insert_node(lst1: in out list; p: in nodeptr) is
begin
   p.next := lst1.next;
   lst1.next := p;
   lst1.size := lst1.size + 1;
end insert_node;
procedure print_list(lst1: in list) is
   p: nodeptr;
begin
   p := lst1.next;
   loop
      exit when p = null;
      put(p.word);
      p := p.next;
      put(" ");
   end loop;
end print_list;

And here is the program where all this is inserted into:

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
with Ada.Strings.unbounded; use Ada.Strings.unbounded;
with Ada.Strings.unbounded.text_io; use Ada.Strings.unbounded.text_io;

procedure llist2 is

-- Add in declarations here

-- Add in procedures and functions here

begin
   loop
      put("> ");
      get_line(aword);
      exit when aword = "q";

      nnd := make_node(aword);
      insert_node(wrdlst, nnd);
   end loop;

   put_line("the list as read :");
   print_list(wrdlst);
   put(wrdlst.size);

end llist2;

Here is the program running:

> the
> quick
> lazy
> dog
> jumped
> over
> the
> red
> fox
> q
the list as read :
fox red the over jumped dog lazy quick the           9
An example of what is happening with the first two inputs to the program.

Now if you want to do some housekeeping and clean up the linked list, then you can use the package unchecked_deallocation. Basically add the following to the program to overload the procedure free as a procedure capable of deallocating a nodeptr.

with unchecked_deallocation;

procedure free is new unchecked_deallocation(node, nodeptr);

Then you can build a procedure that erases the linked list:

procedure erase_list(lst1: in out list) is
   nextnode, nodetodelete: nodeptr;
begin
   nextnode := lst1.next;
   while nextnode /= null loop
      nodetodelete := nextnode;
      nextnode := nodetodelete.next;
      free(nodetodelete);
   end loop;
   lst1.size := 0;
   lst1.next := null;
end erase_list;

Deciphering how it works is left to the reader.

A basic linked list of words in Ada (ii)

With a basic linked list that really only builds the list and prints it out, let’s add another function to reverse the list. The procedure below works by extracting the head element repeatedly and building it back in reverse.

procedure reverseList(head: in out list) is
   temp: list := null;
   revl: list := null;
begin
   while head /= null loop
      temp := head;
      head := head.next;
      temp.next := revl;
      revl := temp;
   end loop;
   head := revl;
end reverselist;

What is happening here? The list temp holds the extracted item, while revl holds the reversed list. Line 5 loops through the list. Line 6 extracts a node the list (head). Line 7 sets the list head to the next item, and Line 8 adds the extracted node to the reverse list. Finally Line 9 sets the input list pointer to the reversed list. Finally on Line 11, the input list pointer is set to the reversed list.

For something a little more interesting, here’s the recursive version of the procedure:

function reverseListR(head: in list) return list is
   rest: list;
begin
   if head = null or else head.next = null then
      return head;
   end if;
   rest := reverseListR(head.next);
   head.next.next := head;
   head.next := null;
   return rest;
end reverselistR;

Skip_line through the tulips…

In ADA, like many languages, there are situations where user input is thwarted by the existence of <RETURN> characters. For example, consider the following program which reads an integer following by a string.

with ada.Text_IO; use ada.Text_IO;
with ada.Integer_Text_IO; use ada.Integer_Text_IO;
with ada.Unchecked_Deallocation;

procedure skip is
   a : integer;
   s : string(1..4);

begin
   get(a);
   s := get_line;
   put(a);
   put_line(s);
end skip;

When this program is run, we get the following after entering 1291 as the integer:

% ./skip
1291

raised CONSTRAINT_ERROR : skip.adb:11 length check failed

This is caused in part because Ada expects a string of length 4, and what it got was the <RETURN> key pressed after entering 1291. Here is where we use skip_line, which gets ready to read the line after the current line, discarding any text on the current line that hasn’t been read. So if we add the procedure skip_line in the code will work fine:

with ada.Text_IO; use ada.Text_IO;
with ada.Integer_Text_IO; use ada.Integer_Text_IO;

procedure skip is
   a : integer;
   s : string(1..4);

begin
   get(a);
   skip_line;
   s := get_line;
   put(a);
   put_line(s);
end skip;

A basic linked list of words in Ada (i)

Amazingly linked lists in Ada are no different than they are in C, except actually they may be somewhat easier to interpret. You can find out about basic pointers in Ada here (although in Ada they are termed “access values”.

Let’s jump right in and create a linked list, in this case to store a series of words. We will start out with some declarations.

type node;

type list is access node;

type node is record
   word: unbounded_string;
   next: list;
end record;

head: list;

For some people the declaration of node on Line 1 will seem a little odd, but it helps resolve the circularity of the definitions. The declaration on Line 3 refers to node, and the declaration of node (Line 5) refers to list. Line 1 is somewhat of an incomplete declaration as it simply tells the compiler that node is the name of a type of some sort so that the name can be used on Line 3. Nothing else very exciting here – Lines 5-8 declares node to be a record containing the data (word), and the pointer to the next node (next).

Next, we’ll create a procedure buildList() which will create the list, adding one word at a time. There isn’t anything magical here, just a stock-standard list implementation. Remember, lists are LIFO structures, so the last term that gets added is the head of the list.

procedure buildList(head: in out list; aword: in unbounded_string) is
   newNode: list;
begin
   newNode := new node'(word=>aword, next=>null);
   newNode.next := head;
   head := newNode;
end buildList;

On Line 4, the new node is created using new, which takes a free block of memory from the heap and reserves it for use as a node type variable. At the same time it assigns the string (aword), to word, and sets next to null. Lines 5 and 6 insert the new item into the list.

Next, we are going to add a procedure, printList(), to print the list.

procedure printList(head: in list) is
   scanPtr: list;
begin
   scanPtr := head;
   loop
      exit when scanPtr = null;
      put(scanPtr.word);
      scanPtr := scanPtr.next;
      put(" ");
   end loop;
end printList;

The variable scanPtr is a node used to scan the list. Line 5 is the exit strategy for the loop, exiting when the list becomes empty. Line 7 prints the word, and line 8 goes to the next word. Now we need some code to actually run the program.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Strings.unbounded; use Ada.Strings.unbounded;
with Ada.Strings.unbounded.text_io; use Ada.Strings.unbounded.text_io;

procedure linkedlist is

   -- code from above goes here
   aword: unbounded_string;

begin
   loop
      put("> ");
      get_line(aword);
      exit when aword = "q";
      buildList(head, aword);
   end loop;

   put_line("the list as read :");
   printList(head);
   new_line;

end linkedlist;

Nothing special here. Lines 11-16 provides a loop, prompting the user for words, and building the list until “q” is entered, terminating the input. Here is the programming running:

> the
> cat
> sat
> on
> the
> hat
> q
the list as read :
hat the on sat cat the

The rise of the mediocre programmer

It’s funny that people are scared of the “rise of the machines”. We are nowhere near the point where machines will become sentient, partially because we don’t even understand our own sentience. People should be more scared of the one thing that seems to be on the rise – the mediocre programmer.

Sometimes I’m sure people wonder why some software is so bad. It’s actually not hard to find an answer – mediocre software engineers, or programmers. But they get degrees you ask? Yes, they do, but that isn’t truthfully very meaningful. Many people get degrees, it’s actually whether they have the ability to actually design and implement software. Can they solve a problem? Do they understand usability (from the perspective of the user, not the developer)? Do they understand the language the software is being written in? Some people get through CS degrees by the skin of their teeth, and aren’t really that good at programming. Ask yourself if you would be okay with seeing a doctor that passed their medical degree with a 55% average.

In truth over the years I have seen a lot of mediocre work coming from CS students. I once had a student submit a program that was more than 50,000 lines of code (LOC) long, for a problem that could be solved with a program containing 300-400 LOC. Even programs that “meet the criteria” are often awful to read, and full of extraneous code. It almost seems like the programs are cobbled together from code snippets on the internet. The lack of style tends to obfuscate the code even further. It’s just terrible, like they have little or no respect for the code they are writing.

Don’t get me wrong, other professions suffer from the same malady – engineering for example. Some engineers these days have no clue how to actually build things. It’s not their fault, standards of doing tangible things have vaporized in most institutes for higher education, it’s just a reality. Most engineering students never really build anything substantial, just like most computer science students never build large scale software (although to be fair, with secondary schools not teaching shop anymore because it’s “too dangerous”, we have a generation that’s generally clueless when it comes to the most basic manual-work type things – and we wonder why we don’t have enough plumbers?) . If we did, it might weed out some of the individuals who produce bad software.

Oh, you’ll say I’m being mean. Sure, if that’s what you’d like to call it, but the reality is that I don’t want mediocre software developers or programmers building anything that could impact people’s lives. Not everyone should be a computer scientist, as not everyone should be a doctor – if you don’t have the talent you will do a mediocre job.

The mediocre programmer has a number of traits.

  • They tend to sling together code with little or no credence to the overall design. They are often shocked their system doesn’t always work (because they can’t be bothered testing it).
  • They consider writing HTML is programming (shock-horror, it isn’t).
  • They code “on-the-fly” with little or no design, and next to no concept of maintainability.
  • Their code is messy and unreadable – and you thought we got rid of spaghetti code.
  • They barely document anything, because they really don’t know what’s going on.
  • Their code is often littered with bugs, some which may not manifest for a while – but they are there, waiting. A fix of one bug inherently introduces another.
  • A certain level of arrogance, a belief that their code is great, and a belief that others have less intelligence than they do.

In truth mediocre programmers may have little interest in programming beyond the $, have some difficulty in understanding programming concepts, and a superficial understanding of languages (you can’t learn a language from reading Stackoverflow) . The code they produce is often subpar, and they neither know why their code works, nor why it doesn’t.

Image processing in Ada (v) – the main program

The last piece of the program binds the packages together. Here we read in the filename using the procedure getfilename(), and the main program which provides a menu based system to access all the subprograms in the prorgam.

with ada.Text_IO; use Ada.Text_IO;
with ada.strings.unbounded; use ada.strings.unbounded;
with ada.strings.unbounded.Text_IO; use ada.strings.unbounded.Text_IO;
with ada.directories; use ada.directories;
with imagestr; use imagestr;
with imagepgm; use imagepgm;
with imageprocess; use imageprocess;

procedure image is
   fnameI, fnameO : unbounded_string;
   img0 : imagep;
   opt : character;
   buf : unbounded_string;
   lb,ub : float;
   bit : integer;

   -- Procedure to
   procedure getfilename(fname: out unbounded_string; t: in character) is
      res : character;
      buf : unbounded_string;
   begin
      if t = 'r' then
         loop
            put_line("Enter PGM filename: ");
            get_line(fname);
            exit when exists(to_string(fname));
         end loop;
      elsif t = 'w' then
         loop
            put_line("Enter PGM filename: ");
            get_line(fname);
            if exists(to_string(fname)) then
               put_line("Overwrite? (y/n):");
               buf := get_line;
               res := element(buf,1);
               if res = 'y' then
                  exit;
               end if;
            else
               exit;
            end if;
         end loop;
      end if;
   end getfilename;

begin

   loop
      put_line("        Image Processing");
      new_line;
      put_line(" 1. Read in PGM image from file");
      put_line(" 2. Apply image invertion");
      put_line(" 3. Apply LOG function");
      put_line(" 4. Apply contrast stretching");
      put_line(" 5. Apply histogram equalization");
      put_line(" 6. Write PGM image to file");
      put_line(" 7. Apply reduce grays");
      put_line(" 8. Quit");
      put_line(" Choice? ");
      buf := get_line;
      opt := element(buf,1);

      case opt is
         when '1' => getfilename(fnameI,'r');
                     readpgm(img0,fnameI);
         when '2' => imageinv(img0);
         when '3' => imagelog(img0);
         when '4' => put_line("lower bound:");
                     buf := get_line;
                     lb := float'value(to_string(buf));
                     put_line("upper bound:");
                     buf := get_line;
                     ub := float'value(to_string(buf));
                     imagestretch(img0,lb,ub);
         when '5' => histequal(img0);
         when '6' => getfilename(fnameO,'w');
                     writepgm(img0,fnameO);
         when '7' => put_line("bit value (1-8):");
                     buf := get_line;
                     bit := integer'value(to_string(buf));
                     reducegrays(img0,bit);
         when others => exit;
      end case;

   end loop;

end image;

The getfilename(), will get a user-input name for a file, based on whether it is for reading or writing. For a file that is to be read, the procedure will continue looping until a valid filename is input. For writing, if the file already exists the user is prompted to indicate whether or not they want the file overwritten.

Image processing in Ada (iv) – more functionality

So we’ve added the basic functionality, but with some more graft we can add a couple of more functions. The first reduces the amount of gray levels in an image, and the second performs histogram equalization on the image.

These can be easily added to the imageprocess package.

Reducing grays

First let’s deal with the procedure reducegrays(). All this function does is effectively reduce the number of grays in an image to new amount. For example a typical grayscale image is 8-bit, or 256 levels of gray. To reduce the number of grays we effectively have to compress a number of bins into a single bin. Consider the example shown in the figure below. If we convert an 8-bit image to a 4-bit image we are reducing the number of graylevels from 256 to just 16. So that means that graylevels 0-15 will be compressed into 0, 16-31 will be compressed into 1,…, and finally 240-255 compressed into 15. So effectively we are left with 16 levels of gray from 0-15. However if we tried to view this, we would get almost a black image, as the graylevels 0-15 are extremely dark (unless it is viewed as a 4-bit image). To mitigate this, we redistribute the grays in an 8-bit fashion (that’s why the histogram looks so sparse). In the figure you can see than pixels with values 32-47 in the original image become 2, which is then redistributed. Now the images look very similar because the human visual system can only distinguish about 32 levels of gray.

procedure reducegrays(img: in out imagep; bits: in integer) is
   levels, compf, temp : float;
begin
   levels := 2.0**float(bits);
   compf := 256.0 / levels;
   for i in 1..img.dy loop
      for j in 1..img.dx loop
         temp := float(img.pixel(i,j))/255.0*levels;
         img.pixel(i,j) := integer(float'floor(temp) * compf);
      end loop;
   end loop;
end;

On Line 8 of the procedure a pixel is divided by 255, and then multiplied by the new grayscale value (which is calculated as bits2). So from the example, a the new value for a pixel with grayscale value 35 would be: 35/255.0×16 = 2.19. Line 9, then redistributes this value. The floor of 2.19 is 2, which when multiplied by compf (16) is 32. Similarly value 47 gives 47/255.0×16 = 2.94, but the floor of 2.94 is again 2.

Histogram equalization

Histogram equalization (HE) is a popular technique for improving the appearance of an image. Its function is similar to that of histogram stretching. Histogram equalization is a technique where the histogram of the resultant image is a flat as possible. It is based on the probability distribution of the grayscale values in the image. The process consists of a series of steps:

  1. Calculate the histogram of the grayscale image. The histogram of an 8-bit image is a 256 element array, with each element (or bin) representing the number of times a particular grayscale appears in the image. For example H[256] is the histogram, then if the value of H[1] is 197, then it means that 197 pixels in the image have the grayscale value 0.
  2. Calculate the probability density function(PDF) of the histogram by dividing each bin in the histogram by the total number of pixels in the image.
  3. Calculate the cumulative histogram (CH). This is a 256-element array where each value is the cumulative value from the PDF. For example CH[1] = PDF[1], CH[2] = CH[1] + PDF[2], CH[3] = CH[2] + PDF[3] etc.
  4. Multiply the CH by 255 and round.
  5. Map the new grayscale values from the results of Step (4) using a one-to-one correspondence.

The illustration below shows the mapping of a pixel at location [378,345] in an image using the histogram equalization algorithm. The initial value of the pixel is 119, however using the mapping function calculated, the new value is 188 (making it brighter).

Here is the first procedure, makehist(), to build a histogram.

procedure makehist(img: in imagep; hist: out histogram) is
   begin
      hist := (others => 0);
      for i in 1..img.dy loop
         for j in 1..img.dx loop
            hist(img.pixel(i,j)) := hist(img.pixel(i,j)) + 1;
         end loop;
      end loop;
   end;

It uses a type also defined in the imagestr package:

type histogram is array(0..255) of integer;

Now the main procedure to perform the histogram equalization:

procedure histequal(img: in out imagep) is
   h,fL : histogram;
   totalP : integer;
   pdf,chst : array(0..255) of float;
begin
   makehist(img,h);

   totalP := img.dx * img.dy;
   for i in 0..255 loop
      pdf(i) := float(h(i)) / float(totalP);
   end loop;

   chst(0) := pdf(0);
   for i in 1..255 loop
      chst(i) := chst(i-1) + pdf(i);
   end loop;

   for i in 0..255 loop
      fL(i) := integer(chst(i) * 255.0);
   end loop;

   for i in 1..img.dy loop
      for j in 1..img.dx loop
         img.pixel(i,j) := fL(img.pixel(i,j));
      end loop;
   end loop;
end;

Here is the rundown of what happens:

  • Line 6: Calculate the histogram of the image.
  • Lines 8-11: Calculate the probability density function of the histogram, i.e.frequency of a pixel / (total number of pixels).
  • Lines 13-16: Derive the cumulative histogram (cumulative distribution probability).
  • Lines 18-20: Derive the remapping function.
  • Lines 22-26: Remap the image.

The world of useless things

“Sir, We must beware of needless innovations, especially when guided by logic.”

Sir Winston Churchill, (Dec.1942)

There are some products that shouldn’t exist, either because they are just useless products, or because they make a simple task more complicated. Often these devices have appeared in kitchens, as tools to “make our lives easier”. This of course is a thinly veiled way of taking away the skills humans once had. A great example are the series of banana dividers shown below, devices for evenly dividing a banana. Why bother? Because humans have become that incompetent that they can’t use a knife? Maybe. But these are things nobody needs – you don’t need you’re banana cut into pieces that are exactly the same. In fact the banana is likely the best fruit from a usability point-of-view.

Below are some more useless things. A weird way of removing the skin from fruit and vegetables (because peelers are hard to use?), a way to make cubed eggs (why oh why, when a egg already comes in a container, the aesthetics of cubes?), and an avocado slicer (need I say more). The reality is that many of these things actually detract from the skills humans learn.

Humans don’t need useless things, regardless of whether they are everyday things, or software.