daokoder / dao

Dao Programming Language
http://daoscript.org
Other
199 stars 19 forks source link

Improve frame (specialize none type) and/or defer and/or code sections #191

Closed dumblob closed 10 years ago

dumblob commented 10 years ago

It seems, the behavior of frame{} is not so consistent as I'd expect it to be :) This is currently not possible:

routine r() => int {
  if (rand() > 0.5) return 5 else error('<=0.5')
  return 3  # just for compiler
}
routine wrap() {
  # the goal is to silently return if an exception occured in r()
  x = frame(none) { r() }
  if (x == none) return;
  # do some useful stuff with x
  io.writeln(x)
}

Also variants with defer{} inside of the frame{} are not currently feasible as the behavior of frame is sometimes kind of unexpected:

x = frame { defer { 5 } 3 }
io.writeln('first', x)
x = frame { defer { 5; 9 } 3 }
io.writeln('second', x)
x = frame { defer { 5 } return 3 }
io.writeln('third', x)
x = frame { defer { 5; 9 } return 3 }
io.writeln('fourth', x)
x = frame { defer { none } 3 }
io.writeln('fifth', x)
x = frame { defer { none } return 3 }
io.writeln('sixth', x)
x = frame { defer { return none } 3 }
io.writeln('seventh', x)
x = frame { defer { return none } return 3 }
io.writeln('eighth', x)

It should be the same for every code section block, not only frame{}.

Night-walker commented 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, frames) 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.

dumblob commented 10 years ago

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?

daokoder commented 10 years ago

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.

Night-walker commented 10 years ago

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.

dumblob commented 10 years ago

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.

dumblob commented 10 years ago

Btw my subjective perception of Pythons/Javas 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.).

daokoder commented 10 years ago

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.

dumblob commented 10 years ago

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.

Night-walker commented 10 years ago

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 Pythons/Javas 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" :)

Night-walker commented 10 years ago

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.

dumblob commented 10 years ago

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.

Night-walker commented 10 years ago

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.

daokoder commented 10 years ago

"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
}
Night-walker commented 10 years ago

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 :)

dumblob commented 10 years ago

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.

Night-walker commented 10 years ago

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.

daokoder commented 10 years ago

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.

daokoder commented 10 years ago

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.

Night-walker commented 10 years ago

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:

The 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.

daokoder commented 10 years ago

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 occurred
  • defer(any|none){} covers both cases
  • defer(typename){}, a more generic form, intercepts an error of particular type, or several error types in case of variant typing
  • defer(typename as varname){}, the base form, allows to reference error value by name like in try-catch

The 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.

Night-walker commented 10 years ago

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 defers 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){}.

Night-walker commented 10 years ago

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 :)

daokoder commented 10 years ago

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.

Night-walker commented 10 years ago

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.

daokoder commented 10 years ago

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.

Night-walker commented 10 years ago

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?

daokoder commented 10 years ago

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.

daokoder commented 10 years ago

Now the new defer is implemented. After getting into the details, I found I need to add/support the followings:

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).

Night-walker commented 10 years ago

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.

dumblob commented 10 years ago

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).

Night-walker commented 10 years ago

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? :)

Night-walker commented 10 years ago

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.

dumblob commented 10 years ago

Not sure what you mean by "multiple exception objects", but Gosrecover()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/anotherpanic()occurs in a lower-by-one-level context and none of them is handled byrecover(), when returning from the nested deferred context(s), the innerpanic()``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

  1. deferred and always
  2. unconditionally
  3. conditionally
daokoder commented 10 years ago

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.

Night-walker commented 10 years ago

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.

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.

Night-walker commented 10 years ago

Regarding the keyword, the only alternative I have in mind is out.

daokoder commented 10 years ago

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.

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 :)

Night-walker commented 10 years ago

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.

daokoder commented 10 years ago

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.

daokoder commented 10 years ago

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.

Night-walker commented 10 years ago

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.

Night-walker commented 10 years ago

Regarding the keyword, the only alternative I have in mind is out. Keep thinking :)

I tried hard :) My last word -- suspend.

daokoder commented 10 years ago

I tried hard :) My last word -- suspend.

I believe you:)

suspend is a bit confusing. Maybe better let's stay with defer.

daokoder commented 10 years ago

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.

Night-walker commented 10 years ago

OK.

dumblob commented 10 years ago

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.

daokoder commented 10 years ago

@dumblob, thanks for the example.

daokoder commented 10 years ago

I believe this issue can be safely closed now.