Option and Result types

Hi!

I’m looking at the error handling story of Ada, which seems to be pretty much all-exception based (and “unchecked” in Java parlance).

An alternative to exceptions which I’ve learned to appreciate is the use of option/maybe types or result/either types. Because it forces the caller to check the possible error state, which is carried over by the type. It’s also possible to build “fallible pipelines” with it, a bit like legos.

These types have been incorporated into the latest C++ standard: Functional exception-less error handling with C++23's optional and expected - C++ Team Blog

So I’m wondering if there’s been any talk about this style of error handling in the Ada community? And dealing with null related errors in general.

2 Likes

Option types can be modeled in Ada as a tagged type (class) or discriminated record. See the optional Alire crate for an example of the former.

Many embedded applications use pragma Restriction (No_Exception_Propagation);, which causes the application to “crash” if any exception is raised. In these environments, a common pattern is to provide an out argument for procedure status. Example:

type Status_Type is (Ok, Bus_Error, Value_Error);
procedure Bus_Write (Data : Data_Type; Status : out Status_Type);

With SPARK_Mode => On, gnatprove’s “silver” mode will verify Absence of Runtime Exceptions (AoRTE) at compile/verification time rather than runtime. gnatprove will also catch Constraint_Errors raised by things like out of range integer assignments in this mode.

So yes, the full featured Ada language has runtime exceptions, but there are several options for avoiding or eliminating them in your application and with static verification.

1 Like

for simple types, I generally roll my own using a Variant record:

type Option(Valid : Boolean := False) is record
   case Valid is
      when False => null; -- nothing in the object when false
      when True  => Value : My_Type; -- Value here when true
   end case;
end record;

Then you can just return one of those:

   Thing : Option := Some_Function;
begin
   if Thing.Valid then
      -- do stuff with Thing.Value
   end if;

You can use enumerations for error codes in place of the boolean as well:

type Error is (No_Error, Some_Error, Bad_Error);
type Option(Status : Error:= Bad_Error) is record
   case Valid is
      when No_Error => Value : My_Type; -- Value here when in error
      when others   => null; -- nothing in the object when in error
   end case;
end record;

For unconstrained types and more complex types, the optional library linked above is pretty good.

4 Likes

Unfortunately when a returned value is volatile then I believe the variant makes it incompatible with SPARK mode. You can mirror the value into a non-volatile type but then you have to ask yourself if it is worth it.

In general null and access types aren’t required much in Ada and access types can be designated as not null.

Yep, for volatile objects I generally return a non volatile copy rather than the object itself. I found in my experience it is best to limit volatile accesses to as few as possible and localized as much as possible to allow the compiler to optimize elsewhere. Anytime a type or object passed into or out of a function/procedure is volatile, I mark that as a code smell (not out of the box bad, but worthy of very focused review).

I used to do the same but I think I now disagree because there is little to no performance issue and when the svd file is corrected which happens a lot (or swapped out for a similar chip) then the maintainability is higher.

So this got me thinking and using a subtype inherits Volatile but a new type copy doesn’t seem to. So I think you can drop the volatile issues but keep the maintainability of having e.g. one enum as the memory register itself.

Package Non_Volatile is
      type CFG2_COMM_Field is new STM32_SVD.SPI.CFG2_COMM_Field
end Non_Volatile;

It means conversions are required but that’s ok.

Great feedback, thanks! pragma Restriction sounds interesting.

I tried to play with that approach. But ideally, code like this should not compile when using an option type (conceptually I mean). That’s because you’re “forced” to unwrap the container type to then observe that something is present for use, or a type representing nothing.

with Ada.Text_IO;

procedure My_Program is
   package IO renames Ada.Text_IO;

   type My_Number is range 1 .. 10;

   type Option (Valid : Boolean := False) is record
      case Valid is
         when False =>
            null;
         when True =>
            Value : My_Number;
      end case;
   end record;

   Opt : Option := (Valid => False);
begin

   if not Opt.Valid then
      IO.Put_Line ("Values is:" & Opt.Value'Image);
   else
      IO.Put_Line ("No values");
   end if;

   IO.Put_Line ("Hello, World!!");

end My_Program;

Why not make the type generic?

1 Like

Slight improvement. There is actually no benefit of avoiding volatile when it is as in the usual case an input but this does regain nicer/exact selection semantics in the IDE as I would normally design into my own types.

Package Non_Volatile is
   Package CFG2_COMM is
      type Field is new STM32_SVD.SPI.CFG2_COMM_Field
   end CFG2_COMM;
end Non_Volatile;

p.s. I do not use package use clauses.