6 ms·
Just thinking about the example... Let's say... just use excepions! This code std::optional<image_view> get_cute_cat (image_view img) { auto croppe
by _ZeD_ 9y ago
Just thinking about the example... Let's say... just use excepions! This code
std::optional<image_view> get_cute_cat (image_view img) {
auto cropped = find_cat(img);
if (!cropped) {
return std::nullopt;
}
auto with_tie = add_bow_tie(*cropped);
if (!with_tie) {
return std::nullopt;
}
auto with_sparkles = make_eyes_sparkle(*with_tie);
if (!with_sparkles) {
return std::nullopt;
}
return add_rainbow(make_smaller(*with_sparkles));
}
Becomes
image_view get_cute_cat (image_view img) {
auto cropped = find_cat(img);
auto with_tie = add_bow_tie(*cropped);
auto with_sparkles = make_eyes_sparkle(*with_tie);
return add_rainbow(make_smaller(*with_sparkles));
}
And it could throw a no_cat_found, a cannot_see_neck, or a cat_has_eyes_shut excepion
- HumanDrivenDev 9y agoIt's not a great usecase for optionals. a better one would be retrieving a value from a std::map - that one is pretty binary, it's either there or it isn't. To me optionals are for things where you'd consider it reasonable to return a null that represents "doesn't exist".
- Shoothe 9y agoBut if you already have null then the method could return null, empty optional or optional with value.
- HumanDrivenDev 9y agoI don't know about C++, but for example Java has a method "ofNullable" that will convert a null to a None option. But yes, that could potentially happen. IIRC that problem exists in many languages, including F# and Scala. Not sure how big of an issue it is in practice...
- steveklabnik 9y agoTranslated to today's Rust, this code looks like: fn get_cute_cat(img: image_view) -> Option<image_view> { let cropped = find_cat(img)?; let with_tie = add_bow_tie(cropped)?; let with_sparkles = make_eyes_sparkle(with_tie)?; add_rainbow(make_smaller(with_sparkles)) } Basically, with the same amount of boilerplate as your exception-based solution, just three ?s. If you prefer, you could also do fn get_cute_cat(img: image_view) -> Option<image_view> { find_cat(img).and_then(|cropped| add_bow_tie(cropped).and_then(|with_tie| make_eyes_sparkle(with_tie).and_then(|with_sparkles)| add_rainbow(make_smaller(with_sparkles)) ) ) ) } ... but that has rightward drift issues. I _think_ you could do fn get_cute_cat(img: image_view) -> Option<image_view> { find_cat(img) .and_then(add_bow_tie) .and_then(make_eyes_sparkle) .and_then(|with_sparkles| add_rainbow(make_smaller(with_sparkles)) ) ) } but I'm not 100% sure without a comparable example. I bring this up because I don't believe the thing you're seeing is inherent to this style of error-handling, but instead to the language's support for these kinds of monadic patterns. Haskell would look even more succinct than the Rust!
- kindfellow92 9y agoI agree with your final point. Sadly Rust’s model for errors is tied to a specific implementation. That is, you must issue check instructions after every function call. In C++, errors are truly zero-cost: Exceptions only cause overheard when they happen. (Also in C++, different exception handling implementations are supported) Rust could do something like this but it would need syntactic support for something like “throw.” In this hypothetical world, on throw the compiler could generate code that does the unwinding and all that. When a normal value is returned, it would just return as normal. The function call ABI would be one such that functions that returned Result<T> and returned normally would be assumed to have succeeded. Actually Result<T> wouldn’t need to be a real type, just decoration at the syntax level.
- Rusky 9y ago> Exceptions only cause overheard when they happen. They only execute instructions when they happen... but they still inhibit the optimizer when they don't. Calls that may throw act like branches in the control flow graph. They also introduce more code bloat for their landing pads. > Rust’s model for errors is tied to a specific implementation. The Rust ABI is unspecified for a reason. We're currently seeing a lot of optimization around struct and enum layout; I'm not sure why this couldn't be applied to the calling convention without any extra syntax for "throw."
- danbruc 9y agoThe author kind of sabotaged his point here. Optional values are a greate choice if you expect the operation to fail and if you expect the consumer to do something meaningful in case of a failed operation. But if the the consumer can't really do anything in case of a failed operation it becomes a burden to deal with the optional values and you have to bring in additional machinery to keep your code clean and propagate the failure transparently one level up the call stack. With exceptions on the other hand propagating the failure up the call stack is the default behavior not requiring additional code unless you can and want to deal with the failure. As so often it seems to me that you don't want one or the other but the right one for the situation. The author unfortunately picked an example that seems a much better fit for exception because in his example he is unable to respond to the possible failures and just wants to propagate it on level up the call stack.
- zmj 9y agoA method shouldn't know whether its consumer can do something meaningful on failure. Return an error value and let them decide.
- danbruc 9y agoYou are not writing a function in a vacuum. You make choices about how arguments are passed into the function and choices about how results are returned from the function including errors. Optional values and exceptions offer different trade-offs and you should pick the one that is probably most useful and most easy to work with. What do I know what the caller will do with my return value, I will just return a XML string with the serialized result and let them do with that whatever they want is probably not a good design philosophy to adopt either.
- Terr_ 9y agoI think there is also a niche for checked exceptions, which kind of fall in the middle between those two approaches. A checked-exception forces the programmer to notice and make a design-decision. At the same time, if the designer chooses to merely propagate it up the stack, that's extremely easy to do just by editing the function signature.
- kccqzy 9y agoThe syntactic baggage is bothering me too. Sometimes this functional style is syntactically too heavyweight if your language doesn’t support it very well. Compare that example translated to Haskell: getCuteCat img = findCat img >>= addBowTie >>= makeEyesSparkle >>= addRainbow . makeSmaller No more manually checking whether a value is null or not, and the chaining Simply Just Works. Syntactically lightweight and conveys the important information without line noise. Yes this example uses monads, and there’s a reason why monads aren’t that popular in other languages. Most of the time you also need parenthesis-less function application, currying, and lightweight lambdas to make monad-heavy code syntactically acceptable. Yes Rust has .and_then which is same as >>= (sometimes also called flatMap or bind), but still the syntax isn’t lightweight enough. I wish people could just stop copying this idea from Haskell and use what’s reasonable in their language. Shoehorning monads and monad-heavy code into a typical language isn’t reasonable.
- greydius 9y agoA lot of people dislike the terseness of that style (especially the use of operators.) I'm not one of those people, but I can understand their point of view. Fortunately, Haskell also lets us write code in a way that managers can understand. getCuteCat img = do cropped <- findCat img withTie <- addBowTie cropped withSparkles <- makeEyesSparkle withTie return (addRainbow (makeSmaller withSparkles))
- davidgay 9y agoHowever this style breaks down as soon as you have to do something more complicated than thread a single value, or have to do something more complicated than pass failures through.
- runeks 9y agoCode can always be broken up into simpler sub-components. The more you logically compartmentalize your code, the more it lends itself to these simple transformations. At the end of the day, you — not the problem you’re trying to model — decide how much you want to do in each function.