ciceron
ciceron

Reputation: 586

Best practice for implementing in Ada (2005 or 2012) an equivalent of the java finalize block

Java has the finalize block which allows to execute some statements after a block is left (executed even if an exception is raised). Example:

try {
  ...
} catch (Exception e) {
  ...
} finally {
  ... // any code here
}

Ada has the controlled objects which allows to implement a Finalize operation but there is no finalize block equivalent as in java. This is useful for logging, closing files, transactions and so on (without having to create a specific tagged type for each possible block).

  1. How would you implement such finalize block in Ada 2005 (while keeping the code readable)?
  2. Are there plans in Ada 2012 to allow executing any finalization code easily?

Upvotes: 7

Views: 2098

Answers (6)

NiGHTS
NiGHTS

Reputation: 23

Lets put this problem into perspective.

In programming theory there exists concepts of an object's create & destroy and a procedure's try & finally. Both ultimately deal with resource management but they pivot on different things.

  • With objects we pivot on the potentially long-living tree of objects and variables referenced by the object pointer.
  • With procedures we pivot on the usually temporary objects and variables existing in the scope of the procedure call (the notable exception is main)

Now, depending on the procedure we might need to spawn a series of resources which, if the procedure is interrupted by a fatal error, must rollback all spawned resources in reverse order. Quite often this is easiest achieved by creating objects dedicated to managing their respective resource. Ada.Finalization becomes quite useful here.

But this can be overkill, and there can be arguments made against this technique on a case-by-case basis. So if we are interested in self-containing resource management to a procedure and making use of a C++-style finally keyword, consider the following.

I have often initially been pleased by the promise of convenience using finally only to be disappointed by how complicated the code can turn out after making use of it. Nesting is required for complex rollback operations, and many times the process is not linear, requiring logic to decide exactly how to roll back depending on how far we made it through the initialization. The nested block structure corners you to use linear logic. Thus when you need something more complicated then you are seriously considering making use of goto.

I've gone through this enough times to realize that, as appetizing as OOP-style finalize can be with what I mentioned, a true try & finally can be achieved in Ada without all the headaches as demonstrated in the following example. Note, it's far from perfect, but I think the good outweighs the bad with this strategy. What I especially like about this strategy is how explicit the finalize process becomes, all steps labeled and checked against Ada's type system, well organized and easy to manage. The C++-style try & finally scatters this kind of code, and Ada's Finalize hide's it and makes it harder to control the higher-level order of fail-over logic.

procedure Proc is
    
    type Init_Stages_All is (Do_Null, Do_Place, Do_Pour, Do_Drink);
    subtype Init_Stages is Init_Stages_All range Do_Place .. Do_Drink;
    Init_Stage : Init_Stages_All := Do_Null;
    
    procedure Initialize_Next is
    begin
        Init_Stage := Init_Stages_All'Succ(Init_Stage);
        case Init_Stage is
            when Do_Place => ...
            when Do_Pour => ...
            when Do_Drink => ...
            when Do_Null => null;
        end case;
    end Initialize_Next;

    procedure Finally is
    begin
        for Deinit_Stage in reverse Init_Stage .. Init_Stages'Last loop
            case Deinit_Stage is
                when Do_Place => ...
                when Do_Pour => ...
                when Do_Drink => ...
            end case;
        end loop;
    end Finally;

begin
    Initialize_Next; -- Do_Place
    ...
    Initialize_Next; -- Do_Pour
    ...
    Initialize_Next; -- Do_Drink
    ...
    Finally;

exception
    when E : others =>
        ...
        Finally;
        raise;

end Proc;

On a final note, this strategy also makes it easier to deal with finalization exceptions. I'd suggest creating a procedure in Finally that is called when an exception is raised and recursively call Finally by manipulating the Init_Stage as well as interject any additional fail-over logic there.

Upvotes: 0

T.E.D.
T.E.D.

Reputation: 44804

Just thought of another answer. Its a bit heavy (and perhaps more trouble than it is worth). But it would give you something that looks a bit like your old finalize block

The idea would be to put your "finalizable" code in a task. You cannot leave the scope a task is declared in until the task terminates. So you could put your work code in the task and your "finally" code just outside of the scope the task is defined in. The parent task would sit there and wait for the work task to end (one way or another), and then it would run the "finally" code no matter how it ended. The drawback is that if the task throws an exception, it will stop at the task. So you still don't quite get the behavior that you can throw an exception and it will propagate out automatically while the "finalize" code gets run. You could maybe get that behavior back by adding a rendezvous and a second task (that's the problem with tasks. They are like potato chips...you always need one more).

procedure Finalized is
begin
    declare
        task Worker is end Worker;
        task body Worker is begin
            --// Working code in here. Can throw exceptions or whatever. 
            --// Does not matter.
        end Worker;
    begin
    end;

    --// If we get here, we know the task finished somehow (for good or ill)
    --// so the finalization code goes here.

