How would you use Alexandrescu's Expected<T> with void functions?

Even though it might appear new for someone focused solely on C-ish languages, to those of us who had a taste of languages supporting sum-types, it's not.

For example, in Haskell you have:

data Maybe a = Nothing | Just a

data Either a b = Left a | Right b

Where the | reads or and the first element (Nothing, Just, Left, Right) is just a "tag". Essentially sum-types are just discriminating unions.

Here, you would have Expected<T> be something like: Either T Exception with a specialization for Expected<void> which is akin to Maybe Exception.


Have any of you tried Expected; in practice?

It's quite natural, I used it even before I saw this talk.

How would you apply this idiom to functions returning nothing (that is, void functions)?

The form presented in the slides has some subtle implications:

  • The exception is bound to the value.
  • It's ok to handle the exception as you wish.
  • If the value ignored for some reasons, the exception is suppressed.

This does not hold if you have expected<void>, because since nobody is interested in the void value the exception is always ignored. I would force this as I would force reading from expected<T> in Alexandrescus class, with assertions and an explicit suppress member function. Rethrowing the exception from the destructor is not allowed for good reasons, so it has to be done with assertions.

template <typename T> struct expected;

#ifdef NDEBUG // no asserts
template <> class expected<void> {
  std::exception_ptr spam;
public:
  template <typename E>
  expected(E const& e) : spam(std::make_exception_ptr(e)) {}
  expected(expected&& o) : spam(std::move(o.spam)) {}
  expected() : spam() {}

  bool valid() const { return !spam; }
  void get() const { if (!valid()) std::rethrow_exception(spam); }
  void suppress() {}
};
#else // with asserts, check if return value is checked
      // if all assertions do succeed, the other code is also correct
      // note: do NOT write "assert(expected.valid());"
template <> class expected<void> {
  std::exception_ptr spam;
  mutable std::atomic_bool read; // threadsafe
public:
  template <typename E>
  expected(E const& e) : spam(std::make_exception_ptr(e)), read(false) {}
  expected(expected&& o) : spam(std::move(o.spam)), read(o.read.load()) {}
  expected() : spam(), read(false) {}

  bool valid() const { read=true; return !spam; }
  void get() const { if (!valid()) std::rethrow_exception(spam); }
  void suppress() { read=true; }

  ~expected() { assert(read); }
};
#endif

expected<void> calculate(int i)
{
  if (!i) return std::invalid_argument("i must be non-null");
  return {};
}

int main()
{
  calculate(0).suppress(); // suppressing must be explicit
  if (!calculate(1).valid())
    return 1;
  calculate(5); // assert fails
}

Like Matthieu M. said, this is something relatively new to C++, but nothing new for many functional languages.

I would like to add my 2 cents here: part of the difficulties and differences are can be found, in my opinion, in the "procedural vs. functional" approach. And I would like to use Scala (because I am familiar both with Scala and C++, and I feel it has a facility (Option) which is closer to Expected<T>) to illustrate this distinction.

In Scala you have Option[T], which is either Some(t) or None. In particular, it is also possible to have Option[Unit], which is morally equivalent to Expected<void>.

In Scala, the usage pattern is very similar and built around 2 functions: isDefined() and get(). But it also have a "map()" function.

I like to think of "map" as the functional equivalent of "isDefined + get":

if (opt.isDefined)
   opt.get.doSomething

becomes

val res = opt.map(t => t.doSomething)

"propagating" the option to the result

I think that here, in this functional style of using and composing options, lies the answer to your question:

So, what would your code look like if you had another function, say toUpper(s), which modifies the string in-place and has no return value?

Personally, I would NOT modify the string in place, or at least I will not return nothing. I see Expected<T> as a "functional" concept, that need a functional pattern to work well: toUpper(s) would need to either return a new string, or return itself after modification:

auto s = toUpper(s);
s.get(); ...

or, with a Scala-like map

val finalS = toUpper(s).map(upperS => upperS.someOtherManipulation)

if you don't want to follow a functional route, you can just use isDefined/valid and write your code in a more procedural way:

auto s = toUpper(s);
if (s.valid())
    ....

If you follow this route (maybe because you need to), there is a "void vs. unit" point to make: historically, void was not considered a type, but "no type" (void foo() was considered alike a Pascal procedure). Unit (as used in functional languages) is more seen as a type meaning "a computation". So returning a Option[Unit] does make more sense, being see as "a computation that optionally did something". And in Expected<void>, void assumes a similar meaning: a computation that, when it does work as intended (where there are no exceptional cases), just ends (returning nothing). At least, IMO!

So, using Expected or Option[Unit] could be seen as computations that maybe produced a result, or maybe not. Chaining them will prove it difficult:

auto c1 = doSomething(s); //do something on s, either succeed or fail
if (c1.valid()) {
   auto c2 = doSomethingElse(s); //do something on s, either succeed or fail
   if (c2.valid()) { 
        ...

Not very clean.

Map in Scala makes it a little bit cleaner

doSomething(s) //do something on s, either succeed or fail
   .map(_ => doSomethingElse(s) //do something on s, either succeed or fail
   .map(_ => ...)

Which is better, but still far from ideal. Here, the Maybe monad clearly wins... but that's another story..