Option and Result types

It doesn’t seem all that useful to me then. I guess the hype is about problems Ada doesn’t really have? One example uses vec.get giving none implicitly instead of null. With Ada you can do the same with a local constraint or storage error exception handler and it is clear what is happening. Or just working within bounds with range or an iterator.

Perhaps I will use it for out parameters or returns. It isn’t clunky in Ada at all. Though I won’t use it when it affects the api consistency.

If something went wrong creating it, then idiomatically you’d return Result rather than Option. You’d have to handle the error before proceeding, and you absolutely would not return a reference, as unless I misunderstand you that sounds like a lifetime :triangular_flag_on_post:.

If it wasn’t really an error, but rather nothing came of it (i.e., a true Some/None situation) then I don’t see why you wouldn’t just return an empty list. Either way, passing a reference to locally generated data sounds like you’re itching for a fight with the borrow checker - a fight you will lose.

A more idiomatic approach is to pass and/or return the list itself. I’ve definitely seen Rust code to this effect:

fn with_some_type(mut self, some_parameter: &SomeType) -> Self

Here, self “moves” into the function, which does something with it, then returns something of the same type, probably the modified self. You chain a bunch of these together and it makes sense:

let my_thing = Thing::new()
    .with_some_type(&some_thing)
    .with_another_type(&another_thing)
    .have_fun_with_all_these_parameters(&sure_thing);

This reminds me, though, that you do run across things like this:

fn do_something_with(&mut self)

…which might seem to contradict what I wrote earlier, but not really; I’ve seen it mainly with self, not so much on other parameters. Not that it can’t be done!

1 Like

Thinking about this more (and rereading the thread), I suppose you could have something like this:

fn do_something_with(&mut self, state: &mut Option<State>)

…where the ponit is that if state is None, you take one course of action, setting it to None after, and if state is Some(...), you take a different course of action, possibly to Some(...).

That’s not error handling, though. As I wrote earlier, idiomatic Rust uses Result for error handling. You can do similar things with it as the above.

And as far as sloppy engineering goes, Result forces the developer to place the potential error in the function signature, while Ada’s exceptions don’t, leaving a client unaware an exceptional situation could arise. I don’t see the advantage to Ada’s silent approach there at all.

2 Likes

Thank you, that was helpful

I meant local exception handler with returns such as a mutatable variant record in Ada. The exception handling and error handling is then explicit and returning a status enum static predicate works well in all cases.

This is the video where the switch to option is implicit via get.

8 minutes into this video shows a panic on unwrap though so the billion dollar null mistake isn’t completely avoided.

Booch 83 components use a local exception handler for the bounded and unbounded list. In the construct procedure, Storage_Error is raised and he raises overflow as a result. In the bounded managed case the backing store is arrays and the stack shall just cleanup on return and so in this case certainly you could return instead of propagating an overflow exception. In the unbounded managed case. I’m not sure as I haven’t actually used it. I don’t think there is any deallocation though, just re-use. Though it might be best to end the process if new allocation fails unless spark is used.

However, If I recall the right data structure. Rod Chapman stated that a performant/efficient (I can’t find it now to confirm) doubly linked list isn’t possible in Spark nor safe Rust though perhaps he was meaning with deallocation.

I didn’t watch the whole video, did he get into if let at all? That is what avoids the unwrap issue and it is actually pretty nice feature:

    if let Some(inner) = some {
        println!("value = {}", inner)
    }

in the case of your earlier question it would look something like (handwritten, my rust may be a little rusty):

let result = f1(some_params);  // or f1(some_params, &result);
if let Some(mut list) = result {
   f2(&list)
}else{
   // do some error handling
}

I don’t see an issue in Ada there. It actually reads better in Ada to me. You can just test the discriminant

 if Mutatable_List.Output_Available then

or just trust the status or test both.

My main issue is parameters of multiple functions that should have the same type having a different type and in out not being intuitive with a mutatable. If option is prevalent and you can see the type then it might alleviate that in Rust though they still have differences, which feels wrong e.g…

Option<List>

vs

Mutatable_List

