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.

5 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.

If you keep the status or error code as a separate out parameter from the option/result record then you will be warned by the compiler if you do not check it which is more important than the option type in the first place, in my opinion. -gnatw.o

This is flatly incorrect — ‘option’ is not a method for handling errors.

It’s not a method of handling errors… but that style can be done with definite-types and arrays: using this:

Subtype Option_Index is Boolean range True..True;
Type Optional is Array(Option_Index range <>) of ELEMENT_TYPE;
None : Constant Optional := (True..False => DUMMY_VAL); -- 0 length array.
Function "+"(Item : ELEMENT_TYPE) return Optional is
   ( True => Item );

And then for accessing the optional value:

Procedure Some_Operation(Object : Optional) is
Begin
  EXAMPLE:
  For Item of Object loop
    Print_Value_or_whatever( Item );
  End loop EXAMLPE;
End Some_Operation;

Ada does not have generic types.

Weirdly, I know that. At a distance of more than 6 months, I think I meant to ask, “Why not make the package generic?”

It’s an established pattern for error handling that came from functional programming and making its way to mainstream languages (Rust has it, C++ is getting it as I linked in my introduction post). But perhaps you had something else in mind?

Here’s a couple of refs to drive the point:

The option module provides the first alternative to exceptions. The 'a option data type represents either data of type 'a - for instance, Some 42 is of type int option - or the absence of data due to any error as None .

The Maybe type encapsulates an optional value. A value of type Maybe a either contains a value of type a (represented as Just a), or it is empty (represented as Nothing ). Using Maybe is a good way to deal with errors or exceptional cases without resorting to drastic measures such as error .

To be fair, I played with the C++ implementation since I posted and I found the implementation a little bit “clunky”. Still, I think it’s nice tool to have in your toolbox.

Here’s how you can define Option and Result types in TypeScript


Option may contain a value. To access the value, you are forced to “unwrap” this container at which point you’ll have “something” with a value or “nothing”.

interface None {
    readonly kind: 'None';
}

interface Some<A> {
    readonly kind: 'Some';
    readonly val: A;
}

type Option<A> = None | Some<A>;

Result may contain a value (of type A), and if not it provides an error with a value of type B (rather than “nothing”)

interface Err<T> {
    readonly kind: 'Err';
    readonly val: T;
}

interface Ok<T> {
    readonly kind: 'Ok';
    readonly val: T;
}

type Result<A, B> = Err<A> | Ok<B>;

These are not convential types in TypeScript btw, but the type system is flexible enough to allow expressing them.

Without having generic types (in the “generics” sens), it sounds like a deadend though.

For the interest of the discussion, I’ll note that Kotlin also has a Result type (with its own twist on things), but no Option type : Result - Kotlin Programming Language

1 Like

It’s not actually error-handling because it isn’t about error-states; exceptions and error-codes are error-handling (and exceptions can be used for non-exceptional signaling in addition to handling an error-state). — “Option” is more about data-structuring.

I can see how someone could confuse it and think it is about error-handling, but it’s really more about the processing of a list of items (namely 0- or 1-length).

If you structure your program in that list-processing manner then you can go from 0/1 to any size with [generally speaking] minimal refactor because you’re already handling the possibility of variance, rather than having THE data-item… and then having to deal with the absence.

I can see it being somewhat analogous to double entry accounting. I’m not sure whether it will actually catch any bugs that aren’t just the engineer being extremely sloppy. I can see it working or being reassuring for out parameters but what does Rust do about it’s equivalent of in out? Then you might want to have a readable parameter with a failure status. Then it becomes confusing on the API side.

1 Like

I only recall seeing Rust’s Option used as a return or an in type, not for out parameters, let alone in out parameters. I’m sure it can be used that way, but it would feel awfully clunky.

The point of Option is that a variable might not have a valid value; hence the variants are Some and None. Safe Rust requires the use of initialized variables only, so &mut parameters (the Rust version of in out) don’t really make sense as Option types, unless you’re trying to avoid the initialization, and in that case I don’t see why you wouldn’t prefer to pass the parameter as & (no mut) and return it instead, which seems more idiomatic (in my experience).

added in update: see this comment, or indeed the entire thread, which seems to confirm my experience

1 Like

Say you have a package with a function that provides a list type. Perhaps you would want to have a result option type where you wouldn’t have access to the list if something went wrong creating it. Another function operates on a successfully created list. Instead of both taking list. In Rust would one be list and one is option wrapping list. Isn’t that a little unneat/clunky? Isn’t the status/result type then inconsistent also? I tried to look at some rust crates but that didn’t help much. Perhaps I will have another look.

Rust has built in language level pattern matching constructs for option and result types, so it usually isn’t clunky. You get the option type and then can call the other functions using something like if let or match constructs.