end Finalized;

It seems to me there might be a way to do something like this with protected objects too. I'll leave that one for others to figure out.

Upvotes: 1

user571138
user571138

Reputation:

Assuming you have understood the difference between ada.finalization and a finalize block in java, i would do something similar to the following, which should have the same effect.

procedure x is 
begin

  -- some code
  begin
    -- more code (your try)
  exception 
    -- handle exception if necessary (caught exception)
  end;
  -- yet more code which is executed regardless of any exception handling.

end x;

Upvotes: 2

Simon Wright
Simon Wright

Reputation: 25511

I believe this code will do what you ask; it successfully prints out 42 with the present raise or with return. It's an implementation of T.E.D's suggestion.

Tested with GCC 4.5.0 on Mac OS X, Darwin 10.6.0.

with Ada.Finalization;
package Finally is

   --  Calls Callee on deletion.
   type Caller (Callee : not null access procedure)
      is new Ada.Finalization.Limited_Controlled with private;

private

   type Caller (Callee : not null access procedure)
      is new Ada.Finalization.Limited_Controlled with null record;

   procedure Finalize (Object : in out Caller);

end Finally;


package body Finally is

   procedure Finalize (Object : in out Caller)
   is
   begin
      Object.Callee.all;
   end Finalize;

end Finally;


with Ada.Text_IO; use Ada.Text_IO;
with Finally;
procedure Finally_Demo is
begin

   declare

      X : Integer := 21;

      --  The cleanup procedure, to be executed when this block is left
      procedure F
      is
      begin
         Put_Line ("X is " & Integer'Image (X));
      end F;

      --  The controlled object, whose deletion will execute F
      F_Caller : Finally.Caller (F'Access);

   begin

      X := 42;

      raise Constraint_Error;

   end;

end Finally_Demo;

Upvotes: 2

T.E.D.
T.E.D.

Reputation: 44804

Marc C has the right approach for trying to emulate that in straight-line procedural code.

However, IMHO that structure is mostly a way to hack around Java's OO system, for those who want one of the structural benifits of OO in old-fashioned procedural programming. Even in Java you are almost always better off creating a proper class instead.

So I don't think it is too much of a stretch to say that the proper way to get that functionality in Ada would be to make a proper object, and make your object a child of Ada.Finalization.Controlled.

If you don't want to bother with creating an actual object, you could just create a dummy one, put your finalization code in it, and declare it on the stack at the top of the block you want it run for. The drawback to that is that controlled types themselves (as least the last time I used them) have to be declared at package-level scope. When that's the case, you'd be unable to put direct references to lower-declared objects in them. They claimed they were going to fix that in future language revision, but I haven't tried it recently to see if they did.

Upvotes: 1

Marc C
Marc C

Reputation: 8522

As Adrien mentions in the comment, Finalize is more analogous to a destructor.

To get something approximating an exception/final sequence you can do something along these lines (WARNING, not compiled, just typed--we'll work out any errors together :-) See also the Exceptions section of the Ada RM.

with Ada.Exceptions;  use Ada.Exceptions;

procedure Do_Something is

   -- Variables and what-not...

   -- In case you have an exception and want to reraise it after you've done
   -- the 'final' processing.
   Exception_Caught : Exception_Occurrence := Null_Occurrence;

begin
   -- You can have some statements, like initializations, here that will not
   -- raise exceptions.  But you don't have to, it can all just go in the
   -- following block. However you want to do it...

   declare
      -- If you need to declare some entities local to a block, put those here.
      -- If not, just omit this declare section.  Be aware, though, that if
      -- you initialize something in here and it raises an exception, the
      -- block's exception handler will not catch it. Such an exception will
      -- propagate out of the whole procedure (unless it has an outermost
      -- exception handler) because you're _not_ in the block's scope yet.

   begin
      -- Main processing that might raise an exception

      ...

   exception
      when E : others =>
         -- Handle any exception that's raised.  If there are specific
         -- exceptions that can be raised, they should be explicitly
         -- handled prior to this catch-all 'others' one.

         -- Save the exception occurrence, i.e. make a copy of it that can
         -- be reraised in the 'Final' section if needed.  (If you want to
         -- reraise for a specific exception, do this in those handlers as
         -- well.
         Save_Occurrence(Exception_Caught, E);

   end;

   -- Final processing. Everything from here to the end of the procedure is
   -- executed regardless of whether an exception was raised in the above
   -- block.  By it including an others handler, it ensured that no exception
   -- will propagate out of this procedure without hitting this 'Final' code.

   -- If an exception was raised and needs to be propagated:
   if Exception_Caught /= Null_Occurrence then
      Reraise_Exception(Exception_Caught);
   end if;

end Do_Something;

Upvotes: 3

Related Questions