That’s fine. We are kinda in the realm of subjective. What reads better and feels better can vary from person to person. I don’t recall saying there was an issue with Ada here, I was just commenting on the various rust methods since there was some discussion on that.

1 Like

Yeah, I’m getting lost in the conversation; I’m a bit confused on what exactly you’re getting at. For example:

Yes, but also no, for two reasons.

  1. More importantly IMHO, the .unwrap() makes it clear that, HEY HO THERE’S A POSSIBILITY OF FAILURE HERE which doesn’t ordinarily occur with null pointer access.
  2. Just a few moments later he shows how you’re supposed to handle such situations without an .unwrap().

No, that’s not right. Option is right there in the function signature of .get(), which means it’s not implicit.

Perhaps you mean that the type is implicitly deduced, since it’s a let? But that’s the case throughout the Rust language, so it’s not an issue with Option so much as an issue with type inference. As for .get() itself, it’s a known idiom that it returns an option wherever it appears in the standard library. It’s used when you consider that preferable to risking a panic on [] or similar.

As I say, I think we’re talking about different things, or something. Or maybe @jere is right and it’s a question of de gustibus non disputandum.

Any null access issue shouldn’t really reach the user.

Yes the actual code is implicit and the library explicit. I guess this is style. In fact I actually preferred using var with every variable instead of := in Go.

Let me put it this way. The Result option type or mutatable record as a defence against ignoring status and yet attempting to use an output in Ada is of marginal benefit and I was considering using it everywhere despite the potential of creating a crash as the program is wrong. Whether the consequences of crashing are worse than the bug is for the birds. However if I cannot use it everywhere then I am not convinced that I should use it anywhere. Of course you can use it everywhere including in out but does that bring confusion. I’m unsure if whether it is worth the added complexity is style. I expect not.

It’s more like “a fork in the road”, with 2 branches. You are forced to go either one way or the other. Of course it’s still possible botch things up if you decide to swallow the error inappropriately.

So this is the scenario: let’s say you didn’t realize an operation could fail, so you now mark it’s output type with a Result (or Option). And now all your callers won’t compile because they’ll have to handle the error (whatever that means as that’s context dependent).

I’m not familiar enough with “in out” parameters to have a formed opinion, but if we look at this resource:

Wiki: Ada_Programming/Error_handling

Then Option is similar to the scenario in Error_Handling_2, except we don’t encounter this downside:

The bad news is that the Success parameter value can easily be ignored.

In terms of Rust terminology, instead of returning false with a bogus value we would return the “variant” of the Option type representing no value: None.

It’s similar in spirit to Go’s way of handling errors, but with better semantics (and much less verbosity due to other factors).

The linked resource proposes 5 ways to handle errors, with exceptions being the favored way. Did you have some other way to handle errors in mind? I’m not sure I understood your point.

Wiki: Ada_Programming/Error_handling misses

  • Variant records. The sample was already presented in the discussion.
  • Invalid values. This is how IEEE 754 standard deals with overflows and zero divide. Ada has X’Valid attribute to check validity of a value.

In practice one should almost always use exceptions.

  • Exceptions are safe, you cannot miss or ignore it.
  • Faster, modern implementations have zero overhead when an exceptions is not raised. Other variants require explicit checking. In some cases and schemas checking is not possible, e.g. ahead testing for the file end.
  • A cleaner the code as you see only the normal flow.
  • Clean error handling logic, the exception handler is placed where the problem can be dealt with. E.g. a file read error is not handled in the device driver. The driver simply tells I cannot handle this and whoever can does recovery etc.
  • Suitable for software design. E.g. an tree walker need not to know exceptions node visitor may raise. It just passes them through. Error codes require maintenance any change/addition imposes a massive problem. It is tight coupling and exposing implementation details.

The problem with Ada exceptions is that there is still no exception contracts. After some many, sometimes questionable additions, made into the language…

A border case is variant record, used when the distinction between normal and abnormal path is not so clear.

Return code is used only where there are two roughly equivalent paths. It is quite rare.

3 Likes

So I missed the point given by @kevlar700 that reignited this post.

If I add this compile flag to my *.gpr file

package Compiler is
   for Default_Switches ("Ada") use Hello_Ada_Config.Ada_Compiler_Switches & "-gnatw.o";
