Closed dumblob closed 10 years ago
The fist example doesn't work because it triggers frame(@T)[ => @T] => @T
. Maybe frame(@T)[ => @V] => @T|@V
can be added to address this case.
Regarding defer
: some frame()
calls return zero, which is nonsense indeed. Others result in the value obtained from deferred block, which probably reflects how the code is arranged on the low level. It may still be quite unintuitive.
As we touched defer
, I begin to question its fitness for Dao. It obviously disrupts the normal control flow and hinders reasoning about the code. Its ability to do some post-stuff at scope exit and even modify the returned value does not actually add much, as proper code structuring (functions, frame
s) should mostly remove the need for such quirks. So defer
is mainly about exception handling, where it still turns the logic inside out.
In Go where defer
originates from, it is basically a mean to cope with the lack of destructors and things like Ruby blocks and Python with
operator -- which is not an issue for Dao. The use of deferring for exception handling in Go is likely the result of ascetic language design. Overall, while the versatility of defer
in Dao is certainly attractive, the shift in use cases comparing to Go may benefit a more specialized solution.
I'm not about to propose to roll back to boring try-catch. But if code deferring per se is not very important for Dao, I may have an idea or two about exception handling.
Well, I really like defer
as it's flexible, simple and logical way of ensuring execution of something. But I agree, that it`s usage for exceptions is kind of odd, although straightforward.
I'm also convinced that defer
has it's place in Dao (many times one want be sure to call something disregarding returns, exceptions, etc.). Therefore I'm curious what do you @Night-walker have in your mind regarding exception handling?
The fist example doesn't work because it triggers
frame(@T)[ => @T] => @T
.
Right.
Maybe
frame(@T)[ => @V] => @T|@V
can be added to address this case.
Sounds good. I will changed it to this.
Regarding defer: some frame() calls return zero, which is nonsense indeed. Others result in the value obtained from deferred block, which probably reflects how the code is arranged on the low level. It may still be quite unintuitive.
The root of the issue is that the defers are executed after the function return (in order to support return value modification), and the return values of the defers accidentally overwrite that of the routine.
As we touched defer, I begin to question its fitness for Dao. It obviously disrupts the normal control flow and hinders reasoning about the code.
I don't find it difficult to reason (not more than user-defined code section method, anyway), as long as it does not modify the returning value of the routine.
Its ability to do some post-stuff at scope exit and even modify the returned value does not actually add much, as proper code structuring (functions, frames) should mostly remove the need for such quirks. So defer is mainly about exception handling, where it still turns the logic inside out.
Well, I do often do "proper code structuring" in C in order to simplify cleanup in functions. But it is not exactly convenient after all. So I prefer to keep defers. Its use is more than exception handling. But I am considering to remove the support for return value modification, as it does complicate things with marginal benefits.
But if code deferring per se is not very important for Dao, I may have an idea or two about exception handling.
Let's hear you out first.
Maybe frame(@T)[ => @V] => @T|@V can be added to address this case. Sounds good. I will changed it to this.
Only if the typing system can properly resolve @T|@T
to @T
.
I don't find it difficult to reason (not more than user-defined code section method, anyway), as long as it does not modify the returning value of the routine.
User-defined code sections have normal control flow, just as higher-order functions they resemble -- you just pass one function as an argument to another. But deferred code executes at some implicit point different to where you see it. This results in somewhat odd, backward-order logic, e.g. "error handling first, then the code which may raise errors". The most puzzling for me is the use of defer
inside loops, which turns the code logic into some kind of circus show.
Well, I do often do "proper code structuring" in C in order to simplify cleanup in functions. But it is not exactly convenient after all.
But we don't need that much cleanups in Dao as in C and Go. As I already pointed out, Dao has destructors and code sections like mutex::protect(){}
which mostly remove the need to do something at scope exit. And instead of using defer
one can always define an ad-hoc code section:
# deferring
file = io.open(path)
defer { file.close() }
...
# section
routine with(path: string)[file: io::stream]{
f = io.open(path)
yield(f)
f.close()
}
with(path){ [file]
...
}
Surely you can point out that in the above example deferring is simpler. Well, it certainly is for one-time use. But when you have at least several similar cases, deferring obviously becomes a clunky boilerplate code with no modularity and abstraction. And the code section retains simplicity (no repetition), readability (straightforward control flow) and controllability (you can change the routine at any time without affecting its use). That is what I call "proper structuring", and I believe it is generally how code should be written in a modern high-level language -- more abstraction, less monkey-patching.
Now about alternative exception handling. The most trivial idea I have in mind may be implemented using code sections only -- without even a dedicated language operator. try()[ => @T] => @T|Error
or something logically similar to it is minimally sufficient for exception handling. The only issue is the lack of selectivity -- you can't list the errors you want to catch -- but there may be ways to resolve it preserving the general idiom.
It is just a "prototype" at the moment -- comparing it technically to defer-recover
is premature. I just wanted to highlight the basic principle -- switching from imperative exception handling to functional one. That is, viewing error as a result of execution alongside with normal return.
If you are interested, we could make something out of this -- instead of quirky defer
which looks a bit hackish among other Dao idioms.
I like the functional idea of handling exceptions, but regarding the general defer
, your example implementation of with
doesn't have not much in common with defer
as defer
guarantees it`s execution no matter what has happened.
Btw my subjective perception of Pythons/Java
s with
-like statements is that it inherently demands forced/prescribed interfaces (especially naming) which I personally don't like becase it encourages to create ecosystems like around Java (absolutely no simplicity, focus on naming/interfaces/language_itself instead of high-quality results, readability etc.).
But deferred code executes at some implicit point different to where you see it. This results in somewhat odd, backward-order logic, e.g. "error handling first, then the code which may raise errors". The most puzzling for me is the use of defer inside loops, which turns the code logic into some kind of circus show.
Just consider a defer
block as a closure that is executed right after the routine returns for whatever reason, and different defers are executed in the reverse order of their creation. I don't find this logic hard to follow.
You example is good one showing how to use code section to clean up resources, but as @dumblob has pointed out, defer
has the guarantee not offered by your example.
From what I know, language designers already tried to abstract exception handling as much as possible (C++, Java, Python, ...) in last decade(s) and they've failed (if not totally) - this is imho a sign, that abstraction in this area (exception handling) is contra productive.
I like the functional idea of handling exceptions, but regarding the general defer, your example implementation of with doesn't have not much in common with defer as defer guarantees it`s execution no matter what has happened.
It's just a matter of catching a possible exception at yield
, albeit nothing bad happens if it stays the current way.
Btw my subjective perception of Python
s/Java
s with-like statements is that it inherently demands forced/prescribed interfaces (especially naming) which I personally don't like becase it encourages to create ecosystems like around Java (absolutely no simplicity, focus on naming/interfaces/language_itself instead of high-quality results, readability etc.).
But I don't propose Python's with
.
Just consider a defer block as a closure that is executed right after the routine returns for whatever reason, and different defers are executed in the reverse order of their creation. I don't find this logic hard to follow.
"Reverse order" is hard to follow, as one have to reverse the parts of code in mind in order to reason about them.
You example is good one showing how to use code section to clean up resources, but as @dumblob has pointed out, defer has the guarantee not offered by your example.
There is nothing defer
can do which cannot be done with a code section (except for modifying returned value, of course):
routine with(path: string)[file: io::stream]{
f = io.open(path)
res = try { yield(f) } # assuming the 'try' I proposed
f.close()
if (res ?< Error) error(res)
}
Usually, however, you don't need a real cleanup in case of unhandled exception in Dao -- for instance, the file descriptor in the example would have been automatically closed by the garbage collector anyway.
From what I know, language designers already tried to abstract exception handling as much as possible (C++, Java, Python, ...) in last decade(s) and they've failed (if not totally) - this is imho a sign, that abstraction in this area (exception handling) is contra productive.
C++, Java and Python have completely identical exception handling in its essence -- and conceptually different comparing to defer
, which by your logic implies that deferring is "contra productive" :)
By the way, regarding guaranteed execution of defer
. While it may make sense for cleanup, which is rather unlikely to require to be carried out manually in Dao, it may be undesirable for certain other cases.
For instance, you may occasionally write an "optimistic" deferred code which is supposed to run if everything went OK. But if there was an uncaught error, bringing this code up may lead to undesirable consequences.
There is nothing defer can do which cannot be done with a code section (except for modifying returned value, of course):
Really? If f = io.open(path)
(or any exception-throwing call) throws an exception, no other code in the code section will be executed. Shouldn't the code be rather like this:
routine with(path: string)[file: io::stream]{
(res, f) = try { io.open(path) } # assuming the 'try' I proposed
if (! (res ?< Error)) (res2, _) = try { yield(f) }
f.close()
if (res ?< Error) error(res)
if (res2 ?< Error) error(res2)
}
As you may have noticed - I'm actually mimicking the `defer``s backwards-execution, but manually :(
which by your logic implies that deferring is "contra productive" :)
Limbo/Go is the only exception, in the others exceptions are identical, but handling not (consider not only syntax, but also meta-programming e.g. in Python/C++, destructors in C++, their lack in Java, and some other constructs like class statements throws
in Java etc.).
But if there was an uncaught error, bringing this code up may lead to undesirable consequences.
This is the same as handling more than one inner-section return codes and so it's nothing new nor complicated.
And regarding your try
, it's pretty much the same as get_throwed_exceptions_as_list
which could be useful imho, but it shouldn't imply removal of defer
.
If f = io.open(path) (or any exception-throwing call) throws an exception, no other code in the code section will be executed.
If f = io.open(path)
throws an exception, f.close()
should not be executed (and it will not be executed with defer
as well), so no extra manual handling is required here. As for executing the code after yield
(or defer
), it may not necessary be desired at all, and I already addressed such case in one of the previous comments.
But if there was an uncaught error, bringing this code up may lead to undesirable consequences.
This is the same as handling more than one inner-section return codes and so it's nothing new nor complicated.
Not at all. Consider the following case:
routine writeData(data: list<some>, cn: SQLConnection){
cn.run('BEGIN TRAN') # transactional insert is meant
defer { cn.run('COMMIT TRAN') } # an optimistic outcome
for (row in data)
cn.run('INSERT ...') # writing row into a table
}
If an unexpected exception occurs during the insertion, the transaction must not be committed -- and it will not with a section-based solution which simply omits error handling. But defer
will run the inner code regardless of the outcome, which would then result in committing an incomplete transaction to the database.
What I am trying to say: in Dao, defer
is much more likely to be utilized to carry out some main logic rather then a cleanup, and such use may be prone to unwanted, erroneous behavior.
"Reverse order" is hard to follow, as one have to reverse the parts of code in mind in order to reason about them.
This shouldn't be a problem, if one places defers in the right places.
routine with(path: string)[file: io::stream]{ f = io.open(path) res = try { yield(f) } # assuming the 'try' I proposed f.close() if (res ?< Error) error(res) }
I have to say, this is a lot more convoluted than using defer
.
routine writeData(data: list<some>, cn: SQLConnection){ cn.run('BEGIN TRAN') # transactional insert is meant defer { cn.run('COMMIT TRAN') } # an optimistic outcome for (row in data) cn.run('INSERT ...') # writing row into a table }
Of course, this is a wrong way to do transaction. It should have been something like this:
routine writeData(data: list<some>, cn: SQLConnection){
cn.run('BEGIN TRAN') # transactional insert is meant
defer {
if( % recover() ){
cn.run('ROLLBACK TRAN')
}else{
cn.run('COMMIT TRAN')
}
}
for (row in data)
cn.run('INSERT ...') # writing row into a table
}
routine with(path: string)[file: io::stream]{ f = io.open(path) res = try { yield(f) } # assuming the 'try' I proposed f.close() if (res ?< Error) error(res) } I have to say, this is a lot more convoluted than using defer.
But one rarely need to resort to it -- just because cleanup in Dao is a rather uncommon need.
Of course, this is a wrong way to do transaction. It should have been something like this:
routine writeData(data: list
, cn: SQLConnection){ cn.run('BEGIN TRAN') # transactional insert is meant defer { if( % recover() ){ cn.run('ROLLBACK TRAN') }else{ cn.run('COMMIT TRAN') } } for (row in data) cn.run('INSERT ...') # writing row into a table }
It should have, but it's so easy to make such a mistake presuming that nothing can go wrong. How often people check for exceptions? Quite rare, mostly when they want to handle them in some special way. In all other cases exceptions result in hard failure, where cleanup is doubtfully needed and everything else should simply not get executed.
If you use defer
for the so-called business logic (which, again, may be the most frequent case in Dao), you then need to always check that you don't proceed when things run amok. I have to say, this is a lot more convoluted than using code sections :) Missing cleanup is much less dangerous, virtually harmless comparing to the possibility of body continuing to move when head is already dead. Headless zomby, ruthless and unpredictable -- I'm already scared to use defer
:)
I already mentioned the saying: "I don't want a language which allows to write good code, I want one which disallows bad code". Not that I'm trying to prove by all means that defer
is inherently wrong, but I didn't think about the possibility of "zomby code" before, and it surely worries me now :)
If
f = io.open(path)
throws an exception,f.close()
should not be executed (and it will not be executed with defer as well)
In this case yes, but think of some general case like chaining (which is actually almost the only way where exceptions make sense if one doesn't want to create empty objects). Also, did you really wanted to let the with
statement throw IO exception? To catch it, one would need to use another try{}
or frame{ defer{ if( %recover() ) ... } }
, i.e. one more nesting => ugly unnecessary clutter.
and it surely worries me now :)
It's interesting as when I started to learn Go, defer
was one of the scariest things, but after a short time, I've started to understand it as one of the most predictable and clear things in the whole language - just because it's always the same - it just always executes no matter what. This is the only thing one has to know. There is no conditional, no jumping, no exceptions, just always. Pretty simple and transparent - as I said before, lack of abstraction in this exception/closure cases is apparently much more useful.
As I said, we can add a method to catch all exceptions and return them as a list (now recover()
sort of does it, but we could add a code-section variant), but this is definitely not meant as substitution for defer
.
In this case yes, but think of some general case like chaining (which is actually almost the only way where exceptions make sense if one doesn't want to create empty objects).
Exceptions make a lot of sense in triggering hard failure.
Also, did you really wanted to let the with statement throw IO exception?
Yes, of course. Hiding exceptions is a bad practice, and handling it in with
doesn't seem appropriate.
It's interesting as when I started to learn Go, defer was one of the scariest things, but after a short time, I've started to understand it as one of the most predictable and clear things in the whole language - just because it's always the same - it just always executes no matter what. This is the only thing one has to know. There is no conditional, no jumping, no exceptions, just always. Pretty simple and transparent - as I said before, lack of abstraction in this exception/closure cases is apparently much more useful.
Which essentially boils down to always starting defer
section with if (!%recover())
unless you're making a cleanup (which is unlikely in Dao comparing to Go) or handling an error. Not very nice and quite error-prone. Even if deferring stays in Dao, the issue with "zomby code" should definitely be addressed -- simply because you cannot make people always remember to check their logic for exceptions.
As I said, we can add a method to catch all exceptions and return them as a list (now recover() sort of does it, but we could add a code-section variant), but this is definitely not meant as substitution for defer.
Exception handling and code deferring are not the same thing. Go-like defer
attempts to do both at the same time and thus forces the programmer to always keep in mind both possibilities: when an error occurred, and when it didn't. This problem can actually be resolved preserving defer
-- I have an idea on how to modify it -- but only if deferring is really useful in Dao, for which I still have doubts.
I've started to understand it as one of the most predictable and clear things in the whole language - just because it's always the same - it just always executes no matter what. This is the only thing one has to know. There is no conditional, no jumping, no exceptions, just always. Pretty simple and transparent - as I said before, lack of abstraction in this exception/closure cases is apparently much more useful.
Same feeling. One advantage of defer
is its lack of unnecessary abstraction.
I don't want a language which allows to write good code, I want one which disallows bad code
Disallow bad code
is virtually impossible for some people with any feature. So we should remove all the features :) ?
Go-like defer attempts to do both at the same time and thus forces the programmer to always keep in mind both possibilities: when an error occurred, and when it didn't.
No forcing here. You only need to handle errors if you think they are recoverable or should be handled, and let other errors simply to be hard failures.
This problem can actually be resolved preserving defer -- I have an idea on how to modify it -- but only if deferring is really useful in Dao, for which I still have doubts.
Let's hear about it.
There was a parsing/compiling bug for defer blocks. A defer block is not supposed to return a value, but a change made some weeks ago causes a defer block to automatically return the value of its only expression. Now it is fixed, and extra checking is used ensure no value will be returned from defer blocks.
Let's hear about it.
Then please be patient to hear out my ranting to the end, for I am going to try my best :)
Disallow bad code is virtually impossible for some people with any feature. So we should remove all the features :) ?
What a bold move -- but you didn't expect that I couldn't parry it? :) No, we shouldn't remove all the features, but we need to smooth out the sharpest edges. The progress in programming languages is largely about hiding sharp technical stuff underneath soft abstractions to minimize possible misuse.
For instance, C is simple as a stool, transparent as a tear and provides only the most basic abstractions over machine code, but does this make programming in it simple? You may have heard the saying about dancing with razors on a wet floor, that's what it indeed like. A simple stick may have a lot more uses then a spear, but the latter is undoubtedly better at hunting mammoths.
No forcing here. You only need to handle errors if you think they are recoverable or should be handled, and let other errors simply to be hard failures.
But it isn't currently possible to just "let" them to be hard failures. defer
doesn't know whether it is an expected error or not, or whether there was an error at all -- it will execute its code anyway, as it's just a stick with no particular aim. It can pierce mammoth skin, but only if you use it "right" and does not loose concentration.
What if I'm don't expect an error at all? Then I'll have to manually check for exceptions in defer
, or the mammoth may run amok wreaking havoc all over the place. The largest advantage of exceptions is that they cause hard failure by default, so that if something went wrong, it won't become like in Jumanji movie. And that should concern defer
as well, simply because it's not meant exclusively for exception handling.
A possible solution is thus the following: attach a blade to the stick to turn it into a spear. First, remove the current defer(result){}
form to clear a place for a new syntax which is about making defer
behavior more controllable and precise:
defer {}
and defer(none){}
run only when no error happened (no exception handling)defer(any){}
runs only when certain error occurreddefer(any|none){}
covers both casesdefer(typename){}
, a more generic form, intercepts an error of particular type, or several error types in case of variant typingdefer(typename as varname){}
, the base form, allows to reference error value by name like in try-catchThe full syntax is thus
'defer' [ '(' <type> [ as <name> ] ')' ] '{' <expr> '}'
-- pretty simple.
This implies shifting error recovering and discrimination from recover()
to defer
, with the latter loosing its agnostic attitude towards exceptions and becoming more flexible.
If such or similar enhancement was made, defer
would be excellent for exception handling even with its backward logic, covering all variants of try-catch and try-finally without extra motions, while allowing for hard-failure-safe code deferring. Mammoth slain, stick intact.
A possible solution is thus the following: attach a blade to the stick to turn it into a spear. First, remove the current defer(result){} form to clear a place for a new syntax which is about making defer behavior more controllable and precise:
defer{}
anddefer(none){}
run only when no error happened (no exception handling)defer(any){}
runs only when certain error occurreddefer(any|none){}
covers both casesdefer(typename){}
, a more generic form, intercepts an error of particular type, or several error types in case of variant typingdefer(typename as varname){}
, the base form, allows to reference error value by name like in try-catchThe full syntax is thus
'defer' [ '(' <type> [ as <name> ] ')' ] '{' <expr> '}'
-- pretty simple
I thought about something similar when I was implementing defer
(also inspired by catch
or rescue
). But it seemed less convenient to implement than the Go-like defer, and also in order to keep it familiar to people used to the Go kind defer, so I didn't go deep with this idea.
Now you convinced me to give serious consideration for this. Here are a few details I am considering and want hear your opinions.
defer(errortype){}
should not respond to errors raised by other defer
s defined at the same level;defer{}
or defer(){}
should execute unconditionally in all cases, since they does not specify conditions.I think defer(errortype){} should not respond to errors raised by other defers defined at the same level;
If here you think about defer
as of try-catch branch, then definitely yes: it should be possible to raise errors in defer
without being hindered by other defer
s around.
defer{} or defer(){} should execute unconditionally in all cases, since they does not specify conditions.
Agree, that's probably better both as the default behavior and comparing to defer(any|none){}
.
A motivating example :)
routine writeData(cn: SQLConnection){
cn.run('BEGIN TRAN')
defer (any){ cn.run('ROLLBACK TRAN') }
defer (none){ cn.run('COMMIT TRAN') }
...
}
Here, in the same case as before, defer(typename)
is obviously superior to both dedicated try-catch-finally and defer
with manual recover()
. Both the semantics and capabilities makes me forget anything I had against defer
earlier :)
Here, in the same case as before, defer(typename) is obviously superior to both dedicated try-catch-finally and defer with manual recover(). Both the semantics and capabilities makes me forget anything I had against defer earlier :)
With this new defer
, I am even considering to remove recover()
entirely. The only problem is that recover()
can take such as "Exception::Error::Index"
etc. as parameter, which is a bit inconvenient to support.
I implied that recover()
was no longer needed. Instead of using string exception names, it is better to register all user-defined errors upon module loading. This way it looks more "sane", and the VM will check that specified error types actually exist. The latter is particularly important -- usually error handling is not thoroughly tested, especially if numerous error types are being caught, so a minor typo in exception name can lead to very subtle bugs.
Instead of using string exception names, it is better to register all user-defined errors upon module loading. This way it looks more "sane", and the VM will check that specified error types actually exist.
Sure, if there is a reasonable way to declare user-defined error types in Dao without sub-classing from Exceptioin::Error
.
Sure, if there is a reasonable way to declare user-defined error types in Dao without sub-classing from Exceptioin::Error.
And why not sub-classing Exception::Error?
And why not sub-classing Exception::Error?
Nothing wrong with sub-classing Exception::Error, it is just that, in most case, it is not really necessary, and all one needs is distinguishable error type. The builtin C data type (DaoException
) for exception is quite adequate for most exception types, as it can hold all the information that defines an exception in most cases. So I found it much more convenient to declare a new exception type and let the Dao runtime to create it as a C data type based on DaoException
.
For example, currently one can do error( "Exception::Error::MyError" )
and recover( "Exception::Error::MyError" )
, then Dao runtime will automatically generate a new type named MyError
as C data type derived from Exception::Error
. This way one can use a new exception type without explicitly define it. Of course, one has to be careful with the names here, but being able to do this is very convenient.
You may have noticed that, the current Dao implementation only has 3 predefined exception types: Exception
, Exception::Warning
and Exception::Error
, and all other exception types are generated at running time, only when they are actually need!
So I certainly want a way to specify a new exception type and let the Dao VM create it for me.
Now the new defer
is implemented. After getting into the details, I found I need to add/support the followings:
defer
block can be invoked multiple times if there are multiple exception objects that are compatible to its parameter type. This should happen rarely, but it seems necessary to support it, otherwise, we may need to support passing a list of exception objects to a defer block. Or users will need define a list of defers in a loop to handle potentially multiple exception objects in some cases;I don't know if it sounds complicated, but I assure you it is not, I may need to find a better way to explain it (demo/defers.dao
and demo/errors.dao
may be helpful).
The semantics and reasoning is pretty clear. It is obvious that, unlike in Go, defer
in Dao cannot function properly without handling routine result. However, a block-based construct (like try-catch) would not require such complications...
Anyway, I wonder if we should consider a different keyword instead of defer
, for we've just made a huge step away from the original semantics.
Amazing ideas - I'm pleased to see such progress after the few days I was gone :)
Regarding changing keyword defer
to something else I wasn't able to come up with something more appropriate and short enough (btw don't forget that defer{}
or defer(){}
will have the same semantics as the original defer).
btw don't forget that defer{} or defer(){} will have the same semantics as the original defer
Actually, it can also run after an exception breaks free, so it should also adhere to the rule about return
as scope result and recovery indicator -- not the same as earlier anyway.
By the way, what if an exception is raised in defer
? Notably, if there already was a pending error, will we then have two exceptions to deal with? @dumblob, how the hell Go handles it? :)
Got it myself: the phrase "multiple exception objects" is pretty self-explaining. Kinda unconventional approach to exception handling, but I suppose nothing lethal.
We should definitely choose another keyword because of the fact that defer (errtype){}
can be called multiple times. Thus in general it's now conceptually not just a code which executes later, but a handler of certain kind -- sort of a scope guard.
Not sure what you mean by "multiple exception objects", but Gos
recover()handles only the error object from the nearest higher context (see http://golang.org/test/recover1.go - you can play with it e.g. on http://play.golang.org/). If second/another
panic()occurs in a lower-by-one-level context and none of them is handled by
recover(), when returning from the nested deferred context(s), the inner
panic()``s value overwrites the original value from the outer context.
Anyway, what new keyword would you recommend instead of defer
? We have 3 things to express: the block is executed
Actually, it can also run after an exception breaks free, so it should also adhere to the rule about return as scope result and recovery indicator
Not necessarily. Because they do not suppress exceptions, if an exception happens, the return value will not be used anyway.
Thus in general it's now conceptually not just a code which executes later, but a handler of certain kind -- sort of a scope guard.
Yes. But its primary meaning is still deferring, now it is just differentiated into: conditional, unconditional and repeating deferring. Of course, if you have a better keyword, I'd gladly change to it. One keyword I have considered before implementing defer
is atexit
, but it doesn't seem to be better than defer
, because it looks more appropriate for static blocks instead of dynamically created closures.
Not sure what you mean by "multiple exception objects", but Go
s recover() handles only the error object from the nearest higher context (see http://golang.org/test/recover1.go - you can play with it e.g. on http://play.golang.org/). If second/another panic() occurs in a lower-by-one-level context and none of them is handled by recover(), when returning from the nested deferred context(s), the inner panic()
s value overwrites the original value from the outer context.
Maybe we should also keep only the most recent exception in order to simplify things? Chaining exceptions like a train may be too much, and without it multiple defer
execution is removed.
Regarding the keyword, the only alternative I have in mind is out
.
but Go
s recover() handles only the error object from the nearest higher context (see http://golang.org/test/recover1.go - you can play with it e.g. on http://play.golang.org/). If second/another panic() occurs in a lower-by-one-level context and none of them is handled by recover(), when returning from the nested deferred context(s), the inner panic()
s value overwrites the original value from the outer context.
I don't see anything about overwriting panic values in test/recover1.go
. I am having some problem accessing golang website, could you copy and paste relevant part here? Thanks.
Maybe we should also keep only the most recent exception in order to simplify things? Chaining exceptions like a train may be too much, and without it multiple defer execution is removed.
Keep only the most recent exception will only remove the need for multiple defer execution. But from implementation point of view, it simplifies nothing.
In Go, I suspect, they do it this way mainly because of recover()
, it is more user friendly to have recover()
to return a single exception object rather than returning a list of them which would require looping.
In Dao, this is no longer an issue, so we should not follow them on this. Also I am not sure if discarding exception objects is a good idea.
Regarding the keyword, the only alternative I have in mind is
out
.
Keep thinking :)
Keep only the most recent exception will only remove the need for multiple defer execution. But from implementation point of view, it simplifies nothing.
Keeping in mind that certain defer
may run twice or more complicate things, particularly if that block is also supposed to do some finalization or cleanup. If you don't know how many times defer
can be executed, presence of any side effects in it is quite apt to errors. Being confident that any defer
executes just once simplifies things a lot in avoiding such subtle problems.
Keeping in mind that certain defer may run twice or more complicate things, particularly if that block is also supposed to do some finalization or cleanup. If you don't know how many times defer can be executed, presence of any side effects in it is quite apt to errors. Being confident that any defer executes just once simplifies things a lot in avoiding such subtle problems.
You have a very good point. defer
will be changed to execute only once. Considering that there can be multiple defer
with different exception condition, multiple exception objects can be still supported without problem, since they are most likely to be of different types.
Then it may be better to let each defer
with a type parameter to consume (suppress) the exception passed to it, just to keep things simpler. And then it will be necessary to require such defers to return (or not to return) in the same way as the host routine.
Then it may be better to let each defer with a type parameter to consume (suppress) the exception passed to it, just to keep things simpler. And then it will be necessary to require such defers to return (or not to return) in the same way as the host routine.
Yes, it probably would be simpler, as errors rarely get re-thrown.
Regarding the keyword, the only alternative I have in mind is out. Keep thinking :)
I tried hard :) My last word -- suspend
.
I tried hard :) My last word --
suspend
.
I believe you:)
suspend
is a bit confusing. Maybe better let's stay with defer
.
I believe a bit restriction on the type parameter for defers would be reasonable and helpful. Considering that one can write multiple conditional defers for different exception types, there is no need to support variant types, in fact, there is no need to support types other than: none
, any
and Exception
and its derived types. It is simpler and more efficient to handle with this restriction.
OK.
I don't see anything about overwriting panic values in
test/recover1.go
. I am having some problem accessing golang website, could you copy and paste relevant part here? Thanks.
I'm sorry if I confused you. The referenced tests should show how panic()
and recover()
in conjunction with defer
work. Then I said that panic()
overwrites existing "exception", but didn't point out how to achieve it because none of those tests does it. The following should do.
package main
func nested_defer1() {
defer func() {
println("rec1.1", recover())
// just for demonstration that recover() doesn't
// remember anything implicitly
println("rec1.2", recover())
println("rec1.3", recover())
}()
defer func() {
println("howk2")
panic(2)
}()
println("howk1")
panic(1)
}
func nested_defer2() {
defer func() {
println("rec1.1", recover())
// just for demonstration that recover() doesn't
// remember anything implicitly
println("rec1.2", recover())
println("rec1.3", recover())
}()
defer func() {
defer func() {
println("howk3")
panic(3)
}()
println("howk2")
panic(2)
}()
println("howk1")
panic(1)
}
func nested_defer3() {
defer func() {
println("rec1.1", recover())
// just for demonstration that recover() doesn't
// remember anything implicitly
println("rec1.2", recover())
println("rec1.3", recover())
}()
defer func() {
defer func() {
// even if we recover panic(2), the panic(3)
// overwrites everything when unwinding
println("howk3", recover())
panic(3)
}()
println("howk2")
panic(2)
}()
println("howk1")
panic(1)
}
func main() {
nested_defer1()
nested_defer2()
nested_defer3()
}
and the result
howk1
howk2
rec1.1 (0x52120,0x2)
rec1.2 (0x0,0x0)
rec1.3 (0x0,0x0)
howk1
howk2
howk3
rec1.1 (0x52120,0x3)
rec1.2 (0x0,0x0)
rec1.3 (0x0,0x0)
howk1
howk2
howk3 (0x52120,0x2)
rec1.1 (0x52120,0x3)
rec1.2 (0x0,0x0)
rec1.3 (0x0,0x0)
Program exited.
i.e. all panics were treated and the program exited successfully. Also notice that all the recovered non-nil
values have the same pointer addr.
there is no need to support types other than: none, any and Exception and its derived types
I agree.
@dumblob, thanks for the example.
I believe this issue can be safely closed now.
It seems, the behavior of
frame{}
is not so consistent as I'd expect it to be :) This is currently not possible:Also variants with
defer{}
inside of theframe{}
are not currently feasible as the behavior of frame is sometimes kind of unexpected:It should be the same for every code section block, not only
frame{}
.