It requires several additional lines of code just to bubble up an error, for starters, and there's nothing stopping you from ignoring errors and continuing with what could easily be corrupt data.
"there's nothing stopping you from ignoring errors and continuing with what could easily be corrupt data."
In theory, this is a big deal.
In practice, it doesn't seem to be a problem. I've neither hit this very often myself, nor have I seen even newbies have much problem with it.
A lot of error handling procedures are based on reacting to C, which was awful. You could call a function, and then have to call another function, deliberately, to see if it failed. This is a nightmare, absolutely. A "Result" type does indeed solve the problem, but the fact that it solves the problem doesn't mean it is the only solution to the problem. The Go solution seems to be about 99.99% effective. It isn't a 100% solution, no, but by 99.9% or 99.99% or so, it takes it below the level of problem that I care about.
The issue with "bubbling" is a bigger problem in practice, certainly.
> A lot of error handling procedures are based on reacting to C, which was awful
Go's design decisions in general make a lot more sense from this perspective. "X was horrible to deal with in C, how can we make a (reactively) better version?"
The problem is a lot of these choices (willfully?) ignore the decades of language innovation that have happened since C. They are incremental, reactive improvements, where it doesn't feel like the designers necessarily stepped back and looked at the bigger picture
I've ran into bugs multiple times because I ignored an error result, or overwrote the "err" variable and swallowed an error. Errcheck helps a bit.
Other languages that have exceptions that bubble up the stack have a few advantages (easier to instrument with monitoring, stack traces and line numbers out of the box) but developers often misuse error handling as flow control
my golang code is littered with err handling etc, it’s easy to miss something over the course of dozens of prs. This is something best caught at compile time or by a linter. Or tests which I am often lacking
I find code review unreliable at best for catching bugs or logic errors, but depends on the reviewer
The latter is a much stronger argument than the former (no idea why people get so worked up about character counts), but even then, "shit" is really strong considering how often one experiences exception traces when using an application written in Python or Java or some other exception-based language. Point being, we should probably evaluate error handling schemes based on results rather than ideology (even though I tend to agree with some of that ideology).
Screen real estate is limited, especially vertical real estate. Compared to languages with saner error handling, I can read approximately 25% as much Go code at once. That's a real cognitive burden when maintaining code or learning your way around a new codebase, which seems especially egregious from a language whose community consistently proselytizes about how the lack of language features is great for maintainability and onboarding.
I'd take exceptions any day of the week over Go's solution. I'd much rather the program crash by default than attempt to continue with corrupt data by default. I'd rather have concise, explicit, compiler-required error handling than exceptions, though.
I don't think "screen real estate" is the right argument here.
The problem is just that every line creates cognitive load and there's a tradeoff between concision and descriptiveness.
A language with piles of syntactic sugar and magic gets it wrong with too much concision and can read like line noise when it gets overused.
Go goes the other way though and makes it way too verbose and just makes it difficult to read the code. When a method needs to have 6 different error handling clauses in it, then it isn't as clear that 5 of those just bubble up the error while one of them has some unique handling. It also increases the chances that some programmer copypastas the boilerplate bubble-up code to all six of those cases and it sails through PR review. You can write a static analysis linter to force programmers to always handle errors and not ignore them, but you can't force them to handle errors correctly. When humans are reading the code, concision helps and verbosity hurts -- up until that crossover point where magic causes readability to suffer.
Go programmers seem to focus on abhoring magic and rejecting the benefits of concision. But when it comes to PR review your job is to stare at the whole method (or the whole file) and be able to "see" the bug, and more lines of code will make this job more difficult (and is also why some of the recommendations of the "clean code" book are pretty bad since extracting more tiny little methods can harm overall readability). There's a happy optimum somewhere where cognitive load is minimized. That isn't attained though by just having the simplest language design possible and offloading complexity into more verbose code.
I disagree. IMO, there's much more cognitive load in parsing dense, "minified" code than there is in scanning code whose control flow mirrors its visual structure. Humans are very good at seeing visual structure (which is why we tend to indent, split code across lines, and other syntactically insignificant usage of whitespace). By convention in most mainstream programming languages, this visual structure mirrors code flow, so we can see the control flow at a glance; however, many languages have special hidden control flow (exceptions) or control flow which is otherwise isn't part of the visual structure and thus easily overlooked at a glance (e.g., Rust's `?`). In my opinion, this "hidden" control flow allows more errors to slip past reviewers (though some languages might recoup some quality by other means).
So, the thing specifically about ? in a language with Result is that you can read some code that uses it and not worry about what happens for Error cases if that's not currently your focus - the question marks aren't a "Look at me!" focus the way something like try-catch is.
But if you are wondering about Error cases, they are there to see when you're looking for them because that ? while unobtrusive is something you can look for.
I'm sure in most IDEs you could have it highlight ? in a "Looking for error handling" mode if that's what you want.
Note that Rust does not consider control flow to be something the core language owns exclusively, you can return core::ops::ControlFlow to say actually I also have an opinion about whether you should keep going, this can make sense for a closure or function intended to be called inside an iterator or other loop context. Some of the ergonomics for this aren't finished, but what is there is already useful where a Result would work but is ugly because your early exit scenario isn't in fact an error at all.
I mean I spent quite a few words talking about how there's a happy optimum where beyond that you start to get too much magic and code gets too terse and unreadable.
You just did prove my point though which is that this is the only argument that Go programmers consider, and they blindly reject that adding more lines of code can harm readability.
I don't think not handling errors after every single method call, makes the code dense. Its just way easier to read. 99% of the time you're just going to wrap the error in your own error and return so why not just have a single place that does that?
focusing on a pedantic detail that i clearly didn't intend and which doesn't change my point.
consider it from a blocks-of-code metric, or some better slightly more abstract metric, that isn't affect by simple things like whitespace transformations, and try assuming that we all understand that we should write code that isn't monstrous to begin with.
> Screen real estate is limited, especially vertical real estate. Compared to languages with saner error handling, I can read approximately 25% as much Go code at once.
In my experience, people can't actually read everything on the screen at one time anyway, and the more dense/terse things are the harder it is to read (otherwise we would minify everything).
> I'd much rather the program crash by default than attempt to continue with corrupt data by default.
It's not likely that it will continue with corrupt data because you can't use the return value without explicitly ignoring the error. It's not perfect, because there are cases where you want to crash when there is an error but no return value, and Go doesn't help you here. I would like to see this improve, but it's relatively low on my list of qualms with Go (I would rather have sum types, for example). It certainly isn't worth changing languages over especially since, in practice, Go seems to have fewer error handling bugs than exception-based languages.
> In my experience, people can't actually read everything on the screen at one time anyway, and the more dense/terse things are the harder it is to read (otherwise we would minify everything).
Whether or not you can read everything on the screen at one time is missing the point entirely. The point is that context matters, and the more frequently you have to scroll to find it is more cognitive burden.
> It's not likely that it will continue with corrupt data because you can't use the return value without explicitly ignoring the error.
It is far too easy to accidentally do the wrong thing with an error in Go. In Rust, for example, no matter what you want to do with the result of a fallible call, you have to do it explicitly. If you want to crash on error, you `.unwrap()`; if you want to bubble it up, you `?`; if you want to continue with a default value, you `.unwrap_or()` or one of its variants.
> in practice, Go seems to have fewer error handling bugs than exception-based languages
> Whether or not you can read everything on the screen at one time is missing the point entirely. The point is that context matters, and the more frequently you have to scroll to find it is more cognitive burden.
And I disagree. Scrolling IMO is a lot easier than squinting to parse dense code. We have visual structure (indentation blocks and so on) for a reason. The visual structure aids in readability, and indentation blocks help the eye scan quickly over a document. The visual structure in most languages resembles control flow, except some languages make an exception (no pun intended) to this rule for error handling paths which are not easy to see at a glance.
> > in practice, Go seems to have fewer error handling bugs than exception-based languages
>
> This is based on?
By explicitly annotating functions as fallible the language hints to the programmer that errors need to be accounted for.
With exceptions, the hints only appear at runtime - when your program crashes. There's nothing that nudges you towards handling errors at the point of writing code, so you end up with brittle software.
Additionally, the more repetitive code there is, the more opportunities there are for some subtle difference to be lurking in one particular chunk. And with pervasive boilerplate it becomes easier to eyeglaze past that subtle difference. Whether that difference is a bug or intentional, it's important to have code that highlights it by default.
> Screen real estate is limited, especially vertical real estate.
meh, my ide squashes short if `err != nil` clauses (goland, but I've seen other editors/ides/golang plugins do this as well ), also I run a vertical monitor. It's just not enough of an issue to care about. I've seen similar features for editors for other languages that have features or patterns that also create a lot of 'extra bullshit that takes up screen realestate'.
> no idea why people get so worked up about character counts
Think of reading code as mining ore. If the ore is rich, you don't have to mine and process nearly as much of it to get the material you need. If the ore is poor, you have to invest extra effort to mine more ore to get the same amount of refined material.
You might think Go is easy to read because lines are individually very easy to read, but Go code is so information-poor (partly because of error handling boilerplate) that you have to read a lot more lines of it to understand what a system does compared to other languages. Quantity has a quality all its own, and Go does bog you down with its sheer line count. I'm not one who often appeals to this argument, by the way; the only other language I've done significant work in that I would apply it to would be C. It's very common to write bloated, information-poor Java code, but that is still a choice, even if it is the most popular one.
My reaction looking at Go initially was that it was exciting to have a fast, simple language designed for writing services. My reaction to reading and writing code of real applications has been that Go is badly suited for writing nontrivial application logic.
> how often one experiences exception traces when using an application written in Python or Java
Python and Java aren't particularly ambitious standards for a 21st-century language.
> Think of reading code as mining ore. If the ore is rich, you don't have to mine and process nearly as much of it to get the material you need. If the ore is poor, you have to invest extra effort to mine more ore to get the same amount of refined material.
Reading code and mining have nothing in common. In particular, mining technology works best on dense ore, human visual perception requires whitespace to operate efficiently.
> You might think Go is easy to read because lines are individually very easy to read, but Go code is so information-poor (partly because of error handling boilerplate) that you have to read a lot more lines of it to understand what a system does compared to other languages.
I think Go is easy to read because (1) it ranks at the top in my experiences with other languages and (2) because humans are very good at scanning visual structure and less good at parsing arbitrary syntax. Most languages tacitly acknowledge (2) by way of indentation and other syntactically irrelevant whitespace, but they don't apply the same rigor to error handling.
> Python and Java aren't particularly ambitious standards for a 21st-century language.
I was remarking specifically about exception handling. Has there been much innovation in exceptions among 21st-century languages?
I don't think the difference between Go and more expressive languages is about whitespace. If the only way a language achieved fewer lines of code was by cramming more characters into a line, I wouldn't give it any credit for that..
> Has there been much innovation in exceptions among 21st-century languages?
Yes, there has, mostly in the ability to use exceptions less than previously. With Java (at least old-school Java, not sure where it is now) exceptions are the only type safe language-supported way for a function to terminate with multiple types. If a function has multiple possible return types that don't have an inheritance relationship, you can choose between 1) returning Object and dynamically checking for the specific types, 2) defining a return class with a field for each possibility, or 3) picking one type to be the "expected" outcome and defining all other outcomes as exceptional. With 1) you lose many of the benefits of static type-checking; with 2) you get code bloat; with 3) you get the hazards of using a nonlocal control mechanism when you don't want that power.
If your language has sum types, you have a better option for those situations.
I disagree. These two bits of code cater to different ways of reading. The first caters to a happy-path reading, where the reader has the choice to yadda-yadda the error handling or mentally expand it. The second foregrounds the error handling on an equal footing with the other logic.
I like your example, because this is exactly what happened in the application code I had to work with. In an application with complicated business logic, it isn't just one line of code turning into ten like you have here. It's ten lines of business logic turning into forty or fifty, where each operation is separated from the next by multiple lines of error-handling boilerplate.
The trade-off is that in Go code you can see every error path. This is a good trade-off for systems where absolute reliability and rigorous error handling are critical.
In an exception-oriented language the error paths are often invisible. This is a good trade-off for complicated business logic where error handling usually means aborting with an appropriate exception. Think about processing a record or a request where you have to validate the request, look up a few related objects in a datastore, check some business rules, do an authorization check for the requesting user, calculate the result of the request, store the result in a datastore, and produce a response. Each step can be written in a couple of lines of code that are hopefully pretty understandable if you have good names, like this:
validate_request(request)
user = fetch_user(request.for_user_id)
authorize_user(user, Privileges.CanFoo)
dingles = fetch_dingles_by_dongle_group(request.dongle_group_id)
unfooable_dingles = dingles.filter(not_fooable)
if (!unfooable_dingles.is_empty()) {
throw BadRequestException("This dongle group contains unfooable dingles")
}
fooment = calculate_total_fooment(dingles)
fooment_store.save(fooment)
return DongleGroupFooed(request.dongle_group_id)
From one point of view, this code is nice. You can read these lines of code quickly and see what the basic request handling logic is. It reads like a story.
From another point of view, this code is terrible. A lot of different things can go wrong here, and only one of them is visible. What happens if the user can't be found? What happens if the user isn't authorized? What happens if the dongle group id can't be found? If the wrong exception is thrown, the wrong result will be reported for this request. You have to navigate to other functions to check that. If that makes it bad code for you, then you'd probably rather be writing Go. In Go, these eleven lines would turn into thirty to fifty lines of code. The handling of each error would be visible, at the cost of the happy path being harder to follow.
You're absolutely spot-on with this analysis. And it is a core assertion of Go that non-explicit error handling produces less reliable programs. It's a value judgment and it's a subjective assessment.
The solution space here isn't "go vs exceptions," it's "errors as values vs exceptions (vs "let it crash" vs...)," and Go isn't the only implementation of errors as values.
Of course. I wasn't trying to imply anything to the contrary. But given the prevalence of exceptions, if Go's error handling is performing on par or better, then it seems pretty ridiculous to characterize Go's error handling as "shit". Don't worry Steve, I think Rust's error handling is pretty cool!
I do agree that exceptions feel like the worst of the various bits of the problem space, to me, but just to be extra clear about it, I have never written a significant amount of Go, and therefore don't really have a very strong opinion about its error handling.
And error handling is such a huge and interesting problem space! I've long wondered about why I didn't like checked exceptions in Java but do like errors as values, for example.
Character count does matter, but I still don't see how it makes code more readable if all the significant operations are supposed to happen in the condition block of an if statement.
Errors as return values is only acceptable for code that is so performance-sensitive that you aren't allowed to do dynamic memory allocations. For everything else, conditions+restarts are the correct answer, because errors-as-values restricts you to a single error-handling strategy and couples high-level code to low-level code as a result.
> Errors as return values is only acceptable for code that is so performance-sensitive that you aren't allowed to do dynamic memory allocations.
Exceptions-based code can be zero-cost, if no errors occur, at an increased error-case cost. Using error values pessimises this, and increase branch prediction load (as every callsite is now a branch).
So in the common case where errors are extremely rare, exceptions-based error handling can be quite a bit faster than return-value error handling.
Which doesn't mean it's preferable, but beware thinking that return values are faster.
Fun history fact: Rust had conditions, a very very very long time ago, but folks didn't use them and found them vaguely confusing, so they were removed.
This doesn't mean very much. Most people find the Rust borrow checker "vaguely confusing" (if not very confusing), and most people also wouldn't use it were it not strongly suggested by both the compiler and the community ("suggested" as unsafe Rust exists, but you of all people are aware of that).
Conversely, I understand condition systems, and I'm not a very good programmer. (I've tried and failed to learn Rust once already) That's a pretty low upper bound on how hard they are, especially relative to advanced features of languages like Haskell.
We're very fortunate that programming language design doesn't advance solely by giving people more of what they already use.
Doesn't adding conditions/restarts require your language to have stackful continuations? Those are quite complicated unless you don't bother to make them safe (like C doesn't bother to with setjmp/longjmp). I would be nervous about them for the same reason I am about exceptions.
Also, the restart seems to imply that you should "handle" the error, but I think this is really overemphasized cause what are you actually going to do about it? There's nothing to do about a lot of errors except die, but this encourages programmers to just make something up they think might help.
> Errors as return values is only acceptable for code that is so performance-sensitive that you aren't allowed to do dynamic memory allocations
Not really. It is acceptable for code whose maintainers value readability and simplicity over everything else. I totally agree that readability and simplicity are quite subjective and this is up to the maintainers.
I don't really know what "conditions+restarts" is but a few articles landed me into LISP which I find totally unreadable. So, can you point me to some "conditions+restarts" code that I can understand/appreciate easily? Any language is fine, I just want to understand the concept better since I am more a C/C++/JS programmer. (FWIW, never written Go, but it's easy to read and understand).
It's probably sufficient for this conversation to just understand it as try-catch. A function is invoked; if it "signals" (throws) then control moves to a handler that matches the signal (exception); the handler runs and resolves the situation. Of course, Lisp being Lisp, the system is extended to announcer voice FULL. GENERALITY. but in its simplest form it's basically equivalent to exception throwing.
This is correct (in that the most simplistic case is try-catch).
However, the difference between a try-catch and conditions / restarts is that when one signals a condition (exception), the restart (catch) has a continuation from the condition. This allows you to inject an expression into the location where an exception occurred and "restart" your code from that point.
Whether you do such a thing or not depends on the code, on the type of condition raised, and on what expressions are valid. So you get a lot more flexibility in how errors are handled across the system. But likewise: more complexity in having to make that choice in the first place.
Going farther than this, conditions and restarts are really just a fancy way of packaging delimited continuations. I don't personally know any non-Lisp language that has attempted to package these concepts (maybe Dylan, which is a Lisp-like in its own way but without the syntax?). Going back to the original thought regarding error handling - I think Result<T, Err> type handling is fine and that most languages would be better served by that than having different types of exceptions. Conditions and restarts are powerful but your language has to be very expression focused (i.e. does not use a lot of statements) and it's not really clear that there's been a lot of work on making restarts nice to use. Exceptions in all languages that have them have their own set of associated problems, for what its worth, and it's not as easy to move Lisp features into a non-Lisp as one might believe...
> Going farther than this, conditions and restarts are really just a fancy way of packaging delimited continuations. I don't personally know any non-Lisp language that has attempted to package these concepts (maybe Dylan, which is a Lisp-like in its own way but without the syntax?).
Dylan does have a condition system, but it’s basically a Lisp without the parens, so probably doesn’t count. On the other hand, algebraic effects are another fancy way of packaging delimited continuations, so arguably the research languages Eff[1] and Koka[2] tried. (I don’t think either one explored the connection with condition systems, but I’m not sure.)
> I think Result<T, Err> type handling is fine and that most languages would be better served by that than having different types of exceptions. Conditions and restarts are powerful but your language has to be very expression focused (i.e. does not use a lot of statements) [...]
Huh? I don’t know why you’d say that, if anything I think it’s the Either err t / Result<T, Err> style that is more expression-focused (I mean, it even originates in Haskell :). I wouldn’t even call Common Lisp particularly expression-oriented, honestly, not unless we’re comparing with plain old C and not Rust.
> Huh? I don’t know why you’d say that, if anything I think it’s the Either err t / Result<T, Err> style that is more expression-focused (I mean, it even originates in Haskell :). I wouldn’t even call Common Lisp particularly expression-oriented, honestly, not unless we’re comparing with plain old C and not Rust.
I think that's exactly what I mean. The vast majority of languages (including golang, in TFA) use statements for dealing with exceptions. Rust also had try-catch, but has long since removed that syntax.
Anyways, the reason I said it is because it is not clear what to do when one wants to restart a statement. There are plenty of non-expressions that can throw, and usually it's not thought about deeply, but from a language semantics point of view one does need to have an idea of how to engage with it. For example, if you wrote:
with open('somefile') as f:
for line in f:
# ...
in Python, and had to deal with a restart during `open`, how do you manage this? The naive answer is to just return the continuation at `open`, but the "with" statement may have contextual setup. For example, `open` might be fine during `__init__`, but may have failed in `__enter__`. If you "restart" in `__enter__`, you need to deal with the partial state. Expression-based languages don't really have this issue because the call stack is usually clear (there's no magic under the hood). Similar analogues would be the `using` keyword in C#, or perhaps even lambda-expressions in C++. The abstraction in the code is separated from the execution of the restart, so it gets kind of gross as a language implementer in terms of not having to have very specific places where restarts can and cannot be.
This is a good reason why Rust / Haskell don't package these and just use Either / Result instead. If you have a bunch of types that you didn't write, injecting a restart into any failing code now brings a question of: "Can you safely inject types into a restart for code that you do not have access to?" and the answer is often no. The visibility rules in Rust make this a non-starter, and in Haskell you have a problem of mutability as well. A condition may be triggered at a point where IO could be injected, and so many of the language semantics would be in question. I suspect the type definitions for a restart in any arbitrary location in the code would be pretty hard to write, so maybe this is an open research area in Haskell already, but I doubt it'd be as ergonomic.
> It is acceptable for code whose maintainers value readability and simplicity over everything else.
Errors-as-return values are less readable than conditions, not more - there's literally more visual noise on the screen.
And if you want "simplicity", don't use a computer. Computers are intrinsically complex devices, users desire features with complex implementations, and our job as programmers is to manage complexity, not pretend that it doesn't exist. One of the article's main points is that Go does the latter in lieu of the former, and that's also what errors-as-return-values does.
----------------------------------
The formal name for a condition+restart system appears to be "algebraic effects"[1].
Conditions and restarts are similar to exceptions, with the following changes:
First, conditions are conceptually used for non-error conditions in some cases, like what Python does.
Second, throwing a condition doesn't cause the stack to unwind up to the handler, unlike exceptions.
Third, in addition to throwing conditions, you, uh, wrap ("establish" is the jargon used) code in what are called "restarts", similar to wrapping things in try/catch blocks (but distinct, because with conditions you still have condition handling blocks). Restarts can have names and are non-mutually-exclusive. Conceptually, restarts represent error-recovery strategies, while conditions represent the errors themselves.
Fourth, when a condition is thrown, it propagates upward until it hits either the toplevel (in which case the interactive debugger is launched), or it hits a condition handler - without unwinding the stack. Then, either the human looking at the debugger can pick which restart they want to use, or the logic at the condition handler can do so.
Why is this better than any alternative error-handling mechanism? Because every other error-handling mechanism (1) unwinds the stack (destroying all contextually useful information that isn't explicitly saved by the programmer, and preventing you from restarting a computation in the middle) (2) forces you into a single error-recovery strategy and (3) couples low-level code to high-level code as a result.
In general, low-level code has details about the specific kind of error, context around it, and access to data and control flow that would allow the error to be recovered from (e.g. for a log-processing program, reasonable restarts while parsing a log entry would be (1) skip it (2) retry (3) use an alternative parser and (4) return an empty entry), while high-level code has the application context about why the low-level operation is being performed in the first place and which error-recovery option should be picked.
Conversely, high-level code doesn't have details about what the low-level code was doing at the time of the error, and low-level code doesn't have the high-level context necessary to determine which error recovery strategy is appropriate in this use of the low-level code.
> Errors-as-return values are less readable than conditions, not more - there's literally more visual noise on the screen.
No.
When you make a function call, and that call can fail, then the happy-path and the sad-path are both things that you need to manage as a caller. Happy-path and sad-path are two equivalent states that both need to be accommodated by the program logic.
Error handling code is not "noise". It is equally important to success-path code.
Yes. There is literally more visual noise on the screen. This is not up for debate - more pixels are lit on the monitor you are looking at.
> When you make a function call, and that call can fail, then the happy-path and the sad-path are both things that you need to manage as a caller.
False - the direct caller is not responsible for error-handling, in general - some transitive super-caller will be. Errors as return values needlessly generate this visual noise for every caller, when not needed, in addition to introducing aforementioned coupling.
> Error handling code is not "noise". It is equally important to success-path code.
You're misunderstanding my point. I never said that error-handling code is noise - it isn't. What is noise is forcing every single function call between the appropriate error-handling point and the error location to have extra useless junk. When there's an error, you should see exactly two things in your codebase: some stuff at the point where the error is thrown, and some stuff at the point where the error is handled - and, given that the place where the error should be handled is rarely the direct caller, you should see nothing in between.
Error handling is not visual noise. It is equally important to non-error-handling code paths.
The direct caller is _absolutely_ responsible for error handling.
> What is noise is forcing every single function call between the appropriate error-handling point and the error location to have extra useless junk.
No. Falliable operations must be managed by the thing which calls them. Anything else is shadow control flow, which subverts understanding and negatively impacts reliability.
Maybe this is the fundamental argument here. Plenty of cases I have written code where I am trying to do something over a big set of things, e.g. check a file for hard-coded paths, or send a message to a lot of people; it isn’t weird for those things to fail. Maybe I couldn’t open the file. Maybe the file lacked hard coded paths. Maybe the sender lacked rights to send to that receiver, or maybe the receiver is currently offline. But if most of your code is some complex calculation, say weather simulation?, maybe there is by default just one path.
> I don't really know what "conditions+restarts" is but a few articles landed me into LISP which I find totally unreadable. So, can you point me to some "conditions+restarts" code that I can understand/appreciate easily?
You’ll have to read Lisp, I’m afraid; the best description I know is in the book Practical Common Lisp[1].
(Come on, Lisp syntax is quirky, but it’s not unreadable, and unlike APL or Forth or even Haskell it doesn’t require you to memorize a bunch of semi-meaningless punctuation before you can understand what is going on—it’s pretty wordy usually. I’m not saying you must bring yourself to love writing (f x y) instead of f(x, y), only that adjusting from one to the other should not be particularly hard.)
I mean, I have done a toy Forth implementation, but that is hardly more readable with no experience with the language.
One system that is almost conditions and restarts is 32-bit(!) Win32 SEH, but it is not particularly well-documented and the language bindings usually try rather hard to hide that (though, if you think about it, On Error Resume Next from classic VB is unimplementable on top of bare try/catch).
...
OK, you nerd-sniped me :) Here’s a toy (no subtyping! no introspection! no condition firewall[2]! no tracebacks! no support for native errors! etc.) condition system in Lua (sorry, nested functions in Python are painful):
-- save as cond.lua
local M = {}
local error, unpack = error, unpack or table.unpack
local running = coroutine.running
local stderr = io.stderr
local exit = os.exit
local insert, remove = table.insert, table.remove
-- conditions
local handlers = setmetatable({}, {
__mode = 'k', -- do not retain dead coroutines
__index = function (self, key) -- no handlers by default
self[key] = {}; return self[key]
end,
})
local function removing(xs, x, ok, ...)
assert(remove(xs) == x)
if ok then return ... else error(...) end
end
-- establish a handler during call
function M.hcall(h, f, ...)
local hs = handlers[running()]
insert(hs, h)
return removing(hs, h, pcall(f, ...))
end
-- signal the given condition to currently active handlers
function M.signal(...)
local hs = handlers[running()]
for i = #hs, 1, -1 do hs[i](...) end
end
local signal = M.signal
function M.error(...)
signal(...)
stderr:write("error: " .. tostring(...) .. "\n")
exit(1)
end
function M.warn(...)
signal(...)
stderr:write("warning: " .. tostring(...) .. "\n")
end
-- restarts
-- invoke the given restart
function M.restart(r, ...)
local n = select('#', ...); r.n = n
for i = 1, n do r[i] = select(i, ...) end
error(r)
end
local function continue(r, ok, ...)
if ok then return ok, ... end
if ... == r then return false, unpack(r, 1, r.n) end
error(...)
end
-- establish a restart during call
function M.rcall(f, ...)
local r = {}
return continue(r, pcall(f, r, ...))
end
return M
Example: DOS-style abort-retry-ignore prompt implemented in the shell with some support in the (mock) I/O system and no support in the application:
local cond = require 'cond'
-- common condition types (XXX should use proper dynamic variables instead)
local retry, use = nil, nil
-- I/O library
local function _gets()
if math.random() < 0.5 then cond.error 'lossage' end
return 'user input'
end
local function gets()
local ok, value = cond.rcall(function (_use)
use = _use
local ok, value
repeat ok, value = cond.rcall(function (_retry)
retry = _retry
return _gets()
end) until ok
return value
end)
-- ok or not, we got a value either way
return value
end
-- application (knows nothing about errors)
local function app()
for i = 1, 5 do print(string.format("got: %q", gets())) end
return "success"
end
-- shell
local ok, value = cond.rcall(function (abort)
return cond.hcall(function (err)
io.stderr:write("I/O error: " .. err .. "\n")
while true do
io.stderr:write("[a]bort, [r]etry, [u]se value? ")
local answer = io.read('*l')
if answer == 'a' then cond.restart(abort, "aborted") end
if answer == 'r' then cond.restart(retry) end
if answer == 'u' then
io.stderr:write("value? ")
cond.restart(use, io.read('*l'))
end
end
end, app)
end)
print(ok, value)
This is not a perfectly accurate semantic model for real condition system, but it should be enough to give a general idea of how these things work and what the advantage over bare unwinding mechanisms like try / throw or Lua’s pcall / error is.
Error handling is basically orthogonal to performance.
If a function call can fail, it should return an error, and that error should be managed by its caller. Any other approach means callstacks are unpredictable, which makes a program way way harder to model.
Objectively false. Correct answer: the error should be managed by the code that makes sense to handle the error.
> Any other approach means callstacks are unpredictable
Also false. I use conditions regularly, and my callstacks are very predictable - errors bubble upward through the call tree until they're handled. There's nothing simpler.
> which makes a program way way harder to model
Also false. I have no problem at all modeling and understanding my code rife with conditions.
The problem is that a system of "conditions + restarts" approaches the generality of fully async code. Go can of course do async well enough via its goroutines.
For starters, there is no way to consume multiple return values of a function or method inline. This makes chaining extremely verbose and often results in having three lines of error handling per one line of "normal" logic.
Secondly, Go creates a silly dichotomy by introducing two completely different mechanisms for error processing: error values and panics. (Soon to be three, because people will start using generics.)
Thirdly, "errors are values" approach is extremely counterproductive when you have to create generic error handling (with logging, default behaviors on failur,e etc). Something as simple as printing why a web page panicked becomes an exercise in cleverness.
Go enthusiasts will probably say none of this matters if you follow some set of "good practices". However, even core language libraries often fail to handle errors consistently. (E.g. text/template.)