[outcome] Requesting second pre-review of Boost.Outcome tutorial
Dear Boost,
Last November you gave me some valuable feedback on rewriting the
tutorial for Boost.Outcome, a C++ 14 library providing a
factory and family of policy driven lightweight monadic
value-or-error transports. I recently came out of employment contract
and so have had the time to rewrite Outcome's Tutorial (anyone
interested in employing a remote working C++ consultant please do get in
contact). I'd appreciate feedback on https://ned14.github.io/boost.outcome/:
1. Is this rewritten tutorial too simple, too detailed or about right?
2. Do you feel you could use Outcome in your own code without difficulty
after reading it? If not, what information is missing?
3. Are you persuaded of the merits of using Outcome after reading it? If
not, what else do I need to say?
The first FOUR sections at https://ned14.github.io/boost.outcome/ have
been rewritten by me to hopefully reflect the feedback you gave me last
November, so these have been refactored so far:
* Description, Prerequisites and Support
* Installing, building and testing Outcome
* Quick start for the expert
* Tutorial
The package repo install methods haven't been implemented yet, these are
marked in the docs as todo.
These sections have yet to be refactored:
* Frequently Asked Questions
* Advanced usage: Outcome as a Monad
* Outcome and the upcoming std::expected
Hi Niall, I wanted to invest 5-10 minutes into learning the library and this is my feedback about the tutorial. The tutorial is too long, and it goes too deep into unnecessary details, and it does not teach me properly how to do the basic things with the library (in the default configuration). The reasons are the following: - Too much unnecessary discussion about implementation details. During the first 5 minutes of learning the library, I do not care that optional, result and outcome are implemented using basic_monad. - Too much unnecessary discussion about performance. During the first 5 minutes of learning the library it is enough to tell me that it provides zero-cost abstractions and maybe link that to a performance section _with data_. Telling me how many cycles something costs is unnecessary and useless without showing me data (since at this point in my learning of the library I am still very skeptic about its usefulness). - Fundamental concepts are not explained in an idiot-proof way. I have to skim through a wall of text to figure out when do you use outcome vs result. IIUC result is faster, so if you can use it you should. The docs should put fundamental concepts upfront. You can link to a performance section for the details, but these do not add anything value during the tutorial. - Too much non-c++ speak. For example, basic_monad raise warning flags ("Is that a base implementation for all Monads? (that cannot be true, does the author even know what Monad is?, etc.)"), you probably want to call that basic_outcome instead. - I have to read too much before actually getting to what Outcome does. - Too many superlatives and words devoid of meaning ("much more comprehensive monadic programming interface", "ultra low runtime overheads", "really elegant expression of error handling in C++", the way of doing error handling in C++17 and beyond (20) ...). Basically reading the tutorial feels like you are trying to sell me a broken car. The tutorial uses lots of words to tell me how great the library is, but almost no examples/data to show that this is the case. How to improve the tutorial: - Reduce the time needed to come in contact with Outcome. Put the code examples at the beginning side-by-side if possible, e.g., show only C++ using throw, C++ using std::error_code, and C++ using Boost.Outcome, and then tell me _as briefly as possible_ the advantages of using Outcome (noexcept, composition, performance... don't use a wall of text!). - Focus the tutorial on examples that show how to use the library and which problems does it solve good: - Examples that show how to use the whole API of result/outcome, composition (error values, functions chaining errors, ...), ... for dealing with optional values and doing error handling! (which is the raison-d'etre of the library) - If the examples show how good the library solves the problem it is supposed to solve you don't need to tell me in the text! - Move all the performance-related discussions/details to a section which first show the data, and then discusses it and convinces me with facts. Just tell me that the library is a zero-cost abstraction (or fast), and link to this section. - Move all the implementation-related details to a different section about the implementation (e.g. how to implement optional using basic_monad) - Remove any mention of optional since it raises too many questions (another optional type in Boost? why can't Boost.Optional be improved? ...) - Make sure that the fundamental concepts are stated in a water-tight easy-to-understand way, for example: - When to use outcome vs result: result is faster so prefer result if you can - What does Boost.Outcome does? "Boost.Outcome provides an unified way of working with result values that may become optional due to error conditions or exceptions." (try to use C++-speak, do not mention Monad, do not use complicated words that an user might need to google, ... i.e. keep it simple). - use space (and user time) efficiently, e.g., the part of the tutorial where you have three "What happens if..." can probably be summarized into a small table (less words, a bit more visual, might be a more efficient way of conveying the information). - Change the tone of the tutorial. That is, go through: - every single sentence and evaluate whether somebody with less than 5 min experience with the library needs to learn this right now, or at all (maybe delete it, maybe move it to a more advanced tutorial like the configuration stuff or the basic_monad stuff) - every single word and evaluate whether it adds any extra meaning to the sentence (in particular kill all those superlatives, they should be clear from the examples or the data, otherwise I feel like I am being scammed). - remove non-C++ speak terms (like Monad), basic_monad raises to many questions that aren't answered so maybe rename that to basic_outcome. - Rethink whether to expose a boost::outcome::optional type in the library at all. This is going to raise tons of questions during review (we already have Boost.Optional and std::optional). - Go through all functions in the tutorial and make sure they are linked properly (e.g. the get and get_error member functions are not). - Rethink the cheat-sheet: - move it to its own section - make sure that it solves its purpose, that is, for somebody who already knows how to use the library but cannot remember how a function, macro, ... was called, can the cheat-sheet answer its question in say less than 10 seconds? (e.g. the cheat-sheet of the Eigen library does a great job at this). I hope the feedback helps, and sorry if it sounds harsh. As an error handling solution, how to use the library to do error handling can probably be explained in full with examples in 1-2 pages, mainly because the API is both simple and nice. The current tutorial does not reflect this but with more time you can probably make it do so. Bests, Gonzalo
On 16/01/2017 10:47, Gonzalo BG wrote:
Hi Niall,
I wanted to invest 5-10 minutes into learning the library and this is my feedback about the tutorial. The tutorial is too long, and it goes too deep into unnecessary details, and it does not teach me properly how to do the basic things with the library (in the default configuration). The reasons are the following:
That's really great and detailed feedback. I also got some good feedback from SG14. I'll take a third attempt at rewriting the documentation. It's lucky I'm currently out of contract, phew ... Regarding specific points of yours:
- Examples that show how to use the whole API of result/outcome, composition (error values, functions chaining errors, ...), ... for dealing with optional values and doing error handling! (which is the raison-d'etre of the library)
I had one of these in the first write of the tutorial where I had taken a real world actual use case of Outcome and reduced it to a minimum standalone snippet. Feedback from here said it was too complicated, too long and needed to be removed, so I deleted it. I think what you're asking for is cookbook of common use case sequences? I'll see what I can do on that.
- Remove any mention of optional since it raises too many questions (another optional type in Boost? why can't Boost.Optional be improved? ...)
Given feedback from elsewhere, I think sadly I'm going to have to drop the monadic programming support too. Too many people want a Hana style monad, and I have no personal need for one of those especially when said people should just go use Hana. I'll leave the implementation in the code which can be enabled with a macro, but drop any mention of monads completely from the docs. It's a shame though. The monadic programming API is way better than Expected's. Much nicer to write against. But it's hard on the compiler, and its limitations will disappoint many keen on monadic programming. As with Expected, it probably needs to disappear entirely to pass a peer review here.
I hope the feedback helps, and sorry if it sounds harsh. As an error handling solution, how to use the library to do error handling can probably be explained in full with examples in 1-2 pages, mainly because the API is both simple and nice. The current tutorial does not reflect this but with more time you can probably make it do so.
Thank you so much for the feedback. I'll try a third rewrite. Please God let the third time be lucky, writing documentation seems to never end ... Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Dear Boost,
Last November you gave me some valuable feedback on rewriting the tutorial for Boost.Outcome, a C++ 14 library providing a factory and family of policy driven lightweight monadic value-or-error transports. I recently came out of employment contract and so have had the time to rewrite Outcome's Tutorial (anyone interested in employing a remote working C++ consultant please do get in contact). I'd appreciate feedback on https://ned14.github.io/boost. outcome/:
1. Is this rewritten tutorial too simple, too detailed or about right?
2. Do you feel you could use Outcome in your own code without difficulty after reading it? If not, what information is missing?
3. Are you persuaded of the merits of using Outcome after reading it? If not, what else do I need to say?
The first FOUR sections at https://ned14.github.io/boost.outcome/ have been rewritten by me to hopefully reflect the feedback you gave me last November, so these have been refactored so far:
* Description, Prerequisites and Support * Installing, building and testing Outcome * Quick start for the expert * Tutorial
The package repo install methods haven't been implemented yet, these are marked in the docs as todo.
These sections have yet to be refactored:
* Frequently Asked Questions * Advanced usage: Outcome as a Monad * Outcome and the upcoming std::expected
Many thanks in advance for any feedback received.
Hi Niall, I intend to review the docs again. So far I had only a glimpse at the initial page, and it really looks good. I was now able to grasp the idea of
2017-01-11 13:53 GMT+01:00 Niall Douglas
Many thanks in advance for any feedback received.
I intend to review the docs again. So far I had only a glimpse at the initial page, and it really looks good. I was now able to grasp the idea of the library in less than a minute.
Cool. The simplest primitives are often the hardest to convey the use case for.
One thing I wanted to bring up immediately, is not really related to your library but to a detail in the example. It uses `noexcept` to illustrate that the function does not throw itself:
``` bo::outcome<int> getConfigParam(std::string name) noexcept; ```
I strongly believe that this is the wrong thing to do (or at least controversial) to annotate a function that can fail-but-not-throw as noexcept. It is more in the spirit of noexcept intentions to indicate a no-fail guarantee rather than the no-throw guarantee.
I have provided the justification for this claim in the following post: https://akrzemi1.wordpress.com/2014/04/24/noexcept-what-for/
And conversely, only because you guarantee that a given function never throws, it does not immediately imply that a function should be declared as noexcept.
I think that your example would not suffer if the noexcept is removed. It still does the good job of illustrating the intent. But you would avoid certain controversies that might divert the reader's attention from the main point.
Most of what you write in your blog I would generally agree with and you saved Outcome from a design mistake in https://akrzemi1.wordpress.com/2014/12/02/a-gotcha-with-optional/. But regarding https://akrzemi1.wordpress.com/2014/04/24/noexcept-what-for/ I think you're wide of the mark, and actually because I recognise that your opinion is not uncommon, it's one of the reasons why the tutorial bangs on about error handling in C++ in such lengthy detail plus presenting the error handling design patterns in does in such hand wavy and superlative language terms. Indeed I *am* trying to sell something, and it is the enormous value of making all your extern APIs noexcept which I call "sea of noexcept, islands of throwing" in the tutorial. Quoting your blog post:
Why do you need to know?
Is this an important information for you if a given function may throw or not? If so, why? Some possible answers include the following:
1. Because if it throws and I do not catch it, std::terminate will be called and I do not want this to happen.
2. Because I need this function to provide no-throw exception safety guarantee.
3. Because this may allow certain compiler optimizations.
I agree with you that answers 1 and 3 ought to be discounted for the large majority of C++ programmers. If you are using noexcept for those reasons, then don't because it's a lousy solution to not the problem you think you have. About answer 2 you say:
If your motivation is (2), noexcept will also not help you much. Suppose you detect that the function is not declared as noexcept, what do you do? Don’t use it? It may still not throw exceptions. A function can throw nothing and still be declared noexcept(false). This is the case for the std::swap specialization for STL containers: following the recommendation from N3248 it is guaranteed not to throw, but is declared noexcept(false). For this reason, the example I gave in my other post with function nofail_swap is wrong. It doesn’t take into account that swap on STL containers is no-fail. You cannot check the no-fail or no-throw guarantee with a compile-time expression, because in some cases it is just announced informally in the documentation.
Also, if a function is declared noexcept it can’t throw, but it can call std::terminate. Do you consider this behaviour suitable for a component that is supposed to be “exception safe”?
Here is where I think you've missed the beat. In everything you say above you are 100% correct. noexcept has a lousy, poorly thought through implementation in C++ and it shows every bit of having been tacked on at the last minute in the C++ 11 standard, and I said so at the time to anyone who would listen in 2010. But do you see that the lousy implementation has nothing to do with this:
2. Because I need this function to provide no-throw exception safety guarantee.
Your argument against using noexcept to make a function provide a no-throw exception safety guarantee was all about the lousy implementation of noexcept, and nothing about whether explicitly guaranteeing that calling some function will not invert control flow is a good or bad thing. You are absolutely right that marking a function with noexcept means it simply adds a call to std::terminate which is usually not what the programmer intended. That's that lousy implementation again, not least that this stupid piece of code: void somehow_this_is_not_a_compile_failure() noexcept { throw "foo"; // i.e. std::terminate() } ... is legal, and worse, many compilers don't even warn, you need to run clang-tidy on it to get any indication of this being very unlikely to be what the programmer intended. But that's a problem for the library implementor, and a very different viewpoint arrives at the library *user*. One of the main reasons you'd want to use Outcome is because *you really don't want control flow to invert most of the time*. There is a lot of buy in for this amongst SG14 members and others with really big C++ codebases, but for STL maintainers and Boost devs it is not as widely appreciated how much more costly debugging and maintaining multi translation unit code which can invert control flow is. That said, there is recognition even amongst the hardcore of SG14 members that being able to use the standard STL rather than the EA custom STL more frequently would be useful, so if one could create small, localised islands where exception throws can happen just within that translation-unit-local island then one could use the STL just within that island. The island is then guarded by a catch all try catch because you can't safely have anything else in a noexcept function. This leads to the "sea of noexcept" design pattern described in the Outcome tutorial which is: extern outcome<void> some_public_api() noexcept; ... outcome<void> some_public_api() noexcept { try { ... STL using code ... return {}; // return empty outcome } catch(...) { // return exceptioned outcome, defaults to using std::current_exception() return make_exceptional_outcome<>(); } } All the above requires some programmer discipline, but I would argue much less programmer discipline than writing exception safe code which is correct and bugfree. Also, the tooling will catch up. clang-tidy is getting better at warning you when you forget the catch all try catch wrapping a noexcept function. I also intend, at some point, to add Outcome-awareness to clang-tidy so you'll get a much harder error if you leak exceptions out of an outcome returning noexcept function. Or, in other words, by returning outcomes the programmer really does not intend exception throws to be aliased into std::terminate() for them by the compiler. Finally back this statement of yours:
I strongly believe that this is the wrong thing to do (or at least controversial) to annotate a function that can fail-but-not-throw as noexcept. It is more in the spirit of noexcept intentions to indicate a no-fail guarantee rather than the no-throw guarantee.
On this I think you're just plain wrong. noexcept has a very specific meaning on an API for the *users* of that API: calling this API will not invert control flow. Not EVER. And you can write code calling it assuming that in the strongest sense possible. It does NOT mean that the function cannot fail. The reason why is the heritage from C. Any C function does not throw exceptions, yet they definitely can fail. I would say most C++ programmers would agree therefore that marking a function with noexcept means "no exceptions", not "no failures". So, tl;dr and all that, I agree with your opinion if I'm wearing my library developer's hat. I disagree with you if I'm wearing my library user's hat. Hopefully all that above actually made some sense. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-01-16 16:50 GMT+01:00 Niall Douglas
Many thanks in advance for any feedback received.
I intend to review the docs again. So far I had only a glimpse at the initial page, and it really looks good. I was now able to grasp the idea of the library in less than a minute.
Cool. The simplest primitives are often the hardest to convey the use case for.
One thing I wanted to bring up immediately, is not really related to your library but to a detail in the example. It uses `noexcept` to illustrate that the function does not throw itself:
``` bo::outcome<int> getConfigParam(std::string name) noexcept; ```
I strongly believe that this is the wrong thing to do (or at least controversial) to annotate a function that can fail-but-not-throw as noexcept. It is more in the spirit of noexcept intentions to indicate a no-fail guarantee rather than the no-throw guarantee.
I have provided the justification for this claim in the following post: https://akrzemi1.wordpress.com/2014/04/24/noexcept-what-for/
And conversely, only because you guarantee that a given function never throws, it does not immediately imply that a function should be declared as noexcept.
I think that your example would not suffer if the noexcept is removed. It still does the good job of illustrating the intent. But you would avoid certain controversies that might divert the reader's attention from the main point.
Most of what you write in your blog I would generally agree with and you saved Outcome from a design mistake in https://akrzemi1.wordpress.com/2014/12/02/a-gotcha-with-optional/.
But regarding https://akrzemi1.wordpress.com/2014/04/24/noexcept-what-for/ I think you're wide of the mark, and actually because I recognise that your opinion is not uncommon, it's one of the reasons why the tutorial bangs on about error handling in C++ in such lengthy detail plus presenting the error handling design patterns in does in such hand wavy and superlative language terms. Indeed I *am* trying to sell something, and it is the enormous value of making all your extern APIs noexcept which I call "sea of noexcept, islands of throwing" in the tutorial.
Quoting your blog post:
Why do you need to know?
Is this an important information for you if a given function may throw or not? If so, why? Some possible answers include the following:
1. Because if it throws and I do not catch it, std::terminate will be called and I do not want this to happen.
2. Because I need this function to provide no-throw exception safety guarantee.
3. Because this may allow certain compiler optimizations.
I agree with you that answers 1 and 3 ought to be discounted for the large majority of C++ programmers. If you are using noexcept for those reasons, then don't because it's a lousy solution to not the problem you think you have. About answer 2 you say:
If your motivation is (2), noexcept will also not help you much. Suppose you detect that the function is not declared as noexcept, what do you do? Don’t use it? It may still not throw exceptions. A function can throw nothing and still be declared noexcept(false). This is the case for the std::swap specialization for STL containers: following the recommendation from N3248 it is guaranteed not to throw, but is declared noexcept(false). For this reason, the example I gave in my other post with function nofail_swap is wrong. It doesn’t take into account that swap on STL containers is no-fail. You cannot check the no-fail or no-throw guarantee with a compile-time expression, because in some cases it is just announced informally in the documentation.
Also, if a function is declared noexcept it can’t throw, but it can call std::terminate. Do you consider this behaviour suitable for a component that is supposed to be “exception safe”?
Here is where I think you've missed the beat. In everything you say above you are 100% correct. noexcept has a lousy, poorly thought through implementation in C++ and it shows every bit of having been tacked on at the last minute in the C++ 11 standard, and I said so at the time to anyone who would listen in 2010. But do you see that the lousy implementation has nothing to do with this:
2. Because I need this function to provide no-throw exception safety guarantee.
Your argument against using noexcept to make a function provide a no-throw exception safety guarantee was all about the lousy implementation of noexcept, and nothing about whether explicitly guaranteeing that calling some function will not invert control flow is a good or bad thing.
You are absolutely right that marking a function with noexcept means it simply adds a call to std::terminate which is usually not what the programmer intended. That's that lousy implementation again, not least that this stupid piece of code:
void somehow_this_is_not_a_compile_failure() noexcept { throw "foo"; // i.e. std::terminate() }
... is legal, and worse, many compilers don't even warn, you need to run clang-tidy on it to get any indication of this being very unlikely to be what the programmer intended. But that's a problem for the library implementor, and a very different viewpoint arrives at the library *user*.
One of the main reasons you'd want to use Outcome is because *you really don't want control flow to invert most of the time*. There is a lot of buy in for this amongst SG14 members and others with really big C++ codebases, but for STL maintainers and Boost devs it is not as widely appreciated how much more costly debugging and maintaining multi translation unit code which can invert control flow is. That said, there is recognition even amongst the hardcore of SG14 members that being able to use the standard STL rather than the EA custom STL more frequently would be useful, so if one could create small, localised islands where exception throws can happen just within that translation-unit-local island then one could use the STL just within that island. The island is then guarded by a catch all try catch because you can't safely have anything else in a noexcept function. This leads to the "sea of noexcept" design pattern described in the Outcome tutorial which is:
extern outcome<void> some_public_api() noexcept; ... outcome<void> some_public_api() noexcept { try { ... STL using code ... return {}; // return empty outcome } catch(...) { // return exceptioned outcome, defaults to using std::current_exception() return make_exceptional_outcome<>(); } }
All the above requires some programmer discipline, but I would argue much less programmer discipline than writing exception safe code which is correct and bugfree.
Also, the tooling will catch up. clang-tidy is getting better at warning you when you forget the catch all try catch wrapping a noexcept function. I also intend, at some point, to add Outcome-awareness to clang-tidy so you'll get a much harder error if you leak exceptions out of an outcome returning noexcept function. Or, in other words, by returning outcomes the programmer really does not intend exception throws to be aliased into std::terminate() for them by the compiler.
Finally back this statement of yours:
I strongly believe that this is the wrong thing to do (or at least controversial) to annotate a function that can fail-but-not-throw as noexcept. It is more in the spirit of noexcept intentions to indicate a no-fail guarantee rather than the no-throw guarantee.
On this I think you're just plain wrong. noexcept has a very specific meaning on an API for the *users* of that API: calling this API will not invert control flow. Not EVER. And you can write code calling it assuming that in the strongest sense possible. It does NOT mean that the function cannot fail.
The reason why is the heritage from C. Any C function does not throw exceptions, yet they definitely can fail. I would say most C++ programmers would agree therefore that marking a function with noexcept means "no exceptions", not "no failures".
So, tl;dr and all that, I agree with your opinion if I'm wearing my library developer's hat. I disagree with you if I'm wearing my library user's hat. Hopefully all that above actually made some sense.
I am confused about the usage of term "inversion of control" in the context of throwing exceptions. Maybe I am missing something obvious; but what do you mean when you say that "calling noexcept never causes an unexpected inversion of control"? Regards, &rzej;
Finally back this statement of yours:
I strongly believe that this is the wrong thing to do (or at least controversial) to annotate a function that can fail-but-not-throw as noexcept. It is more in the spirit of noexcept intentions to indicate a no-fail guarantee rather than the no-throw guarantee.
On this I think you're just plain wrong. noexcept has a very specific meaning on an API for the *users* of that API: calling this API will not invert control flow. Not EVER. And you can write code calling it assuming that in the strongest sense possible. It does NOT mean that the function cannot fail.
The reason why is the heritage from C. Any C function does not throw exceptions, yet they definitely can fail. I would say most C++ programmers would agree therefore that marking a function with noexcept means "no exceptions", not "no failures".
So, tl;dr and all that, I agree with your opinion if I'm wearing my library developer's hat. I disagree with you if I'm wearing my library user's hat. Hopefully all that above actually made some sense.
I am confused about the usage of term "inversion of control" in the context of throwing exceptions. Maybe I am missing something obvious; but what do you mean when you say that "calling noexcept never causes an unexpected inversion of control"?
"control flow" not control. So, for example, if I call a noexcept function, I am *guaranteed* that the statement after the function call will be executed [1] (or the program is no longer running). That means I can dispense with using RAII to trap control flow inversion etc. That means fewer execution paths to check for validity and to rationalise during maintenance and peer review. Niall [1]: Actually there is a subtle trap in noexcept functions which return types without noexcept move constructors because while the function may not invert control flow, the _statement_ may invert control flow. And programmers tend to think in terms of statements, not the branch points which make up each statement. Again, only if the damn ISO standard required compilers to refuse to compile functions which could throw exceptions inside a noexcept execution context ... -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Hi,
Again, only if the damn ISO standard required compilers to refuse to compile functions which could throw exceptions inside a noexcept execution context ...
I disagree here. In many applications I would consider functions noexcept even when they can, in theory, throw. Best example is "out of memory" aka "pc lit on fire". Exception handling of rare exceptions is probably among the least tested control paths, so I would often rather terminate in the case of an "unexpected exception" than go on and gamble whether my exception handling is going to die trying to overwrite my last good program state on disk. Therefore, compiler noise like that would probably lead to random try{...}catch(...){} inside all noexcept functions rather quickly. Best, Oswin
Again, only if the damn ISO standard required compilers to refuse to compile functions which could throw exceptions inside a noexcept execution context ...
I disagree here. In many applications I would consider functions noexcept even when they can, in theory, throw. Best example is "out of memory" aka "pc lit on fire". Exception handling of rare exceptions is probably among the least tested control paths, so I would often rather terminate in the case of an "unexpected exception" than go on and gamble whether my exception handling is going to die trying to overwrite my last good program state on disk.
Therefore, compiler noise like that would probably lead to random
try{...}catch(...){}
inside all noexcept functions rather quickly.
An absolutely valid point. One of the big reasons for using something like Outcome is so one can segment really serious "this is totally unexpected" type errors like bad_alloc from the routine errors. However the point was about the quality of implementation of noexcept in C++ 11. Most on the C++ committee did (and do) not see the inversion of execution flow as anything special. They consider it as just another execution path, and hence noexcept got the unhelpful spec it did. One thing I felt noexcept really lacked was defaulting to noexcept(auto) which means "this function gets noexcept from the things it could potentially call" i.e. if everything it could call is noexcept, it too becomes noexcept and all extern "C" functions automatically get noexcept(true) applied to them. Then the compiler could examine all functions called from a noexcept(true) function and error out if you ever call anything which deduces to noexcept(false) or is noexcept(false). This behaviour would be much more useful than the present situation in persuading people to write their code correctly. I did raise this proposal with a senior committee member once and I was told that the showstopper to that idea was the STL which would have needed many breaking changes. And now C++ 17 has resurrected dynamic exception modifiers i.e. throws(int) is back, I guess the ship has sailed and we're stuck with what we've got. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-01-17 13:06 GMT+01:00 Niall Douglas
Again, only if the damn ISO standard required compilers to refuse to compile functions which could throw exceptions inside a noexcept execution context ...
I disagree here. In many applications I would consider functions noexcept even when they can, in theory, throw. Best example is "out of memory" aka "pc lit on fire". Exception handling of rare exceptions is probably among the least tested control paths, so I would often rather terminate in the case of an "unexpected exception" than go on and gamble whether my exception handling is going to die trying to overwrite my last good program state on disk.
Therefore, compiler noise like that would probably lead to random
try{...}catch(...){}
inside all noexcept functions rather quickly.
An absolutely valid point. One of the big reasons for using something like Outcome is so one can segment really serious "this is totally unexpected" type errors like bad_alloc from the routine errors.
However the point was about the quality of implementation of noexcept in C++ 11. Most on the C++ committee did (and do) not see the inversion of execution flow as anything special. They consider it as just another execution path, and hence noexcept got the unhelpful spec it did.
One thing I felt noexcept really lacked was defaulting to noexcept(auto) which means "this function gets noexcept from the things it could potentially call" i.e. if everything it could call is noexcept, it too becomes noexcept and all extern "C" functions automatically get noexcept(true) applied to them.
Just for completeness: some "C" functions do throw exceptions, like qsort if you pass it a callback that throws.
Then the compiler could examine all functions called from a noexcept(true) function and error out if you ever call anything which deduces to noexcept(false) or is noexcept(false). This behaviour would be much more useful than the present situation in persuading people to write their code correctly.
I did raise this proposal with a senior committee member once and I was told that the showstopper to that idea was the STL which would have needed many breaking changes.
And now C++ 17 has resurrected dynamic exception modifiers i.e. throws(int) is back, I guess the ship has sailed and we're stuck with what we've got.
What do you mean here? Regards, &rzej;
2017-01-11 13:53 GMT+01:00 Niall Douglas
Dear Boost,
Last November you gave me some valuable feedback on rewriting the tutorial for Boost.Outcome, a C++ 14 library providing a factory and family of policy driven lightweight monadic value-or-error transports. I recently came out of employment contract and so have had the time to rewrite Outcome's Tutorial (anyone interested in employing a remote working C++ consultant please do get in contact). I'd appreciate feedback on https://ned14.github.io/boost. outcome/:
1. Is this rewritten tutorial too simple, too detailed or about right?
2. Do you feel you could use Outcome in your own code without difficulty after reading it? If not, what information is missing?
3. Are you persuaded of the merits of using Outcome after reading it? If not, what else do I need to say?
The first FOUR sections at https://ned14.github.io/boost.outcome/ have been rewritten by me to hopefully reflect the feedback you gave me last November, so these have been refactored so far:
* Description, Prerequisites and Support * Installing, building and testing Outcome * Quick start for the expert * Tutorial
The package repo install methods haven't been implemented yet, these are marked in the docs as todo.
These sections have yet to be refactored:
* Frequently Asked Questions * Advanced usage: Outcome as a Monad * Outcome and the upcoming std::expected
Many thanks in advance for any feedback received.
Hi Niall, Here's my feedback. It is on the documentation alone. I do not comment on the usefulness on the library here. After a conversation with you in another thread I think I understand the scope and the value of the library. It is also my impression that the initial page of the docs does not reflect it clearly enough. The very first paragraph goes like this: This is the Outcome library, a Boost C++ 14 library providing a factory and
family of policy driven lightweight monadic value-or-error transports with the convenience simple specialisations of expected
, outcome<T>, result<T> and option<T>. Its main intended usage is as an ultra light weight error handling framework, providing a more expressive and type safe alternative to error code integers or enums, yet much lower overhead than exception throws.
I learn from this that: - We will be dealing with some factory (but do not know why) - There will be some policies (what for? Do I need to know about them at this point?) - There will be monads involved (word "monadic" used), and, not being a Haskell expert, I have been informed in many blogs, that "monad" is a quite difficult concept to comprehend. - There will be "transports". I am not a native English speaker, and I am confused here. What is a "transport" as a noun, in the context of programming? - There will be many specializations. But I do not know why and what for at this stage After the first sentence, I am concerned that there is a number of things I already do not know (transports, monads, policies), these will probably prevent me from understanding the rest; and I still do not know what the library is for. But now I am more irritated, and less motivated to dig further. Second sentence talks about "error handling framework". I am not sure if name "framework" fits here. The only useful information is that it deals with "error handling". Better than enums: ok. "Lower overhead that exception throws" -- in the other thread you mentioned that the main motivation was not to outperform exceptions, but to make the control flow more explicit. "If you are familiar with Swift's error code throws or Rust's" -- I think it should go to the end of the page. I am not familiar with Swift or Rust, and after monads, transports, policies, the feeling of being alienated gets reinforced. The example does not illustrate that we are "dealing" with the framework. It only illustrates that we have a new type `outcome<int>`. Maybe for an introduction, it should read something like: <blockquote> This library provides a type that can represent a function's output, which can be either of the three: - The value that the function was intended to produce, - An error condition that requires to be handled locally, - A fatal failure that cannot be handled locally (and may even result in program termination). In addition, this library comes with a handful of operators and a control statement in a form of macro BOOST_OUTCOME_TRY() that make the task of inspecting, unpacking, and conveying the outcome up the stack easy and concise. This library is useful for: 1. Discriminating between "local/expected" and "fatal" errors, and 2. For assisting the programming style, where expected/local error conditions are explicitly handled locally, or explicitly passed up the stack. </blockquote> In addition to the example that is already there, I think it would be beneficial to see, still in the first page a short example how an error is conveyed up with macro BOOST_OUTCOME_TRY(). Ok, I stop now. It came out loner that I expected. I hope this helps, though. &rzej;
Hi Niall, Here's my feedback. It is on the documentation alone. I do not comment on the usefulness on the library here. After a conversation with you in another thread I think I understand the scope and the value of the library. It is also my impression that the initial page of the docs does not reflect it clearly enough.
Bah I'd forgotten to commit the updated v3 rewrite edition of that initial page. Sorry. Here's the initial page now:
This is the Outcome library, a Boost C++ 14 library providing an implementation of expected
(which is on the C++ 20 standardisation track), with refinements of expected for large C++ codebases outcome<T> and result<T>. Its main intended usage is as an ultra light weight error handling framework, providing a more expressive and type safe alternative to error code integers or enums, yet much lower overhead than exception throws. Unlike alternative implementations, it works perfectly with exceptions and RTTI disabled and thus is suitable for low-latency/games/finance/SG14 users. One could view Outcome as a minimum overhead universal outcome handling framework for C++, hence being named "Outcome". If you are familiar with Swift's error code throws or Rust's Result<T>, you will find almost identical semantics in the transports provided here. One can therefore write systems programming code using these transports in C++ in the same design pattern as when writing in Rust or Swift, and with a similarly low runtime overhead. Outcome even has a BOOST_OUTCOME_TRY macro doing most of the try keyword in Rust and Swift!
Back to your feedback:
- We will be dealing with some factory (but do not know why) - There will be some policies (what for? Do I need to know about them at this point?)
Any mention of factories or policies is being excised from the documentation right now.
- There will be monads involved (word "monadic" used), and, not being a Haskell expert, I have been informed in many blogs, that "monad" is a quite difficult concept to comprehend.
I'm currently in the process of purging all mention of "monad" too in the docs.
After the first sentence, I am concerned that there is a number of things I already do not know (transports, monads, policies), these will probably prevent me from understanding the rest; and I still do not know what the library is for. But now I am more irritated, and less motivated to dig further.
If your irritation is now improved, what do you think of the new and
completely rewritten for a third time tutorial part A at
https://ned14.github.io/boost.outcome/md_doc_md_02-tutorial_a.html. Part
A is 100% a tutorial on LEWG Expected and has nothing to do with Outcome
at all. Part B will be on why you shouldn't do all the things you think
you should do with expected
Second sentence talks about "error handling framework". I am not sure if name "framework" fits here. The only useful information is that it deals with "error handling". Better than enums: ok. "Lower overhead that exception throws" -- in the other thread you mentioned that the main motivation was not to outperform exceptions, but to make the control flow more explicit.
Good point. I'll remove the mention of overhead.
"If you are familiar with Swift's error code throws or Rust's" -- I think it should go to the end of the page. I am not familiar with Swift or Rust, and after monads, transports, policies, the feeling of being alienated gets reinforced.
A very large part of this library's userbase wants this stuff from Rust
and Swift into C++. You should have a look at the Reddit threads. I've
also got quite a bit of private email on the topic. Lots of very
enthusiastic people for expected
The example does not illustrate that we are "dealing" with the framework. It only illustrates that we have a new type `outcome<int>`.
You make a good point that "framework" is confusing. It's a good suggestion to remove mention of frameworks too. I will do this.
Maybe for an introduction, it should read something like:
<blockquote>
This library provides a type that can represent a function's output, which can be either of the three:
- The value that the function was intended to produce, - An error condition that requires to be handled locally, - A fatal failure that cannot be handled locally (and may even result in program termination).
In addition, this library comes with a handful of operators and a control statement in a form of macro BOOST_OUTCOME_TRY() that make the task of inspecting, unpacking, and conveying the outcome up the stack easy and concise.
This library is useful for:
1. Discriminating between "local/expected" and "fatal" errors, and 2. For assisting the programming style, where expected/local error conditions are explicitly handled locally, or explicitly passed up the stack.
</blockquote>
This is a nice description. Thank you. But as you'll see in v3 of the docs, I've completely rearchitected the message. Now Outcome looks like it's primarily an implementation of LEWG Expected, and it provides a few refinements of Expected which most users won't care about or will refuse to use.
In addition to the example that is already there, I think it would be beneficial to see, still in the first page a short example how an error is conveyed up with macro BOOST_OUTCOME_TRY().
I'm very hesitant to use that macro in an example without explanation of its semantics. Tutorial part B explains it.
Ok, I stop now. It came out loner that I expected. I hope this helps, though. &rzej;
Not only has your feedback and discussion been very valuable, I don't think the v3 tutorial would look like it does now without you. Do let me know if the v3 tutorial now makes sense to you. I even dove into "noexcept as can't fail" vs "noexcept can fail" as a nod to you :) Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-01-25 14:56 GMT+01:00 Niall Douglas
If your irritation is now improved, what do you think of the new and completely rewritten for a third time tutorial part A at https://ned14.github.io/boost.outcome/md_doc_md_02-tutorial_a.html. Part A is 100% a tutorial on LEWG Expected and has nothing to do with Outcome at all. Part B will be on why you shouldn't do all the things you think you should do with expected
, and why to use Outcome instead. Part B is still a work in progress.
Ok, so I had a short glimpse at Part A. Actually, the first thing I did was
to scroll down to the example showing how I will be using this library.
Again, I feel uncomfortable about the choice of the example (so I changed
the irritation into non-comfort). The situations that are tested, I would
classify them as precondition violations, and I wouldn't think of checking
them in return value. If this was a real program, and I didn't trust the
values of x and y, I can check the preconditions prior to invoking
functions:
```
double op(double x, double y)
{
if (y == 0.0) // or "close
{
std::cerr << "PANIC: MatchResult::DivisionByZero" << std::endl;
std::terminate();
}
double ratio = unchecked::div(x, y);
if (ratio < 0.0)
{
std::cerr << "PANIC: MatchResult::NegativeLogarithm" << std::endl;
std::terminate();
}
double ln = unchecked::ln(ratio);
if (ln < 0.0)
{
std::cerr << "PANIC: MatchResult::NegativeSquareRoot" << std::endl;
std::terminate();
}
return checked::sqrt(ln);
}
```
I mean, if it is equally easy to check the condition before and after the
function, it is better to do it before, and I do not have to "pollute" the
return value with potential error conditions. I think the value of these
expected<> types becomes clear, when we cannot see the erroneous situation
from the arguments. Maybe a better illustration of expected<> with a code
would the following situation:
Upon closing the application, I store its state in a text file. When I
reopen the application the next time, I load the state from the file. Here
is the function:
expected
On 25/01/2017 16:22, Andrzej Krzemienski wrote:
Ok, so I had a short glimpse at Part A. Actually, the first thing I did was to scroll down to the example showing how I will be using this library. Again, I feel uncomfortable about the choice of the example (so I changed the irritation into non-comfort). The situations that are tested, I would classify them as precondition violations, and I wouldn't think of checking them in return value. If this was a real program, and I didn't trust the values of x and y, I can check the preconditions prior to invoking functions:
I couldn't agree more. It's an example of bad design and programming. But I had at least five people email me saying "please put the Rust example of use of Result side by side with an example of C++ Expected".
I mean, if it is equally easy to check the condition before and after the function, it is better to do it before, and I do not have to "pollute" the return value with potential error conditions. I think the value of these expected<> types becomes clear, when we cannot see the erroneous situation from the arguments. Maybe a better illustration of expected<> with a code would the following situation:
Upon closing the application, I store its state in a text file. When I reopen the application the next time, I load the state from the file. Here is the function:
expected
state = load_state(); And now, the error conditions are: * file missing * file is not a text file * file contains erroneous contents
And I might want to respond to each of these conditions in a different way.
The first version of the tutorial had a reduced real world use example from AFIO's implementation of opening a file where we perform a series of syscalls and do different stuff depending on the result<T>'s returned. Everybody hated it, they said it was too confusing. Yet in fact the use of result<T> has *greatly* simplified that code. In AFIO v2 it's merely dozens of lines, in AFIO v1 it was approaching a thousand lines so the use of Outcome has been a big improvement in readability and maintainability. I suspect people want contrived toy examples rather than examples of what you'd actually write. I remember thinking the same about the ASIO tutorial, and I personally speaking found the ASIO tutorial so reduced in real world detail as to be confusing at best, productivity damaging at worst. You end up having to grok through the ASIO source code to figure out answers to stuff. Not a good tutorial. So I'm really not sure what to do. I've slowed down the pace of the tutorial several fold over the original now to the point where it's become anodyne. I've reduced the length of the code examples so they always fit onto a screen because according to feedback anything longer than a single screen is "too long". I've also ramped up the frequency of code examples because my prose is "too confusing" and "you need lots more C++ code examples". But all this hand holding comes with the cost that the code examples become really contrived, and the tutorial has become so long it needs three parts, which is frankly ridiculous for something so simple as a slightly enhanced std::optional<T> with a bit of std::variant<...> muxed in. I really don't get what's so hard here, just throw std::optional and std::variant into a bowl and apply a blender. All the APIs and semantics stay the same and there are no surprises in behaviour, it's all STL idiomatic. You're done. I'm still aiming to end Tutorial part C with a real world code example that is actually realistic and taken from actual use case. It will, necessarily, span multiple screens. But I suspect I won't be allowed anything but daft examples in the middle. Sorry. But thanks for your feedback. Much appreciated and keep it coming! Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-01-26 12:15 GMT+01:00 Niall Douglas
On 25/01/2017 16:22, Andrzej Krzemienski wrote:
Ok, so I had a short glimpse at Part A. Actually, the first thing I did was to scroll down to the example showing how I will be using this library. Again, I feel uncomfortable about the choice of the example (so I changed the irritation into non-comfort). The situations that are tested, I would classify them as precondition violations, and I wouldn't think of checking them in return value. If this was a real program, and I didn't trust the values of x and y, I can check the preconditions prior to invoking functions:
I couldn't agree more. It's an example of bad design and programming. But I had at least five people email me saying "please put the Rust example of use of Result side by side with an example of C++ Expected".
Wow. Indeed Rust docs do it. This is what you are referring to: http://rustbyexample.com/std/result.html This example doesn't do good service to Rust, does it?
I mean, if it is equally easy to check the condition before and after the function, it is better to do it before, and I do not have to "pollute" the return value with potential error conditions. I think the value of these expected<> types becomes clear, when we cannot see the erroneous situation from the arguments. Maybe a better illustration of expected<> with a code would the following situation:
Upon closing the application, I store its state in a text file. When I reopen the application the next time, I load the state from the file. Here is the function:
expected
state = load_state(); And now, the error conditions are: * file missing * file is not a text file * file contains erroneous contents
And I might want to respond to each of these conditions in a different way.
The first version of the tutorial had a reduced real world use example from AFIO's implementation of opening a file where we perform a series of syscalls and do different stuff depending on the result<T>'s returned. Everybody hated it, they said it was too confusing. Yet in fact the use of result<T> has *greatly* simplified that code. In AFIO v2 it's merely dozens of lines, in AFIO v1 it was approaching a thousand lines so the use of Outcome has been a big improvement in readability and maintainability.
I suspect people want contrived toy examples rather than examples of what you'd actually write. I remember thinking the same about the ASIO tutorial, and I personally speaking found the ASIO tutorial so reduced in real world detail as to be confusing at best, productivity damaging at worst. You end up having to grok through the ASIO source code to figure out answers to stuff. Not a good tutorial.
I think, what other libraries do is to put real-life practical examples as source code in `examples` folder. You can compile and run them to verify if they are really correct, and you can also see how to get headers and namespaces right.
So I'm really not sure what to do. I've slowed down the pace of the tutorial several fold over the original now to the point where it's become anodyne. I've reduced the length of the code examples so they always fit onto a screen because according to feedback anything longer than a single screen is "too long". I've also ramped up the frequency of code examples because my prose is "too confusing" and "you need lots more C++ code examples". But all this hand holding comes with the cost that the code examples become really contrived, and the tutorial has become so long it needs three parts, which is frankly ridiculous for something so simple as a slightly enhanced std::optional<T> with a bit of std::variant<...> muxed in. I really don't get what's so hard here, just throw std::optional and std::variant into a bowl and apply a blender. All the APIs and semantics stay the same and there are no surprises in behaviour, it's all STL idiomatic. You're done.
LOL. This indeed must be very frustrating. The expectations of different people are just contradictory. The way you put it now, "a slightly enhanced std::optional<T> with a bit of std::variant<...> muxed in", is in fact a quite simple and illustrative description of the library. Maybe the problem here was with how this library was initially introduced: as "monads" and "transports".
I'm still aiming to end Tutorial part C with a real world code example that is actually realistic and taken from actual use case. It will, necessarily, span multiple screens. But I suspect I won't be allowed anything but daft examples in the middle. Sorry.
When I first learned about Boost. I was amazed by the documentation the libraries offer. Quite a number of the libraries has a very nice documentation, which describe not only the library, but also the problem domain the library addresses, why the alternatives are inferior. I learned quite a lot about type safety, generic programming, exception safety, and C++ in general only from reading Boost docs. I have also learned a number good idioms and programming practices. It would be a shame if the library shows in the tutorial page (one of the first we read) an example of a bad programming practice. Some people my get the message that this is how we encourage you to write your code. If someone wants to have a comparison with a poor but popular Rust example, I do see value in it, but maybe not as the flag example of your library. Maybe put it somewhere further, in section dedicated to Rust users. I know people come from different backgrounds and with different preferences. Mine is: show me first a trivial example, than a couple of medium-sized example, finally, show me a real-life example and reference. I know, I am complaining a lot. But all-in-all, I find the current iteration of the docs acceptable: they provide initial motivation that is sufficient to dig further. Regards, &rzej;
On Thu, Jan 26, 2017 at 4:03 PM, Andrzej Krzemienski
Wow. Indeed Rust docs do it. This is what you are referring to: http://rustbyexample.com/std/result.html This example doesn't do good service to Rust, does it?
The implementation might be bad but I don't think the interface is bad, assuming you want to handle the errors as runtime errors.
2017-01-26 16:18 GMT+01:00 Olaf van der Spek
On Thu, Jan 26, 2017 at 4:03 PM, Andrzej Krzemienski
wrote: Wow. Indeed Rust docs do it. This is what you are referring to: http://rustbyexample.com/std/result.html This example doesn't do good service to Rust, does it?
The implementation might be bad but I don't think the interface is bad, assuming you want to handle the errors as runtime errors.
If you want to report overflow or precision errors, then this `MathResult` makes sense. But this example illustrates some bad practice of artificially widening the contract only to report the breakage in the return value. It is as though it were saying "here is how you can use Rust's Result to write poor code". Ok, but does it help me write good code? Regards, &rzej;
On 1/25/17 5:56 AM, Niall Douglas wrote:
<blockquote>
This library provides a type that can represent a function's output,
I would use the phrase "can represent a function's return value" or perhaps "result". When I read this I substituted the word "outcome" in my brain and it seemed to me tautological.
...
</blockquote>
This is a nice description. Thank you. But as you'll see in v3 of the docs, I've completely rearchitected the message. Now Outcome looks like it's primarily an implementation of LEWG Expected, and it provides a few refinements of Expected which most users won't care about or will refuse to use.
Well, how about just calling it "expected" and "selling" it as an alternative to std::expected? There's lot's of precedent for this. We have boost::shared_ptr, boost::enable_if, etc. These have features that the std versions don't have and sometimes are indispensable for this reason.
Not only has your feedback and discussion been very valuable, I don't think the v3 tutorial would look like it does now without you.
make sure that your documentation has an acknowledgements section so you can mention this.
Do let me know if the v3 tutorial now makes sense to you. I even dove into "noexcept as can't fail" vs "noexcept can fail" as a nod to you :)
I'm gratified that you've taken the task of documentation as an integral part of developing the library. I've been flogging this idea in the boost incubator. I've got a lot to say about this there. Robert Ramey
This is a nice description. Thank you. But as you'll see in v3 of the docs, I've completely rearchitected the message. Now Outcome looks like it's primarily an implementation of LEWG Expected, and it provides a few refinements of Expected which most users won't care about or will refuse to use.
Well, how about just calling it "expected" and "selling" it as an alternative to std::expected? There's lot's of precedent for this. We have boost::shared_ptr, boost::enable_if, etc. These have features that the std versions don't have and sometimes are indispensable for this reason.
I would think doing that unwise: i) I'm not on the LEWG (can't justify the expense). ii) Mine is not the reference implementation of Expected which ought to take primacy. iii) I am going to explicitly promise that my Expected WILL change to match the LEWG propsal as it evolves, breaking the code of anyone who uses it. If people don't like that, use outcome<T> or result<T>, those won't change. iv) I believe Vicente still intends to bring the reference Expected to Boost at a later date. All his efforts are currently being expended on WG21 work. But I could see a Google Summer of Code this summer being just that for example. I personally speaking believe anyone using unrestricted Expected directly needs their head examined, but there are an awful lot of people who think unrestricted Expected usage is an amazing thing. They need to be convinced they are wrong. Hence Outcome is really a "please don't use Expected directly, use these instead" library but also one handing them the noose of unrestricted Expected usage if they really, really want it.
Not only has your feedback and discussion been very valuable, I don't think the v3 tutorial would look like it does now without you.
make sure that your documentation has an acknowledgements section so you can mention this.
Good point.
Do let me know if the v3 tutorial now makes sense to you. I even dove into "noexcept as can't fail" vs "noexcept can fail" as a nod to you :)
I'm gratified that you've taken the task of documentation as an integral part of developing the library. I've been flogging this idea in the boost incubator. I've got a lot to say about this there.
I would more describe it as a cross to carry whilst being whipped on the road to Golgotha. Lucky I'm Catholic. I'm reaching the point where I'd prefer a broken arm to rewriting the damn tutorial yet again, the fact people need stuff spelled out to them in such incremental and precise detail and can't just mentally fill in the very obvious small gaps I find extremely frustrating. Still, as you would say Robert, that's the price of good documentation. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 1/26/17 2:05 AM, Niall Douglas wrote:
I'm gratified that you've taken the task of documentation as an integral part of developing the library. I've been flogging this idea in the boost incubator. I've got a lot to say about this there.
I would more describe it as a cross to carry whilst being whipped on the road to Golgotha. Lucky I'm Catholic. I'm reaching the point where I'd prefer a broken arm to rewriting the damn tutorial yet again, the fact people need stuff spelled out to them in such incremental and precise detail and can't just mentally fill in the very obvious small gaps I find extremely frustrating.
Still, as you would say Robert, that's the price of good documentation.
** warning ** off topic rant you've probably been exposed to before. TL;DR I think you're feelings about writing documentation are widely shared. And I think this is big problem for everyone. It works well for me to write the tutorial while I'm writing the code. I go back and forth between example, reference and code more or less at whim. I think of the tutorial and reference are aspects of the code. The reference is basically a form filling exercise - concepts, types and free functions. The code is types and free functions. When I start coding I don't really plan much - it's sort of free form. I write some code and write an example to be able to compile it. I have to do this because it's header only code. When I think I like it, I add the tutorial narrative and fill out the type/function form. Of course at this point I discover a lot of errors. Often the concepts need to be shuffled around, and this ripples through the code. At this point, it's still a small project so this isn't too painful. I've now convinced myself that this is going to be easy - all I have to do is add a few more examples and make sure they work. Of course this an illusion. Like I'm just going to pull this one loose thread out of my sweater. BUT I do have one thing "done" so I can't throw away my "finished" work and I'm motivated to push on. For this to work one needs to have a painless way to make documentation as one proceeds without having to get sucked into a lot issues like presentation, organization, etc, etc. For me Boost Book has worked for me to isolate me from this "noise". I have great tool for editing boost book with a wysiwyg editor so it's quite painless. It's really part of tool setup. IDE for code, BoostBook/XMLMind for documentation, Git for revision control of both the code and documetation. They are always in sync and I can go back, branch etc. The only really weakspot and source of pain is CMake/b2 which always cause problems. Normally programmers write all the code and then try to document it. This is agonizingly tedious. In practice, the documentation ends up as an after thought is is out of sync with the code - and it shows. Also writing the docs and tutorials always reveals problems in the library design. But now it's so late in the process and the ripple effect is so large, it's just to painful to fix and we just say "good enough". I spent too much time on this already. This probably explains why 99% of "open source" code has not documentation at all. Robert Ramey
Niall
On 1/26/17 2:05 AM, Niall Douglas wrote:
I would more describe it as a cross to carry whilst being whipped on the road to Golgotha. Lucky I'm Catholic. I'm reaching the point where I'd prefer a broken arm to rewriting the damn tutorial yet again, the fact people need stuff spelled out to them in such incremental and precise detail and can't just mentally fill in the very obvious small gaps I find extremely frustrating.
:) I think we can all feel with you. Nevertheless, I think writing tutorials is a useful way to get into the user's head. For some reason, it is very hard to put yourself in the shoe of the novice once you understood something. That's universally true for everyone. We also need to accept that some people pick up things up faster and others slower. There are simply vastly different levels of experience out there. Everything is easy once you mentally embraced it, but we need to remember how we felt when we tried to learn a new library and what helped us most. It can also help with realising flaws in the design of the user interface, as Robert pointed out. I like what the Zen of Python has to say about this: "If the implementation is hard to explain, it's a bad idea." "If the implementation is easy to explain, it may be a good idea." I would replace "implementation" with "interface", because we usually allow the implementation to be more messy and complicated, but the user-visible part at least should be intuitive and consistent. Hans
participants (7)
-
Andrzej Krzemienski
-
Gonzalo BG
-
Hans Dembinski
-
Niall Douglas
-
Olaf van der Spek
-
Oswin Krause
-
Robert Ramey