Thread overview
Idiomatic way to express errors without resorting to exceptions
Feb 29, 2020
Adnan
Feb 29, 2020
Sebastiaan Koppe
Feb 29, 2020
Adnan
Feb 29, 2020
Paul Backus
Feb 29, 2020
Sebastiaan Koppe
Mar 07, 2020
Arine
Mar 07, 2020
Sebastiaan Koppe
Mar 09, 2020
Simen Kjærås
Feb 29, 2020
Basile B.
February 29, 2020
I have a struct that has to arrays. Each of those must have the same sizes.

So while constructing the array, if you pass two arrays of different sizes the constructor must return nothing.

In Rust I could easily use Option<T>. D has no answer to Optional types as far as I am concerned. Is throwing exceptions the only idiomatic way?


---

What I already considered:

* Using Nullable!T: Okay but Nullable!T has all the overloads for regular T which makes the  API even more unpredictable.

In Rust you don't just add a Some(44) and 34; No overloads for Some<T> and i32 are allowed (purposefully).

* Option!T from the optional package: Has even worse problem IMO. Not only it allows None + int but also it returns a `[]`. This API is not to my liking. You could say well Haskell has fmap for Optional etc, and I am aware of that, so does Rust with map etc. But I am talking about basic things: like `+`.

* Value-based error handling like Go and C: well, that works but the error checking is opt-in. There's no such thing as [[nodiscard]] in D too so the user of the API might as well forget to check for error value.

* if specialization: Clever workaround but sometimes the struct may fail for complex reasons, not only for a single condition.
February 29, 2020
On Saturday, 29 February 2020 at 12:50:59 UTC, Adnan wrote:
> * Option!T from the optional package: Has even worse problem IMO. Not only it allows None + int but also it returns a `[]`. This API is not to my liking. You could say well Haskell has fmap for Optional etc, and I am aware of that, so does Rust with map etc. But I am talking about basic things: like `+`.

I would argue it is one of its strengths.

Regardless, I don't think option/nullable/maybe is a good type for errors. Rather you should use something like SumType!(T, Error), or any other 'either' type.
February 29, 2020
On Saturday, 29 February 2020 at 13:03:21 UTC, Sebastiaan Koppe wrote:
> On Saturday, 29 February 2020 at 12:50:59 UTC, Adnan wrote:
>> * Option!T from the optional package: Has even worse problem IMO. Not only it allows None + int but also it returns a `[]`. This API is not to my liking. You could say well Haskell has fmap for Optional etc, and I am aware of that, so does Rust with map etc. But I am talking about basic things: like `+`.
>
> I would argue it is one of its strengths.

Doesn't that pretty much defeat the entire purpose of type-guarding a potentially error/absent value?
February 29, 2020
On Saturday, 29 February 2020 at 12:50:59 UTC, Adnan wrote:
> I have a struct that has to arrays. Each of those must have the same sizes.
>
> So while constructing the array, if you pass two arrays of different sizes the constructor must return nothing.
>
> In Rust I could easily use Option<T>. D has no answer to Optional types as far as I am concerned. Is throwing exceptions the only idiomatic way?
>
>
> ---
>
> What I already considered:
>
> * Using Nullable!T: Okay but Nullable!T has all the overloads for regular T which makes the  API even more unpredictable.
>
> In Rust you don't just add a Some(44) and 34; No overloads for Some<T> and i32 are allowed (purposefully).
>
> * Option!T from the optional package: Has even worse problem IMO. Not only it allows None + int but also it returns a `[]`. This API is not to my liking. You could say well Haskell has fmap for Optional etc, and I am aware of that, so does Rust with map etc. But I am talking about basic things: like `+`.
>
> * Value-based error handling like Go and C: well, that works but the error checking is opt-in. There's no such thing as [[nodiscard]] in D too so the user of the API might as well forget to check for error value.
>
> * if specialization: Clever workaround but sometimes the struct may fail for complex reasons, not only for a single condition.

There's no idiomatic way since D lang is based on exceptions...

However I'd use one of those system:

1. return error, write result in ref parameter.

    alias CheckedValueProto(RT, P...) = bool function(ref RT, P params);

2. the same using a special struct and no more ref param. So more like Nullable/Optional but with a dedicated generic type that contain a single opover used to indicate if there's been an error or not.

    struct CheckedValue(T) {
        bool noError;
        T t;
        B opCast(B : bool)() inout pure nothrow @safe {
            return noError;
        }
    }