end Compiler;

Then indeed the following code generates a warning:

with Ada.Text_IO;                       use Ada.Text_IO;
with Ada.Numerics.Elementary_Functions; use Ada.Numerics.Elementary_Functions;

procedure Hello_Ada is
   procedure Square_Root
     (Input : Float; OutputValue : out Float; OutputSuccess : out Boolean) is
   begin
      if Input < 0.0 then
         OutputValue := 0.0;
         OutputSuccess := False;
      else
         OutputValue := Sqrt (Input);
         OutputSuccess := True;
      end if;
   end Square_Root;

begin
   declare
      GotValue  : Float;
      IsSuccess : Boolean;
   begin
      Square_Root (-4.0, GotValue, IsSuccess);
      --  if IsSuccess then
      --     Put_Line ("Square_Root result is:" & Float'Image (GotValue));
      --  else
      --     Put_Line ("Square_Root: Ignoring result due to invalid input");
      --  end if;
      Put_Line
        ("Ignoring IsSuccess generates a warning:" & Float'Image (GotValue));
   end;
end Hello_Ada;

In practical terms, that means I get a non-zero status code from my shell when compiling.

1 Like

Actually you are more likely to miss an exception although spark can ensure that you do not. The system crash should be caught in testing or at design time but that isn’t guaranteed. Or by a global handler. Also not all runtimes support exceptions anyway and I want Ada to be as ubiquitous as possible.

Exceptions are great in any case for e.g. runtime instigated exceptions like storage_error for sure. Unfortunately spark can’t support them even locally though you can wrap them with spark mode off like in the Thales guidance doc.

The alternative, e.g. you forgot to check the return code and let a fault slip through, is undefined behaviour. So exceptions are indeed unquestionably safer.

A real story. IEEE 754 uses invalid values instead of exceptions. A production system had a bug in computations and wrote IEEE 754 non-values into the database. Months later these values crashed a visualisation program rendering the stored data. Months of work was lost because accumulated data were garbage. It would not happen with exceptions. So always:

type Safe_Float is new Float range Float'Range; -- I want exceptions!

Exception means earliest possible run-time error detection. Only static correctness is better … and handling faults (exceptional states) /= debugging anyway.

1 Like

People have written exception handlers that have handled all or others or have been confused with potentially undefined behaviour. gnatw.o and case statements especially if static predicates per procedure are equivalent in my opinion. The trade off is verbosity (explicit unhappy path) but being easier to reason about. They depend less on good documentation too. I actually prefer it to exceptions on any runtime but local exceptions for e.g. constraint error can be nice (caveat: I don’t have that much exception experience)

Exceptions do bubble up, but it makes every subprogram call a potentially dangerous black box. One of my programs had an instant crash on Mac because GNAT didn’t implement that specific standard library function at the time and instead just threw an undocumented exception. It was the only change I remember needing to support that platform, but it was completely unexpected.

My understanding was that explicit exceptions were considered a failure in both Java and C++. C++ now goes the other way, promoting noexcept markers to show exceptions shouldn’t leave the function.

GMP (GNU numeric library) calls abort() on errors, much better than an exception? To crash the whole program… :grinning:

Just being curious, how an unimplemented function could return a result code?

That is an exception contract.

C++ community slowly comes to senses. Maybe, maybe some day they would add contracts to templates…

I already said that exception contracts, statically enforced, would be highly desirable in Ada.

This is what the result codes do for you! So, you do not even need to bother about any error handling! :joy:

Sure, the program reports a failure with the code 0x2391 which meaning should be evident for anybody! :slightly_smiling_face:

However, more likely it would silently corrupt files, produce garbage and security holes…

Now you’re talking nonsense or are you referencing C codes. I use an out with a statically preedicated enum which describes exactly what the procedure returns with a case statement. Though sometimes there might be a record of booleans such as when low level spi can return multiple conditions. I like to keep flow and exception control clear but unified like Rust.

Current SPARK supports exceptional contracts.

https://docs.adacore.com/spark2014-docs/html/ug/en/source/subprogram_contracts.html#exceptional-contracts