Changes between Version 14 and Version 15 of BestPracticeHandbook


Ignore:
Timestamp:
May 7, 2015, 4:00:32 PM (7 years ago)
Author:
Niall Douglas
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • BestPracticeHandbook

    v14 v15  
    386386}}}
    387387
    388 The expected outcome is a shared_ptr to a handle_type, the unexpected outcome is a std::error_code, and the catastrophic outcome is the throwing of bad_alloc. Code using `openfile()` can either manually check the expected (its bool operator is true if the expected value is contained, false if the unexpected value) or simply unilaterally call `expected<T>.value()` which will throw if the value is unexpected, thus converting the error_code into an exception. As you immediately note, this eliminates the need for two `openfile()` overloads because the single monadic return based implementation can now perform ''both'' overloads with equal convenience to the programmer. On the basis of halving the number of APIs a library must export, use of expected is a huge win.
    389 
    390 However I am still not happy with this semantic encapsulation because it is a poor fit to opening files. Experienced programmers will instantly spot the problem here: the `open()` call doesn't just return success vs failure, it actually has five outcome categories:
     388The expected outcome is a shared_ptr to a handle_type, the unexpected outcome is a std::error_code, and the catastrophic outcome is the throwing of bad_alloc. Code using `openfile()` can either manually check the expected (its bool operator is true if the expected value is contained, false if the unexpected value) or simply unilaterally call `expected<>.value()` which will throw if the value is unexpected, thus converting the error_code into an exception. As you will immediately note, this eliminates the need for two `openfile()` overloads because the single monadic return based implementation can now perform ''both'' overloads with equal convenience to the programmer. On the basis of halving the number of APIs a library must export, use of expected is a huge win.
     389
     390However I am still not happy with this semantic encapsulation because it is a poor fit to what opening files actually means. Experienced programmers will instantly spot the problem here: the `open()` call doesn't just return success vs failure, it actually has five outcome categories:
    391391
    3923921. Success, returning a valid fd.
    3933932. Temporary failure, please retry immediately: EINTR
    394 3. Temporary failure, please retry later: EBUSY, EISDIR, ELOOP, ENOENT, ENOTDIR, EPERM, EACCES (depending on changes on the filing system, this could disappear or appear at any time)
     3943. Temporary failure, please retry later: EBUSY, EISDIR, ELOOP, ENOENT, ENOTDIR, EPERM, EACCES (depending on changes on the filing system, these could disappear or appear at any time)
    3953954. Non-temporary failure due to bad or incorrect parameters: EINVAL, ENAMETOOLONG, EROFS
    3963965. Catastrophic failure, something is very wrong: EMFILE, ENFILE, ENOSPC, EOVERFLOW, ENOMEM, EFAULT
    397397
    398 So you can see the problem now: what we really want is for category 3 errors to only return with error_code, whilst category 4 and 5 errors plus bad_alloc to probably emerge as exception throws. That way the C++ semantics of the function would exactly match the semantics of opening files. So let's try again:
     398So you can see the problem now: what we really want is for category 3 errors to only return with error_code, whilst category 4 and 5 errors plus bad_alloc to probably emerge as exception throws (these aren't actually the ideal outcomes, but we'll assume this mapping for the purposes of brevity here). That way the C++ semantics of the function would closely match the semantics of opening files. So let's try again:
    399399
    400400{{{#!c++
    401401namespace std { using std::experimental; }
    402 std::expected<std::expected<std::shared_ptr<handle_type>, std::error_code>, std::exception_ptr> openfile(std::filesystem::path path) noexcept
     402std::expected<
     403  std::expected<
     404    std::shared_ptr<handle_type>,              // Expected outcome
     405    std::error_code>,                          // Expected unexpected outcome
     406  std::exception_ptr>                          // Unexpected outcome
     407openfile(std::filesystem::path path) noexcept  // Note the noexcept guarantee!
    403408{
    404409  int fd;
     
    409414    {
    410415      int code=errno;
     416      // If a temporary failure, this is an expected unexpected outcome
    411417      if(EBUSY==code || EISDIR==code || ELOOP==code || ENOENT==code || ENOTDIR==code || EPERM==code || EACCES==code)
    412418        return std::make_unexpected(std::error_code(code, generic_category());
     419
     420      // If a non-temporary failure, this is an unexpected outcome
    413421      std::string errstr(strerror(code));
    414422      return std::make_unexpected(std::make_exception_ptr(std::system_error(ec, std::move(errstr))));
     
    418426  catch(...)
    419427  {
     428    // Any exception thrown is truly unexpected
    420429    return std::make_unexpected(std::current_exception());
    421430  }
     
    4274361. Code calling `openfile()` no longer need to worry about exception safety - all exceptional outcomes are always transported by the monadic expected transport. This lets the compiler do better optimisation, eases use of the function, and leads to few code paths to test which means more reliable, better quality code.
    4284372. The semantic outcomes from this function in C++ have a close mapping to that of opening files. This means code you write more naturally flows and fits to what you are actually doing.
    429 3. Returning a monadic transport means you can now program monadically against the result e.g. `value_or()`, `then()` and so on. Monadic programming is also a formal specification, so you could in some future world use a future clang AST tool to formally verify the mathematical correctness of some monadic logic. That's enormous for C++.
    430 
    431 You may have noticed though the (Strongly) being in brackets, and if you guessed there are caveats then you are right. The first big caveat is that the expected<T, E> implementation in Boost.Expected is very powerful, but unfortunately has a big negative effect on compile times, and that rather ruins it for practical use for a lot of people. The second caveat is that integration between Expected and Future-Promise especially with resumable functions in the mix is currently poorly defined, and using Expected now almost certainly introduces immediate technical debt into your code.
    432 
    433 The third caveat is that I personally plan to write a much lighter weight monadic result transport which isn't as flexible as expected<T, E> (and probably hard coded to a T, error_code and exception_ptr outcomes) but would have negligible effects on compile times, and very deep integration with a non-allocating all-constexpr new lightweight future-promise implementation. Once implemented, my transport may be disregarded by the community, evolved more towards expected<T, E>, or something else entirely may turn up.
    434 
    435 In other words, I recommend you very strongly consider ''some'' mechanism for cleanly matching C++ semantics with what a function does now that C++ 11 makes it possible, but I unfortunately cannot categorically recommend one solution over another at the time of writing.
     4383. Returning a monadic transport means you can now program monadically against the result e.g. `value_or()`, `then()` and so on. Monadic programming - if and only if there is no possibility of exception throws - is also a formal specification, so you could in some future world use a future clang AST tool to formally verify the mathematical correctness of some monadic logic if and only if all the monadic functions you call are `noexcept`. That's enormous for C++.
     439
     440You may have noticed though the (Strongly) in the title of this section being in brackets, and if you guessed there are caveats in the above then you are right. The first big caveat is that the expected<T, E> implementation in Boost.Expected is very powerful and full featured, but unfortunately has a big negative effect on compile times, and that rather ruins it for the majority of people who only need about 10% of what it provides (and would rather like that to be quick to compile). The second caveat is that integration between Expected and Future-Promise especially with resumable functions in the mix is currently poorly defined, and using Expected now almost certainly introduces immediate technical debt into your code that you'll have to pay for later.
     441
     442The third caveat is that I personally plan to write a much lighter weight monadic result transport which isn't as flexible as expected<T, E> (and probably hard coded to a T, error_code and exception_ptr outcomes) but would have negligible effects on compile times, and very deep integration with a non-allocating all-constexpr new lightweight future-promise implementation. Once implemented, my monadic transport may be disregarded by the community, evolved more towards expected<T, E>, or something else entirely may turn up.
     443
     444In other words, I recommend you very strongly consider ''some'' mechanism for more closely and cleanly matching C++ semantics with what a function does now that C++ 11 makes it possible, but I unfortunately cannot categorically recommend one solution over another at the time of writing.
    436445
    437446