and you make your functions to return CheckedValues...

    CheckedValue!int strToInt(string input);
    ....
    if (const CheckedValue!int = strToInt("a") {} else {}

Although
- both still require self-discpline or a specialized linter to detect unchecked calls ;
- the whole standard library is incompatible ;

I have a personal preference for 2. even if it causes problems when T is of same size as a pointer. Now the question is also what's the more costly ? try/catch or this non atomic return ?
February 29, 2020
On Saturday, 29 February 2020 at 13:40:11 UTC, Adnan wrote:
> On Saturday, 29 February 2020 at 13:03:21 UTC, Sebastiaan Koppe wrote:
>> On Saturday, 29 February 2020 at 12:50:59 UTC, Adnan wrote:
>>> * Option!T from the optional package: Has even worse problem IMO. Not only it allows None + int but also it returns a `[]`. This API is not to my liking. You could say well Haskell has fmap for Optional etc, and I am aware of that, so does Rust with map etc. But I am talking about basic things: like `+`.
>>
>> I would argue it is one of its strengths.
>
> Doesn't that pretty much defeat the entire purpose of type-guarding a potentially error/absent value?

I suppose it depends on what you think that purpose is.

Optional!T will not allow you to do anything that requires a T unless the T is actually present. In some cases, it will allow you to *attempt* such operations, and return an empty Optional at run time if they are not allowed; in others, the attempt will result in a type error at compile time. This is a compromise between strictness and convenience, but there is no compromise to correctness. Unlike, say, a null pointer, Optional!T will not allow you to cause a run-time error by forgetting a check.

(Of course, you can explicitly unwrap the Optional without checking, but that's not something you're likely to do by accident.)
February 29, 2020
On Saturday, 29 February 2020 at 13:40:11 UTC, Adnan wrote:
> On Saturday, 29 February 2020 at 13:03:21 UTC, Sebastiaan Koppe wrote:
>> On Saturday, 29 February 2020 at 12:50:59 UTC, Adnan wrote:
>>> * Option!T from the optional package: Has even worse problem IMO. Not only it allows None + int but also it returns a `[]`. This API is not to my liking. You could say well Haskell has fmap for Optional etc, and I am aware of that, so does Rust with map etc. But I am talking about basic things: like `+`.
>>
>> I would argue it is one of its strengths.
>
> Doesn't that pretty much defeat the entire purpose of type-guarding a potentially error/absent value?

Like I said, I don't use optionals when I care about errors. That is not what they are designed for.

If I want to type-guard potential errors I will use SumType!(T, Error). It forces you to handle both cases, either at compile time (with the match template), or at runtime (with the tryMatch template).

The power of optionals, however, lies in the following:

```
list.first.doSomething();
```

Here doSomething will only be called when the list contains at least one item. Here I don't care about cases where the list is empty, I just want to doSomething if it isn't.
March 07, 2020
On Saturday, 29 February 2020 at 15:23:02 UTC, Sebastiaan Koppe wrote:
> Like I said, I don't use optionals when I care about errors. That is not what they are designed for.
>
> If I want to type-guard potential errors I will use SumType!(T, Error). It forces you to handle both cases, either at compile time (with the match template), or at runtime (with the tryMatch template).
>
> The power of optionals, however, lies in the following:
>
> ```
> list.first.doSomething();
> ```
>
> Here doSomething will only be called when the list contains at least one item. Here I don't care about cases where the list is empty, I just want to doSomething if it isn't.

I feel as though that's it's greatest weakness. It makes the check whether there is or isn't a value hidden. The case when there isn't a value should be handled explicitly, not implicitly. Propogating a None value isn't useful and is computationally demanding as each subsequent call will need to do a check to see if it has a value as it results in an optional type (for binary operators). So something like `a + b * c` is now much more expensive than it appears. This is exactly the kind of abuse of operator overloading that the feature is shunned for.

Anyways not sure what you mean here with the code below. If "first" here returns an optional type, you can't call "doSomething" like that without first checking if the value exists. Optional just doesn't allow it and Nullable will throw an exception.

```
list.first.doSomething(); // error
```

Which makes it odd that it forwards operator overloading to the underlying type, which is basically just syntactic sugar for calling a method on the object. Seems like a conflicting design decision to me.



March 07, 2020
On Saturday, 7 March 2020 at 15:44:38 UTC, Arine wrote:
> I feel as though that's it's greatest weakness. It makes the check whether there is or isn't a value hidden. The case when there isn't a value should be handled explicitly, not implicitly. Propogating a None value isn't useful and is computationally demanding as each subsequent call will need to do a check to see if it has a value as it results in an optional type (for binary operators). So something like `a + b * c` is now much more expensive than it appears. This is exactly the kind of abuse of operator overloading that the feature is shunned for.

What can I say? If you don't like the semantics, don't use it. I have found value in it.

> Anyways not sure what you mean here with the code below. If "first" here returns an optional type, you can't call "doSomething" like that without first checking if the value exists. Optional just doesn't allow it and Nullable will throw an exception.
>
> ```
> list.first.doSomething(); // error
> ```

You are right. Nowadays you need a `.oc` call in between. Also, doSomething cannot be a free-standing function (sadly).
March 09, 2020
On Saturday, 7 March 2020 at 15:44:38 UTC, Arine wrote:
> The case when there isn't a value should be handled explicitly, not implicitly. Propogating a None value
> isn't useful

Except when it is useful, and shouldn't be handled explicitly. I have code in D, C and C++ that looks like this:

    ReturnValue result = someInitialValue;
    auto foo = getFoo();
    if (!foo) return result;
    auto bar = foo.fun();
    if (!bar) return result;
    return bar.gun();

In C#, this would be:

    return getFoo()?.fun().gun() ?? someInitialValue;

And with implicit handling in Optional!T, it looks like this:

    return getFoo().oc.fun().gun().or(someInitialValue);

Clearly the latter two are more readable, and I'm not gonna care that it's a little slower in the 99% of cases where speed is not important.

--
  Simen