|
From: Twylite <tw...@cr...> - 2008-11-19 09:18:04
|
Hi,
>> > TIP #329 Try/Catch/Finally syntax
>>
>
> I'd really like to see this in 8.6. However, I also think
> it's essential that try/catch be able to dispatch based on
> the return code, not just $::errorCode, which the current
> specification does not provide for. Please fix!
>
Although I haven't updated the spec, the current proposal is:
try {
#code
} then {
#success continuation
} onerror {glob emvar optsvar} {
#handle errors based on errorCode
} except {spec emvar optsvar} {
#handle errors based on any matching information in optsvar
} finally {
#always execute this as the last action before leaving the try command
}
The "except" clause will handle exceptions (non-zero Tcl return code) as
opposed to errors (Tcl return code TCL_ERROR with dispatch based on
errorCode). Not certain on what the "spec" in the except clause is
going to look like though. Suggestions welcome (via e-mail or to
http://wiki.tcl.tk/21608).
I still have concerns about this though:
- The construct is getting pretty bulky - is it really adding power &
simplicity as it is intended to do?
- The "except" clause needs to be flexible enough to handle the expected
common use cases, but not so flexible/complex that it's easier to just
use catch. There is little clarity on what the "common use cases" are.
- Often information about an error is implied in the result (as in
Tcl_SetResult) rather than an errorCode -- can except (or some other
clause) adequately handle this case?
I'm still investigating the syntax in other languages to what we can
cherrypick ;)
A modification I find particularly interesting is an alternative
approach to finally. The current approach - while typical for try/catch
syntax - is often a source of subtle bugs. It is common for a finally
to have to check each resource before cleaning it up (and equally common
for checks to be missed or assumptions made ... e.g. if { $somevar ne {}
} { # cleanup } but an exception was thrown before somevar was set.
A nested try/then/finally may go some way to address this situation, but
there are more interesting approaches, e.g. a "finally /script/" command
that can be placed anywhere in a block of code, and queues script for
execution at the end of the block of code. This approach is cleaner (no
"finally" clause adding bulk to the try/catch syntax), and resource
allocation and cleanup concerns can be located together in the source
code (locality is a good thing). Error/exception handling is also
simplified as each finally script runs independently (exception
information is chained) - so if part of your 'finally' fails, other
parts will still run without having explicit exception handling in a
'finally' clause.
Simple example of alternate finally syntax:
try {
set f [open "myfile.txt" r]
finally [list close $f] ;# or finally { close $f } since it executes
in the context of the block
# ...
}
Anyway ... there is still work to be done ;/
Regards,
Twylite
|
|
From: Donal K. F. <don...@ma...> - 2008-11-19 10:06:32
|
Twylite wrote:
> I still have concerns about this though:
> - The construct is getting pretty bulky - is it really adding power &
> simplicity as it is intended to do?
Why are you using all of it at once? That's its most bulky case. (It's
also something that's quite a PITA to write with just [catch], speaking
as someone who's done that in the past...)
> - The "except" clause needs to be flexible enough to handle the expected
> common use cases, but not so flexible/complex that it's easier to just
> use catch. There is little clarity on what the "common use cases" are.
So long as we can do multiple 'onerror' clauses and can omit the capture
variables, we'll be OK. (The errorinfo and errorcode can be picked out
of the options.) For the 'except' clause, it's probably only necessary
to be able to match the 'code' itself (and optionally capture message
and options, of course) while allowing the normal aliases for 0-4 (i.e.
ok, error, return, break, continue).
It's possibly a good idea to forbid the trapping of 'ok' and 'error'
using the 'except' clause; they have their own syntax.
The other thing is that the various trap clauses should be arbitrarily
reorderable.
> - Often information about an error is implied in the result (as in
> Tcl_SetResult) rather than an errorCode -- can except (or some other
> clause) adequately handle this case?
Wrong question. The right question is "should it handle the case?" and I
think the answer is "no". Let's clean up the problems with critical info
not going into the errorcode instead (and I know that might take a bit).
> A modification I find particularly interesting is an alternative
> approach to finally.
[...]
> Simple example of alternate finally syntax:
> try {
> set f [open "myfile.txt" r]
> finally [list close $f] ;# or finally { close $f } since it executes
> in the context of the block
> # ...
> }
I don't like that nearly as much, FWIW.
Donal.
|
|
From: Twylite <tw...@cr...> - 2008-11-20 10:51:47
|
And there I was thinking this had been discussed on tcl-core ;)
First up, I want to be clear about the intent behind TIP #329:
a) The TIP is about error handling, not exception handling. Tcl has
excellent exception handling which can be readily used in a mostly
non-ugly way (e.g. catch + if/then/else or catch + switch), but its
error handling is poor. Developers use the result (Tcl_SetResult, not
return code) as an error message, representing both the class/category
and exact nature of the error (in a human-readable form), but don't
provide a way to programatically identify the class of error making
control flow based on the type of error a hit-and-miss match on the
error code.
b) The innovation of a new 'finally' that simplifies the linguistic
expression of the programmer's desires has been a large part of my
motivation to develop this TIP.
c) The overall intent of the TIP can be summed up as "make a control
structure that makes dealing with errors and resource cleanup simpler -
both logically and visually".
NEM:
> Firstly, "onerror" and "except" seem like bad names to me. "except" in
> particular would imply that the following error case *isn't* handled (as
> in "catch everything *except* for these..."), which is just confusing.
> I also have some problems with the usage. I'd prefer to see something
> like:
I accept your argument about "except", having had the same concern
myself. I drew this from C's try...except. Others have argued against
the use of 'catch' as it could be confused with the existing catch
command. I'll consider 'handle' instead of catch - it sounds reasonable
for the domain; other suggestions are welcome. I feel that "onerror" is
correctly named though.
> Certainly, I think dispatch based on the return code/exception type
> should be at least as easy as based on errorCode -- I can think of lots
> of examples of Tcl code that does this, but almost none that actually
> looks at errorCode.
While my aim in this TIP was to deal with errors, there has been a
strong call to generalise the control structure to handle exceptions.
My feeling is that it should handle both with equal ease.
> Support for existing, widely-used idioms, should be as much, if
> not more, important than promoting lesser-used alternatives. In fact,
> I'd leave out entirely the glob-matching on errorCode, and let people do
> this with [switch] if they want it:
I cannot agree with this -- it would make this try/catch a control
structure focused exclusively on exceptions (not on errors), and provide
no functional or syntactic benefit over catch+switch (other than a
'finally' clause).
On supporting widely-used idioms, it would be much more important IMO to
branch based on the result than on the return code. Branching based on
return code (i.e. exception handling) is useful for creating new control
structures, but if you want to handle errors then you must branch based
on some information that is available from a return code = TCL_ERROR
(2). That means either errorCode or result, and right now most APIs are
using result.
You make a good point about leaving pattern matching to existing
commands -- I have been thinking along those lines for how best to
exploit that (more in another mail).
> Show me the use-cases! Handling different error-cases with [catch] is
> not that ugly now:
The exact same example switched on the return value of catch, or on
[dict get $opts -code], handles exceptions with the same level (or lack
thereof) of ugliness. Arguably if one does not need a different control
structure for errors, then it is no more necessary for exceptions.
> People don't do it because it just isn't very useful. Errors in Tcl
> tend to be real errors -- other than logging them, there is often not
> much to do. Tcl's introspection, use of general control exceptions
> like [continue]/[break], and custom control structures/HOFs etc make
> this kind of exception-based case analysis much less necessary. I may
> well be wrong about this, but I'd prefer to see some concrete use-cases.
The nature of errors in Tcl is a side-effect of the weak support for
distinguishing between types of errors. This functionality is useful
any time that you are calling into an opaque API and can take different
recovery actions based on the cause of the error. e.g. you want to wrap
load balancing and/or fault tolerance (simplest case: auto-reconnect)
around an RPC or DB interface; you want to try alternative
authentication schemes when the password fails (but not when the
connection fails, or the protocol is mismatched); you want to tell the
user whether to 'try again later' or 'call the administrator'.
Let's drop to Java-speak for a moment: the current practice of Tcl
developers is to "catch (Exception e) { // handle }". In C++ it is
"catch (...) { // handle }".
Return code is not a mechanism for exception/error typing, it is a
mechanism for implementing control flows. We need a typing mechanism.
JE:
> I would suggest:
>
> try {
> ...
> } onerror {pattern ?resultVar ?optionsVar??} {
> #
> # return code was TCL_ERROR, errorCode matches $pattern
> #
> } handle {code ?resultVar ?optionsVar??} {
> #
> # return code was $code.
> #
> } finally {
> ...
> }
>
Based on NEM's argument against 'except', this is pretty much what I'm
left with (with 'then' as an alias for 'handle ok').
>> > I still have concerns about this though:
>> > - The construct is getting pretty bulky - is it really adding power &
>> > simplicity as it is intended to do?
>>
> Yes. Dispatching based on the return code is essential
> for creating user-defined control structures.
>
There seems to be a consensus that the try/catch structure needs to
handle return codes (at least as easily at errors).
>> > The only real question is whether
>> > the msgvar and optvar should be that way round. Pass on that! :-)
>>
> Yes, they are in the right order.
>
Agreed - the order {emvar optsvar} is consistent with catch.
> I believe so. Nested try/finally clauses -- one for each
> resource that needs to be cleaned up -- all wrapped in
> an outer try/catch for error recovery would cover
> all the exception handling needs I can think of.
>
> I don't like the alternative approach much either,
> simply because it smells too much like innovation.
>
And there I was thinking that Tcl generally supported innovation ;p
That troll-hole aside, I do a lot of work in C, especially firmware for
embedded systems. Not having syntax for exception handling, there are
two approaches you can take to code flow and resource cleanup:
1) Deeply nested logic. Your "ideal path" reads diagonally as you
acquire resources and check conditions in nested if/then/else
statements. There is one point of return at the end of the function.
2) Fail-fast logic. Your code reads down the page; when an error is
encountered to jump (goto) a cleanup block at the end of the function.
Your code never indents for error codes, only for non-error branches.
Although the only return is at the end of the function this is a
consequence of the lack of try/finally syntax - in effect each 'goto' is
a point of return.
If you've never encountered non-trivial functions written in these two
styles, I suggest you take a look. The latter is (by my reckoning and
that of all experienced developers I've worked with) far more readable,
understandable and maintainable. YMMV, as may your opinion.
The whole point of exception handling (by which I mean
Java/C++/Python/Ruby/whatever, not Tcl's
use-a-return-code-for-flow-control) is to support the fail-fast paradigm
in conjunction with not having to explicitly check for failure on each
call (exceptions branch the flow at the point of the call and propagate
up the stack). If your code must still be deeply nested in order to
(safely) handle cleanup then are you really gaining anything from
exception handling? Your code remains visually complex, and the benefit
of the new syntax is limited.
Looking at this another way, you _could_ use nested try/finally clauses
in Java, C++, etc. to avoid the common bugs in finally clauses (freeing
an unallocated resource, not handling exceptions off resource
deallocation leading to dangling resources) but _developers don't_. It
could be laziness, incompetence, lack of care, lack of testing, not
having been caught by the bugs enough, not being aware of the problem,
or simply that its too much of a pain to do it properly. I don't know
for sure, but from my interactions with developers in my company I'm
leaning towards the last reason.
> There has been a longstanding desire that people [*] make
> better use of -errorcode, but there's a vicious cycle:
> nobody bothers to use meaningful -errorcodes because nobody
> bothers to check -errorcode because there's no convenient
> way to do so.
>
> try/onerror would at least break that part of the cycle.
>
This is the intention of the TIP, yes.
Regards,
Twylite
|
|
From: Neil M. <ne...@Cs...> - 2008-11-20 13:40:54
|
Twylite wrote:
> And there I was thinking this had been discussed on tcl-core ;)
>
> First up, I want to be clear about the intent behind TIP #329:
> a) The TIP is about error handling, not exception handling. Tcl has
> excellent exception handling which can be readily used in a mostly
> non-ugly way (e.g. catch + if/then/else or catch + switch), but its
> error handling is poor.
I don't agree that exception handling is excellent in Tcl currently.
> Developers use the result (Tcl_SetResult, not
> return code) as an error message, representing both the class/category
> and exact nature of the error (in a human-readable form), but don't
> provide a way to programatically identify the class of error making
> control flow based on the type of error a hit-and-miss match on the
> error code.
I believe this is already addressed by the presence of errorCode.
Pattern-matching against this code is already quite simple, with
[switch] and so on. It just hasn't caught on. It seems like wishful
thinking to expect that it will suddenly catch on just because its usage
is made slightly more convenient (we're talking about a reduction of
about 1 line of code).
> b) The innovation of a new 'finally' that simplifies the linguistic
> expression of the programmer's desires has been a large part of my
> motivation to develop this TIP.
I agree with the motivation for this entirely.
> c) The overall intent of the TIP can be summed up as "make a control
> structure that makes dealing with errors and resource cleanup simpler -
> both logically and visually".
I think it is worth explicitly pulling out the separate concerns
embodied in this TIP and tying them to use-cases. As I see it, there are
a number of more-or-less separable concerns here:
1. Support for ensuring resource cleanup in the presence of
exceptions/errors (i.e. "finally").
2. Better support for case-analysis based on exception return code.
3. Better support for case-analysis based on errorcode.
4. Ability to trap only those exceptions/errors that you are interested
in/prepared to deal with, letting others propagate normally.
Now, it we look at some use-cases and how they are handled currently,
versus proposed methods for handling them:
1. Guaranteed resource cleanup
------------------------------
A typical example of this is ensuring that a channel is closed once we
have finished processing it. Currently, you would do this via:
set chan [open $myfile r]; # or [socket] etc
catch { process $chan } msg opts
close $chan
return -options $opts $msg
Some things to notice here are: (a) the catch-all behaviour of [catch]
is exactly what is required here: we don't want any exceptions to
escape, (b) we don't require any case-analysis on the exception type or
error code. The proposed alternative is:
set chan [open $myfile r]
try { process $chan } finally { close $chan }
This is a saving of two lines, but I think the improvement in
readability is worth it. A possible improvement would be for any errors
in the finally clause to not completely mask any errors in the main
script -- perhaps by adding a -original field to the options dict (which
itself contains the message and options dict of the original error).
I'd be happy to see this part of the TIP dropped or separated into a
different command however.
2. Case-analysis based on return code
-------------------------------------
An example here is that of implementing custom control structures. Of
particular interest here is the use of standard exceptions like [break]
and [continue]. For instance, we may want to write a version of
[foreach] that operates asynchronously using the event loop. Currently,
we might write this as:
proc async-foreach {varName list body} {
if {[llength $list] == 0} { return }
set list [lassign $list item]
set code [catch { apply [list $varName $body] $item } msg opts]
switch $code {
3 { return }
0 - 4 { # ok/continue }
default { return -options $opts $msg }
}
after 0 [list async-foreach $varName $list $body]
}
(I can never remember whether you need to [dict incr opts -level] in
these situations).
The try/handle (Twylite/JE) alternative would be:
proc async-foreach {varName list body} {
if {[llength $list] == 0} { return }
set list [lassign $list item]
try {
apply [list $varName $body] $item
} handle {code msg opts} {
switch $code {
3 { return }
0 - 4 { # ok/continue }
default { return -options $opts $msg }
}
}
after 0 [list async-foreach $varName $list $body]
}
This is slightly longer and introduces more nesting than the original.
In general, it seems to hamper rather than improve readability. By the
way, does "handl" catch all exception types, or just non-errors? In
general, try/handle seems just a more verbose version of the existing
[catch].
The alternative using try/catch would be:
proc async-foreach {varName list body} {
if {[llength $list] == 0} { return }
set list [lassign $list item]
try {
apply [list $varName $body] $item
} catch break {} { return } catch continue {} {}
after 0 [list async-foreach $varName $list $body]
}
This scheme is shorter. It is more readable as we have symbolic names
"break" and "continue" rather than magic numbers, and it avoids catching
anything it doesn't know how to deal with.
Note that dispatch based on exception code is a simple branch. There is
no need for complex pattern matching, sub-match capture, or
case-insensitive matching.
3. Case-analysis based on errorcode
-----------------------------------
For this, I'll use Twylite's example of trying different authentication
schemes. We will assume that the API throws an error with code BADAUTH
when the wrong scheme is used, and throws other errors such as NOCONN to
indicate that a connection to the host failed. (Note: this example
doesn't require glob-matching, but I don't think the changes in such a
case are that great). The existing way to handle this would be:
proc connect {schemes host user pass} {
foreach scheme $schemes {
if {[catch { $scheme connect $host $user $pass } res opts]} {
switch [dict get $opts -errorcode] {
BADAUTH { continue }
default { return -options $opts $res }
}
} else { return $res }
}
error "unable to authenticate"
}
Using the proposed try/onerror approach, this would be:
proc connect {schemes host user pass} {
foreach scheme $schemes {
try {
return [$scheme connect $host $user $pass]
} onerror BADAUTH { continue }
}
error "unable to authenticate"
}
Using try/catch:
proc connect {schemes host user pass} {
foreach scheme $schemes {
try {
return [$scheme connect $host $user $pass]
} catch error {msg opts} {
switch [dict get $opts -errorcode] {
BADAUTH { continue }
default { return -options $opts $msg }
}
}
}
error "unable to authenticate"
}
OK, in this case the try/onerror approach certainly is clearer, and the
try/catch approach is little better than the existing way with [catch]
alone. I think it still wins slightly over [catch] in readability. While
it is more verbose, the control flow is easier to read -- the [return]
is in an obvious place, and not tucked away in an inconspicuous "else"
clause. There's also less punctuation. But the onerror approach is
clearly better for this case.
The questions then, are whether "onerror" is sufficient for all/most
such cases, and whether these cases actually arise often/ever in
practice (or would arise given appropriate promotion). Regarding the
first part, dispatching based on errorcode is more complex than based on
return code as while the latter is a simple integer, the former can be
an arbitrarily complex data structure. In particular, the following
requirements may have to be considered:
a. Different forms of pattern-matching (e.g. exact, glob, regexp,
"algebraic" type matching etc). If we stick to one type only, will that
be appropriate? Will it cause problems? (e.g. if glob-only matching,
then we have problems specifying glob-special characters such as * or []).
b. Case sensitivity -- is "arith" the same error as "ARITH" or "ARiTh"?
c. Sub-match capture: an errorcode may contain detail fields which we
want to extract. It seems pointless to match once and then perform a
separate extraction when I could have just used [regexp] or some other
matching facility and performed both operations in one go.
d. Disjunctive matching: perform this action if the errorcode matches
either *this* or *that*.
I'm sure there are others. To me, the range of choices here suggests
that pattern matching is best kept separate from error-handling.
Otherwise there is a risk of duplicating [switch]. Perhaps I am wrong
here, and glob-matching meets all requirements. Personally, if errorcode
matching was to take off, I would use some form of algebraic types
(tagged lists) both for constructing and pattern-matching errorcodes, as
that seems to me to be the most appropriate tool for the job. Perhaps
there is a way to keep the behaviour but to parameterise the matching
command (with switch -glob/string match being the default).
The other question is whether these cases arise in practice. I can't
think of a single existing API that requires this kind of errorcode
pattern matching. Is such a design even appropriate? Clearly, if you
controlled the authentication scheme interface then you could just
return continue for BADAUTH and an error for anything else:
proc connect {schemes host user pass} {
foreach scheme $schemes {
return [$scheme connect $host $user $pass]
}
error "unable to authenticate"
}
4. Trapping only those errors/exceptions you are interested in
--------------------------------------------------------------
It's clear from the above that the Twylite/JE approach achieves this for
errors, but not for other exceptions. My approach achieves it for
exceptions but not for more specific error cases.
> NEM:
>> Firstly, "onerror" and "except" seem like bad names to me. "except" in
>> particular would imply that the following error case *isn't* handled (as
>> in "catch everything *except* for these..."), which is just confusing.
>> I also have some problems with the usage. I'd prefer to see something
>> like:
> I accept your argument about "except", having had the same concern
> myself. I drew this from C's try...except. Others have argued against
> the use of 'catch' as it could be confused with the existing catch
> command. I'll consider 'handle' instead of catch - it sounds reasonable
> for the domain; other suggestions are welcome. I feel that "onerror" is
> correctly named though.
I'd really prefer things to be unified in name at least: "on error", "on
break", etc. One possible unification that might please all would be to
adopt the syntax "on exception-type ?pattern? ?vars? body". The pattern
is an optional glob-style pattern that is matched against the -errorcode
of the exception. (If the pattern is specified then so must the vars).
Clearly this is mostly useful in the case of errors, but I believe it is
possible for non-error exceptions to also set -errorcode, so it might be
useful elsewhere. That would result in the following use-cases:
proc connect {schemes host user pass} {
foreach scheme $schemes {
try {
return [$scheme connect $host $user $pass]
} on error BADAUTH {} { continue }
}
error "unable to authenticate"
}
proc async-foreach {varName list body} {
if {[llength $list] == 0} { return }
set list [lassign $list item]
try {
apply [list $varName $body] $item
} on break { return } on continue {}
after 0 [list async-foreach $varName $list $body]
}
To me this seems like a good compromise, and people who want more
complex pattern matching can still do so. I'd like to be able to support
lists of exception types. I still don't believe glob-style errorCode
pattern matching is useful or particularly satisfactory, but I'm willing
to concede it for the sake of compromise. As before, define "then" as
"on ok", and possibly define "else/otherwise" as a catch-all default clause.
<aside>
I also see this meshing nicely with a hypothetical future
continuation-based exception mechanism that allows
resumable/non-destructive exception/warning notifications.
</aside>
[...]
> On supporting widely-used idioms, it would be much more important IMO to
> branch based on the result than on the return code. Branching based on
> return code (i.e. exception handling) is useful for creating new control
> structures, but if you want to handle errors then you must branch based
> on some information that is available from a return code = TCL_ERROR
> (2). That means either errorCode or result, and right now most APIs are
> using result.
> You make a good point about leaving pattern matching to existing
> commands -- I have been thinking along those lines for how best to
> exploit that (more in another mail).
Branching based on the result just seems like a fragile nightmare.
Localised error messages for instance could totally change the behaviour
of code.
[...]
>> People don't do it because it just isn't very useful. Errors in Tcl
>> tend to be real errors -- other than logging them, there is often not
>> much to do. Tcl's introspection, use of general control exceptions
>> like [continue]/[break], and custom control structures/HOFs etc make
>> this kind of exception-based case analysis much less necessary. I may
>> well be wrong about this, but I'd prefer to see some concrete use-cases.
> The nature of errors in Tcl is a side-effect of the weak support for
> distinguishing between types of errors. This functionality is useful
> any time that you are calling into an opaque API and can take different
> recovery actions based on the cause of the error. e.g. you want to wrap
> load balancing and/or fault tolerance (simplest case: auto-reconnect)
> around an RPC or DB interface; you want to try alternative
> authentication schemes when the password fails (but not when the
> connection fails, or the protocol is mismatched); you want to tell the
> user whether to 'try again later' or 'call the administrator'.
Thanks for these use-cases.
> Let's drop to Java-speak for a moment: the current practice of Tcl
> developers is to "catch (Exception e) { // handle }". In C++ it is
> "catch (...) { // handle }".
> Return code is not a mechanism for exception/error typing, it is a
> mechanism for implementing control flows. We need a typing mechanism.
Hmmm... error handling needs case-analysis not typing per se. Clearly,
any mechanism used for constructing error cases is strongly related to
the mechanism that is used for distinguishing them, which is why I'd
rather see these aspects separated/parameterised from the exception
handling, at least until it is clear what the best mechanism for this is.
[...]
-- Neil
This message has been checked for viruses but the contents of an attachment
may still contain software viruses, which could damage your computer system:
you are advised to perform your own checks. Email communications with the
University of Nottingham may be monitored as permitted by UK legislation.
|
|
From: Donal K. F. <don...@ma...> - 2008-11-20 16:41:10
|
Neil Madden wrote: > I believe this is already addressed by the presence of errorCode. > Pattern-matching against this code is already quite simple, with > [switch] and so on. It just hasn't caught on. It seems like wishful > thinking to expect that it will suddenly catch on just because its usage > is made slightly more convenient (we're talking about a reduction of > about 1 line of code). Also a reduction in the nesting depth. That reduces user-visible complexity. That's a good bonus. > 4. Trapping only those errors/exceptions you are interested in > -------------------------------------------------------------- > > It's clear from the above that the Twylite/JE approach achieves this for > errors, but not for other exceptions. My approach achieves it for > exceptions but not for more specific error cases. The problem with making more use of exception codes is that they're a global resource; they're not easy to restrict to a single interpreter. That makes them significantly less suitable for use in packages, and hence restricts their general applicability rather a lot. They've also got hard-coded traceability behaviour (i.e. none). > Branching based on the result just seems like a fragile nightmare. > Localised error messages for instance could totally change the behaviour > of code. Agreed. And localizing error messages is messy too without in effect the effort required to generate errorcodes properly. Which in turn makes the error trapping proposed viable and useful. :-) Donal. |
|
From: Joe E. <jen...@fl...> - 2008-11-20 18:39:30
|
Neil Madden wrote:
> The try/handle (Twylite/JE) alternative would be:
>
> proc async-foreach {varName list body} {
> if {[llength $list] == 0} { return }
> set list [lassign $list item]
> try {
> apply [list $varName $body] $item
> } handle {code msg opts} {
> switch $code {
> 3 { return }
> 0 - 4 { # ok/continue }
> default { return -options $opts $msg }
> }
> }
> after 0 [list async-foreach $varName $list $body]
> }
A clarification: this is not what I proposed in [1].
In a clause like:
handle {code ?resultVar ?optionsVar??} { script }
"code" would be an integer or a literal "ok"/"error"/etc.,
not a variable name. Same as the
catch code {?resultVar ?optionsVar??} { script }
form you proposed, just spelled slightly differently.
What I had in mind would look like:
try {
apply [list $varName $body] $item
} handle return {
return
} handle ok {
# no-op
} handle continue {
# no-op
}
The clause "default { return -options $opts $msg }" from
the first script is unneeded, since that's the default
behaviour of [try] when no "handle" or "onerror" clause
matches.
--Joe English
[1] <URL: http://sourceforge.net/mailarchive/message.php?msg_name=E1L2s55-0001xW-6L%40eurydice.office.flightlab.com >
|
|
From: Magentus <mag...@gm...> - 2008-11-22 03:49:54
Attachments:
signature.asc
|
On Thu, 20 Nov 2008 10:39:26 -0800,
Joe English <jen...@fl...> wrote:
> handle {code ?resultVar ?optionsVar??} { script }
Is there any actual practical use to putting code in the braces? The
variables, I don't think can be avoided in this style, although I'm not
sure that there is a need to have every single branch specify the
variables individually. I'd much prefer to have them specified once at
the top (and optionally at that) rather than repeated monotonously on
every single branch.....
Something like a:
withvars {resultVar ?optionsVar?}
following the main try script indicating where to stash the variables.
(I did mention that in my last post that hasn't yet been passed through)
Although personally I'd prefer a -vars option to [try], despite the
obvious issue of not being allowed to have a command called -vars that
_takes no arguments_. Is anyone seriously going to try and insist
that that's a big issue?!? If it's a variable name, then fine, it's a
very realistic problem, but what static script is going to non-obviously
conflict (the main use-case), and for passed in scripts through
variables, just use the -- guard option. From using [switch], it's
common to put -- before variables anyhow. I know a "Better Way" would
be preferably, but it's the least ugly and most practical so far.
In any case, a nice clean simple layout seems to be:
handle pattern {...}
where the return code is TCL_ERROR and pattern is glob matched against
errorcode.
on code pattern {...}
now pattern matches against the return value, even if code is
TCL_ERROR. More usefully, it'll gracefully handle applications which
have multiple kinds of success, as well as multiple kinds of error,
and can handle both regular bare [error] returns and the use of
errorcode with equal ease.
Supporting all the different types of match, though, would be a "Good
Thing". -glob, -nocase, -exact, -prefix, -regexp, and so forth...
That's why I think the next part is worth giving a second thought.
For the blending with [if] option, there was chatter a while back about
fast [expr]-local variables intended mostly to hold partial results
during an expression; the main terms of the options dict could quite
readily be pre-loaded as [expr]-local variables.
A little care might be needed to avoid shadowing the variable you wish
to use to hold the value to compare, but that should be doable.
Perhaps [expr]-local variables should use a %syntax synonymous with the
regular $syntax for variables to avoid name clashes (and highlight
the fact that they're [expr]-local).
All this could further be put off by implementing it as a separate
[try] term later, for when extended matching is needed. Then you've got
"handle" and "on" for just about everything, and "when" or even "if"
for anything else. (Imagine an ambiguous case among more readily
matchable cases, that would otherwise force you to break the [try] into
a much less easy to read chunk of code.)
--
Fredderic
Debian/unstable (LC#384816) on i686 2.6.23-z2 2007 (up 44 days, 21:32)
|
|
From: Neil M. <ne...@Cs...> - 2008-11-22 14:08:36
|
On 22 Nov 2008, at 03:49, Magentus wrote:
> On Thu, 20 Nov 2008 10:39:26 -0800,
> Joe English <jen...@fl...> wrote:
>
>> handle {code ?resultVar ?optionsVar??} { script }
>
> Is there any actual practical use to putting code in the braces? The
> variables, I don't think can be avoided in this style, although I'm
> not
> sure that there is a need to have every single branch specify the
> variables individually. I'd much prefer to have them specified
> once at
> the top (and optionally at that) rather than repeated monotonously on
> every single branch.....
Agreed on both counts.
> [...]
> Supporting all the different types of match, though, would be a "Good
> Thing". -glob, -nocase, -exact, -prefix, -regexp, and so forth...
> That's why I think the next part is worth giving a second thought.
If these are supported it should be only by explicit delegation to
[switch], rather than making all these options part of the interface
of [try] too.
I proposed the following interface offline to Twylite (slightly
altered) yesterday:
try ?-matchcommand cmd? script ?handlers ...? ?finally script?
Where -matchcommand is the command to use to do errorCode matching
and defaults to {switch -glob --}. (May need a -- marker to eliminate
ambiguity). The syntax of the handlers part would be:
on exception-types ?vars? ?errorPattern? body ?errorPattern
body ...?
Where exception-types is a list of one or more of "ok", "error",
"return", "break", "continue" or an integer return code. errorPattern
is a pattern (specific to the -matchcommand) used to match against
the -errorcode (if any). ?vars? is a list of var names {?resultVar ?
optsVar??} and finally "body" is a script. If you want to specify the
errorPattern you must also specify the vars, to avoid ambiguity. If
you want to specify multiple patterns, then the first errorPattern is
mandatory. The try command then builds a lookup table mapping return
code -> (ordered) dict of patterns/scripts. It does a simple lookup
based on the return code of the initial script and then passes the
errorCode and dict of patterns to the -matchcommand, i.e. it would
become [switch -glob -- $errorCode $handlers]. If there is no
errorCode (e.g. a non-error exception) then the empty string is
passed. A "default" branch is also added to the end of the handlers
which just rethrows the exception, or if a branch with no pattern is
specified then this becomes the default branch. The body of a handler
can be "-", with the same meaning as in [switch]. To be concrete, the
syntax would look something like:
try {
...
} on error {msg opts} {IO *} - {POSIX *} {
...
} {ARITH *} {
...
} on {break continue} {
...
} finally {
...
}
You could also write those "on error" clauses as:
} on error {msg opts} {IO *} - {POSIX *} {
...
} on error {msg opts} {ARITH *} {
...
}
Or any combination of the styles you prefer (the former allows you to
specify the vars once, the latter may be more readable). The core
dispatch then becomes something like:
set rc [catch { $script } msg opts]
invoke 1 {*}$matchcmd [dict get $opts -errorcode] [dict get
$handlers $rc]
As before, "then" could be taken as sugar for "on ok", and some catch-
all "else/otherwise" could be added.
It seems to me that this covers most (all?) use-cases, while still
keeping [try] relatively minimal and efficient. In particular, [try]
avoids acquiring the interface of any particular pattern matching
construct. It can also support various types of matching (e.g. I can
use algebraic types, OO enthusiasts can use sub-class matching, and
others can use arbitrary expressions). The core [try] command only
needs to implement basic return-code based matching, which is O(1),
so remains efficient for those implementing control structures over
the top of this. Examples of customisation:
interp alias {} datatry {} try -matchcommand {datatype match}
interp alias {} regtry {} try -matchcommand {switch -regexp -
nocase --}
interp alias {} exptry {} try -matchcommand expmatch
proc expmatch {code cases} {
foreach {case body} $cases {
if {$case eq "default" || [expr $case]} { return [uplevel
1 $body] }
}
}
exptry { ... } on error {msg opts} {[lindex $code 2] > 1000} { ... }
Thoughts? I'm sure the syntax may need some jiggling to get order of
things optimal and to eliminate any remaining ambiguities.
-- Neil
This message has been checked for viruses but the contents of an attachment
may still contain software viruses, which could damage your computer system:
you are advised to perform your own checks. Email communications with the
University of Nottingham may be monitored as permitted by UK legislation.
|
|
From: Magentus <mag...@gm...> - 2008-11-23 13:29:27
Attachments:
signature.asc
|
On Sat, 22 Nov 2008 14:06:59 +0000,
Neil Madden <ne...@Cs...> wrote:
>> Supporting all the different types of match, though, would be a
>> "Good Thing". -glob, -nocase, -exact, -prefix, -regexp, and so
>> forth... That's why I think the next part is worth giving a second
>> thought.
> If these are supported it should be only by explicit delegation to
> [switch], rather than making all these options part of the interface
> of [try] too.
I've had the impression for a while now that TCL needs a generic
extensible matching framework that can be plugged into any command.
Not gonna make it into 8.6, but might be worth considering for the
future of not only [try] but the rest of TCL also. My thoughts have so
far run something like this;
Start with a Tcl_Parse_Match_Options() building a struct that can be
passed to a matching Tcl_Perform_String_Match() function to match two
strings with previously specified options. TPMO() would accept any
option not already recognised (or vice-versa), and if it's one of its
options, consume it and add it into the current matching options struct
(allowing the options to be inter-mixed with those of the surrounding
command).
To alleviate the problem of option bloat, a good deal of them could
perhaps be eventually phased out in deference to a single -match option
(-glob, -exact, -prefix, -regexp, etc.), allowing new match functions
to be added to a ::tcl::match namespace, and picked up automatically by
every command using the matching framework. The modifiers (such as
-nocase) would become options to the -match options value.
eg. -match {glob -nocase}
The commands in ::tcl::match would ALL support extraction of the
matched regions, ala [regexp]. But they would ONLY support the -index
method (for efficiency), returning the start and stop of each matched
section. The enclosing command (or a secondary helper) would be
responsible for extracting the actual corresponding strings.
A second-layer matching framework could well extend it a step further
to also pick up on the grouping options, index options, and similar,
reducing [lsearch] to nothing but a very simple wrapper, and allowing
an [lmatch] command that walks two lists, instead of one list and a
static value, in the same manor.
Further, the -match method could be "complex", indicating that the match
arguments are themselves distinct -match values (which in turn may be
nested "complex" descriptions), allowing it to handle deep matching of
any repeated regular nested list structure.
Finally, the original TPMO() function (and the complex match method)
can look at the match it's being asked to use, and if it's one of the
built-in matches, obtain the core match function to be invoked
directly. For other non-built-in matches, it'd invoke a wrapper that
in turn invokes the specified TCL command, though with as much of
the pre-invocation setup as possible already performed to hopefully
alleviate some of the -matchcommand slow-down.
--
Fredderic
Debian/unstable (LC#384816) on i686 2.6.23-z2 2007 (up 46 days, 7:13)
|
|
From: Joe E. <jen...@fl...> - 2008-11-20 18:18:39
|
Twylite wrote:
>
> First up, I want to be clear about the intent behind TIP #329:
> a) The TIP is about error handling, not exception handling. Tcl has
> excellent exception handling which can be readily used in a mostly
> non-ugly way (e.g. catch + if/then/else or catch + switch),
[ FWIW, I don't fully agree. TIP#89 made correct exception
handling _possible_, but it's still not very convenient or pretty.
That's why I think it's essential that TIP#239 support exception
handling as well, not just error handling. Especially since it
can easily do so with only minor changes. ]
> but its error handling is poor.
Full agreement here.
> > I don't like the alternative approach [to try/finally] much either,
> > simply because it smells too much like innovation.
> >
> And there I was thinking that Tcl generally supported innovation ;p
Again: I think the alternative approach sounds interesting
and is worth pursuing, just *not in this TIP*, and *not right now*.
We have a short deadline for 8.6.
[Elsewhere]
Neil Madden wrote:
[ re: error handling ]
> I believe this is already addressed by the presence of errorCode.
> Pattern-matching against this code is already quite simple, with
> [switch] and so on. It just hasn't caught on. It seems like wishful
> thinking to expect that it will suddenly catch on just because its usage
> is made slightly more convenient (we're talking about a reduction of
> about 1 line of code).
Some anecdotal evidence, FWIW: reviewing some old code I
came across two places where I was initially tempted
to use meaningful -errorcodes, but then thought
"Why bother? I'm never going to look at them,
the [if {[catch { }]} { switch $::errorCode { ... } }]
idiom is utterly unperspicuous," so I didn't.
Had a nice try/onerror construct been available,
I would have. So I don't think the hypothesis
is entirely without merit :-) Sure, it's only
a reduction of about 1 line of code, but that 1
line is *ugly* code.
--Joe English
jen...@fl...
|
|
From: Michael S. <sc...@un...> - 2008-11-20 20:16:26
|
Am 20.11.2008 um 19:18 schrieb Joe English:
> Some anecdotal evidence, FWIW: reviewing some old code I
> came across two places where I was initially tempted
> to use meaningful -errorcodes, but then thought
> "Why bother? I'm never going to look at them,
> the [if {[catch { }]} { switch $::errorCode { ... } }]
> idiom is utterly unperspicuous," so I didn't.
Grepping inside Tcllib also shows rather weak usage of -errorcode.
Some of the network protocols (LDAP, SPF), ASN, wip, some math
subpackages, comm.
Michael
|
|
From: Kevin K. <ke...@ac...> - 2008-11-21 13:17:40
|
Joe English wrote:
> Some anecdotal evidence, FWIW: reviewing some old code I
> came across two places where I was initially tempted
> to use meaningful -errorcodes, but then thought
> "Why bother? I'm never going to look at them,
> the [if {[catch { }]} { switch $::errorCode { ... } }]
> idiom is utterly unperspicuous," so I didn't.
> Had a nice try/onerror construct been available,
> I would have. So I don't think the hypothesis
> is entirely without merit :-) Sure, it's only
> a reduction of about 1 line of code, but that 1
> line is *ugly* code.
I think it might be informative to separate the discussion
of "meaningful errorcodes" from try/catch/finally, because
I want meaningful errorcodes for an entirely different reason.
For error messages that are likely to reach an application's
user, there is likely to be a desire for localisation.
The commonest case is filesystem errors, where presenting
the OS status ("file not found", "no permission", "I/O
error", etc.) appropriately translated makes sense. Right
now, that's incredibly hard; you have to parse the error
message and write a new one. It would be nice to have
at least *some* error codes that can hook into msgcat
gracefully.
--
73 de ke9tv/2, Kevin
|
|
From: <dg...@ni...> - 2008-11-21 18:26:04
|
Quoting "Kevin Kenny" <ke...@ac...>:
> The commonest case is filesystem errors, where presenting
> the OS status ("file not found", "no permission", "I/O
> error", etc.) appropriately translated makes sense. Right
> now, that's incredibly hard; you have to parse the error
> message and write a new one.
I agree with your reasoning, but you've chosen bad examples.
I/O errors are some of the few that actually set meaningful
-errorcode values:
% catch {open no-such} m o
1
% dict get $o -errorcode
POSIX ENOENT {no such file or directory}
DGP
|
|
From: Magentus <mag...@gm...> - 2008-11-21 17:37:02
Attachments:
signature.asc
|
On Thu, 20 Nov 2008 12:51:12 +0200,
Twylite <tw...@cr...> wrote:
The [finally script] usage is trivial to implement using unset traces
(although not quite as clean, mostly since it uses a magic variable
name).
proc finally {script {varName --finally--trap--}} {
upvar 1 $varName var
trace add variable var unset [list apply [list args $script]]
} ;# or something to that effect...
The [try] command for matching on something other than the return code
is excellent. Especially if it can match on return values as well as
errorcodes. How about this for a twist on the idea...
try {
script
} catch {
var ?opts?
} then {
script
} handler .....and so on.....
It's a bit freaky, with one or two variable names being where a script
chunk should be, but solves the question of where to put the return
string and option variables. Alternatively [try ?-vars ...? ....] or
something.
Regardless, why not have the handler clause evaluate an expression in
the context of a [dict with $opts]? Then you can use whatever matching
function you wish, the only minor pain is that you have to use some
ugly bracketing of the option names { ${-code} == 2 }. But maybe
there's a way around that, too, especially if the [dict with] is
doable read-only and non-destructively somehow.
And finally for over-all syntax, what'd be wrong with tagging the
try clauses onto the end of the present [catch] command. Make the
options variable mandatory in this usage, and bring it into scope for
the evaluations as above.
Just one of my warped out there thoughts (assuming someone hasn't
suggested the very same thing on the next yet-to-be-read message...
And if they have, count this as a vote for that! ;) ).
--
Fredderic
Debian/unstable (LC#384816) on i686 2.6.23-z2 2007 (up 44 days, 11:43)
|
|
From: Twylite <tw...@cr...> - 2008-11-19 10:43:05
|
Hi,
>> I still have concerns about this though:
>> - The construct is getting pretty bulky - is it really adding power &
>> simplicity as it is intended to do?
> Why are you using all of it at once? That's its most bulky case. (It's
> also something that's quite a PITA to write with just [catch], speaking
> as someone who's done that in the past...)
Fair point. In most cases only a subset of the full functionality will
be used.
OTOH I've had a stab at writing the synopsis line for the man page, and
the bulkiness/complexity is not funny.
>> - The "except" clause needs to be flexible enough to handle the
>> expected common use cases, but not so flexible/complex that it's
>> easier to just use catch. There is little clarity on what the
>> "common use cases" are.
> So long as we can do multiple 'onerror' clauses and can omit the capture
> variables, we'll be OK. (The errorinfo and errorcode can be picked out
> of the options.) For the 'except' clause, it's probably only necessary
As currently specified you would use "onerror {glob ?emvar? ?optsvar?}",
but if you omit emvar/optsvar then you have no access to this
information within that handler.
> The other thing is that the various trap clauses should be arbitrarily
> reorderable.
They are. The syntax is "try /script/ ?handler? ?handler ...? ?finally
body?" where handler is one of:
then body
onerror {glob ?emvar? ?optsvar?} body
except {spec ?emvar? ?optsvar?} body
There can only be one 'then' handler. Handlers are searched in order
(left-to-right) until a matching one is found, and that one is
executed. Following handler executing the finally body (if any) is
executed. The 'finally' clause can be placed in any position where a
handler is valid/expected (it doesn't have to be at the end).
>> - Often information about an error is implied in the result (as in
>> Tcl_SetResult) rather than an errorCode -- can except (or some other
>> clause) adequately handle this case?
> Wrong question. The right question is "should it handle the case?" and I
> think the answer is "no". Let's clean up the problems with critical info
> not going into the errorcode instead (and I know that might take a bit).
The problem with that approach is that it forces a workaround (via
'catch' or an 'except' handler) when integrating with legacy
code/components that are naughty.
>> A modification I find particularly interesting is an alternative
>> approach to finally.
> I don't like that nearly as much, FWIW.
I'd be interested in reasons/insight?
Regards,
Twylite
|
|
From: Donal K. F. <don...@ma...> - 2008-11-19 11:01:25
|
Twylite wrote:
> OTOH I've had a stab at writing the synopsis line for the man page, and
> the bulkiness/complexity is not funny.
Can't be helped. It's not as bad as [lsearch] or [canvas]...
> As currently specified you would use "onerror {glob ?emvar? ?optsvar?}",
> but if you omit emvar/optsvar then you have no access to this
> information within that handler.
Sounds acceptable to me. If people want the info, they'll capture it.
OTOH, they might be quite happy without. (For example, if trapping a
particular POSIX error from [open].) The only real question is whether
the msgvar and optvar should be that way round. Pass on that! :-)
>>> - Often information about an error is implied in the result (as in
>>> Tcl_SetResult) rather than an errorCode -- can except (or some other
>>> clause) adequately handle this case?
>> Wrong question. The right question is "should it handle the case?" and I
>> think the answer is "no". Let's clean up the problems with critical info
>> not going into the errorcode instead (and I know that might take a bit).
> The problem with that approach is that it forces a workaround (via
> 'catch' or an 'except' handler) when integrating with legacy
> code/components that are naughty.
Can't be helped. If one bends over backwards too far, one falls over and
fail to achieve one's goals. I think this is one of those times...
>>> A modification I find particularly interesting is an alternative
>>> approach to finally.
>> I don't like that nearly as much, FWIW.
> I'd be interested in reasons/insight?
It doesn't feel right. Yeah, I know that's not the strongest of points,
but I think it's probably based on the fact that most users^Wprogrammers
are *very* confused by the whole idea of out-of-order execution. They
find [after] and [fileevent] difficult. [bind] too. :-\
Donal.
|
|
From: Joe E. <jen...@fl...> - 2008-11-19 19:01:13
|
Donal K. Fellows wrote:
> Twylite wrote:
> > [...]
> > As currently specified you would use "onerror {glob ?emvar? ?optsvar?}",
> > but if you omit emvar/optsvar then you have no access to this
> > information within that handler.
> Sounds acceptable to me. If people want the info, they'll capture it.
> OTOH, they might be quite happy without. [...]
> The only real question is whether
> the msgvar and optvar should be that way round. Pass on that! :-)
Yes, they are in the right order.
[ ... ]
> >>> A modification I find particularly interesting is an alternative
> >>> approach to finally.
> >> I don't like that nearly as much, FWIW.
> > I'd be interested in reasons/insight?
>
> It doesn't feel right. Yeah, I know that's not the strongest of points,
> but I think it's probably based on the fact that most users^Wprogrammers
> are *very* confused by the whole idea of out-of-order execution. They
> find [after] and [fileevent] difficult. [bind] too. :-\
I don't like the alternative approach much either,
simply because it smells too much like innovation.
try/finally as currently proposed is well-established
in other languages and is known to work well. The
alternative approach certainly sounds interesting
and is probably worth pursuing, but *not for this TIP*.
I want to see this in 8.6. Now is not the time to
invent brand new untested control flow constructs.
--Joe English
|
|
From: Neil M. <ne...@Cs...> - 2008-11-19 15:23:34
|
Twylite wrote:
> Hi,
>>>> TIP #329 Try/Catch/Finally syntax
>>>
>> I'd really like to see this in 8.6. However, I also think
>> it's essential that try/catch be able to dispatch based on
>> the return code, not just $::errorCode, which the current
>> specification does not provide for. Please fix!
>>
> Although I haven't updated the spec, the current proposal is:
> try {
> #code
> } then {
> #success continuation
> } onerror {glob emvar optsvar} {
> #handle errors based on errorCode
> } except {spec emvar optsvar} {
> #handle errors based on any matching information in optsvar
> } finally {
> #always execute this as the last action before leaving the try command
> }
Firstly, "onerror" and "except" seem like bad names to me. "except" in
particular would imply that the following error case *isn't* handled (as
in "catch everything *except* for these..."), which is just confusing. I
also have some problems with the usage. I'd prefer to see something like:
try {
...
} then {
...
} else {
...
} catch error {vars...} {
...
} catch continue {...} {
...
} catch -1200 {...} {
...
} finally {
...
}
(I'm happy to use "on" or "handle" instead of "catch").
The syntax of the catch part would be: catch "?exception-type? vars
script", where exception-type is a non-empty *list* of "error",
"continue", "break", "return", "ok", or numeric return codes. It
defaults to "error". The vars part is just "msgVar ?optsVar?". You must
specify the vars even if not used (e.g. catch continue {} { .. }) to
avoid ambiguity (an alternative would be to make the vars optional and
the exception type mandatory). Note that in this scheme "then" is just
sugar for "catch ok". "else" is a catch-all, and is invoked if no other
catch block matches the return code.
Certainly, I think dispatch based on the return code/exception type
should be at least as easy as based on errorCode -- I can think of lots
of examples of Tcl code that does this, but almost none that actually
looks at errorCode. You might think that is unfortunate, and that it
would be better if more people made use of errorCode, but that's the way
it is. Support for existing, widely-used idioms, should be as much, if
not more, important than promoting lesser-used alternatives. In fact,
I'd leave out entirely the glob-matching on errorCode, and let people do
this with [switch] if they want it:
try {
...
} catch error {msg opts} {
switch -glob [dict get $opts -errorcode] {
{FOO *} { ... }
default { ... }
}
} finally { ... }
To me, this strikes a good balance between flexibility and simplicity.
In particular, it leaves pattern matching to existing commands [switch],
[regexp] and so on. So, for instance we won't down the line have people
asking for catch -regexp or catch -nocase and so on (which would almost
certainly happen if errorCode usage does take on).
Off the top of my head, the following is a sketch implementation (only
partially tested):
proc try {script args} {
set finally [from args finally ""]
set else [from args else ""]
set then [from args then ""]
set handlers [dict create 0 [list _ $then]]
foreach h [lsplit $args "catch"] {
if {[llength $h] ni {2 3}} { error "wrong # args: $h" }
if {[llength $h] == 2} { set h [linsert $h 0 error] }
lassign $h types vars body
foreach type $types {
dict set handlers [returncode $type] [list $vars $body]
}
}
set rc [catch { uplevel 1 $script } msg opts]
if {[dict exists $handlers $rc]} {
lassign [dict get $handlers $rc] vars body
lassign $vars msgVar optsVar
upvar 1 $msgVar cmsg $optsVar copts
set cmsg $msg; set copts $opts
catch { uplevel 1 $body } msg opts
} elseif {$else ne ""} {
if {[catch { uplevel 1 $else } emsg eopts]} {
set msg $emsg; set opts $eopts
}
}
uplevel 1 $finally
dict incr opts -level
return -options $opts $msg
}
(Utility procs at end of this message [*]).
This allows use-cases like:
# Ensure that a channel is closed after use:
# using fd [open $myfile] { puts [read $myfile] }
proc using {varName chan body} {
upvar 1 $varName var
set var $chan
try { uplevel 1 $body } finally { close $chan }
}
proc readfile file { using c [open $file] { return [read $c] } }
# Sugar for db transactions
proc transaction {db body} {
$db start transaction
try {
uplevel 1 $body
} catch {ok continue return} {msg opts} {
$db commit
return -options $opts $msg
} else {
$db abort
}
}
# Version of [dict with] that only updates the dict on success
proc atomic-with {dictVar body} {
upvar 1 $dictVar dict
set env $dict
try { dict with env $body } then { set dict $env }
}
# Mutex locking
proc lock {mutex script} {
thread::mutex lock $mutex
try { uplevel 1 $script } finally { thread::mutex unlock $mutex }
}
# Loop through each line of a file
proc foreach-line {lineVar file body} {
upvar 1 $lineVar line
using chan [open $file] {
while {[gets $chan line] >= 0} {
try { uplevel 1 $body } catch msg { puts "ERROR: $msg" }
}
}
}
Note that these do the Right Thing with regard to most exceptions. In
particular, note that [try] is transparent wrt exceptions it doesn't
catch -- i.e. [try { $script }] is equivalent to $script. For example,
the foreach-line proc only catches errors, so break/continue are
propagated as normal:
foreach-line line foo.txt {
if {[incr i] % 2 == 0} { continue }
puts $line
}
To me, these are the compelling use-cases for a try construct. errorCode
matching seems less generally useful.
----
[*] Utility procs:
proc from {listVar option {default ""}} {
upvar 1 $listVar list
set idx [lsearch -exact $list $option]
if {$idx >= 0} {
set default [lindex $list [expr {$idx+1}]]
set list [lreplace $list $idx [incr idx]]
}
return $default
}
proc lsplit {xs delim} {
set ys [list]
set y [list]
foreach x [lappend xs $delim] {
if {$x eq $delim} {
if {[llength $y]} { lappend ys $y; set y [list] }
} else { lappend y $x }
}
return $ys
}
proc returncode code {
if {[string is integer -strict $code]} { return $code }
return [lsearch -exact {ok error return break continue} $code]
}
-- Neil
This message has been checked for viruses but the contents of an attachment
may still contain software viruses, which could damage your computer system:
you are advised to perform your own checks. Email communications with the
University of Nottingham may be monitored as permitted by UK legislation.
|
|
From: Donal K. F. <don...@ma...> - 2008-11-19 16:39:06
|
Neil Madden wrote:
> Firstly, "onerror" and "except" seem like bad names to me. "except" in
> particular would imply that the following error case *isn't* handled (as
> in "catch everything *except* for these..."), which is just confusing. I
> also have some problems with the usage. I'd prefer to see something like:
I'm going to disagree with you...
> } else {
> ...
> } catch error {vars...} {
> ...
> } catch continue {...} {
> ...
> } catch -1200 {...} {
> ...
Those 'catch' clauses cause me problems. You seem to me to be wanting to
make a single clause for catching all errors, but to me one of the main
points about [try] is that it gets away from that. It's a main point
because it makes a real difference for elevating code above the level of
the current [catch] capabilities. Right now, doing code to handle
different errors with [catch] is fairly ugly (though easier than before
TIP#90) and your proposal doesn't help that much. Twylite's proposal is
more workable, as it matches errors against the errorcode and everything
else against the result code (I suppose we could allow a * for anything
in the spec part of the 'except' matcher, but it's not clear to me why
you'd want to).
> (I'm happy to use "on" or "handle" instead of "catch").
That's more of a bikeshed issue.
> Certainly, I think dispatch based on the return code/exception type
> should be at least as easy as based on errorCode -- I can think of lots
> of examples of Tcl code that does this, but almost none that actually
> looks at errorCode. You might think that is unfortunate, and that it
> would be better if more people made use of errorCode, but that's the way
> it is.
That's why 'onerror' and 'except' clauses are both needed. (The names
could be better, but arguing syntax when the semantics still need fixing
is wasteful.)
> Support for existing, widely-used idioms, should be as much, if
> not more, important than promoting lesser-used alternatives. In fact,
> I'd leave out entirely the glob-matching on errorCode, and let people do
> this with [switch] if they want it:
No. That's advocating a lack of vision. If we make it *easy* to trap
specific errors by errcode, people will do so. People don't now because
it is too hard.
> To me, this strikes a good balance between flexibility and simplicity.
> In particular, it leaves pattern matching to existing commands [switch],
> [regexp] and so on. So, for instance we won't down the line have people
> asking for catch -regexp or catch -nocase and so on (which would almost
> certainly happen if errorCode usage does take on).
What is proposed can still permit such things with an enclosed [switch]
as they can still match all errorcodes with *, but it should encourage
people to make better use of what we've got.
Donal.
|
|
From: Neil M. <ne...@Cs...> - 2008-11-19 17:30:52
|
On 19 Nov 2008, at 16:38, Donal K. Fellows wrote:
> Neil Madden wrote:
>> Firstly, "onerror" and "except" seem like bad names to me.
>> "except" in
>> particular would imply that the following error case *isn't*
>> handled (as
>> in "catch everything *except* for these..."), which is just
>> confusing. I also have some problems with the usage. I'd prefer to
>> see something like:
>
> I'm going to disagree with you...
>
>> } else {
>> ...
>> } catch error {vars...} {
>> ...
>> } catch continue {...} {
>> ...
>> } catch -1200 {...} {
>> ...
>
> Those 'catch' clauses cause me problems. You seem to me to be
> wanting to
> make a single clause for catching all errors, but to me one of the
> main
> points about [try] is that it gets away from that. It's a main point
> because it makes a real difference for elevating code above the
> level of
> the current [catch] capabilities. Right now, doing code to handle
> different errors with [catch] is fairly ugly (though easier than
> before
> TIP#90) and your proposal doesn't help that much. Twylite's
> proposal is
> more workable, as it matches errors against the errorcode and
> everything
> else against the result code (I suppose we could allow a * for
> anything
> in the spec part of the 'except' matcher, but it's not clear to me why
> you'd want to).
Show me the use-cases! Handling different error-cases with [catch] is
not that ugly now:
catch { $script } msg opts
switch -glob [dict get $opts -errorcode] {
... { ... }
... { ... }
default { return -options $opts $msg }
}
People don't do it because it just isn't very useful. Errors in Tcl
tend to be real errors -- other than logging them, there is often not
much to do. Tcl's introspection, use of general control exceptions
like [continue]/[break], and custom control structures/HOFs etc make
this kind of exception-based case analysis much less necessary. I may
well be wrong about this, but I'd prefer to see some concrete use-cases.
>
>> (I'm happy to use "on" or "handle" instead of "catch").
>
> That's more of a bikeshed issue.
>
>> Certainly, I think dispatch based on the return code/exception type
>> should be at least as easy as based on errorCode -- I can think of
>> lots
>> of examples of Tcl code that does this, but almost none that actually
>> looks at errorCode. You might think that is unfortunate, and that it
>> would be better if more people made use of errorCode, but that's
>> the way
>> it is.
>
> That's why 'onerror' and 'except' clauses are both needed. (The names
> could be better, but arguing syntax when the semantics still need
> fixing
> is wasteful.)
>
>> Support for existing, widely-used idioms, should be as much, if
>> not more, important than promoting lesser-used alternatives. In fact,
>> I'd leave out entirely the glob-matching on errorCode, and let
>> people do
>> this with [switch] if they want it:
>
> No. That's advocating a lack of vision. If we make it *easy* to trap
> specific errors by errcode, people will do so. People don't now
> because
> it is too hard.
I just don't see any evidence for this statement (the use-cases will
follow if we just implement this!).
>
>> To me, this strikes a good balance between flexibility and
>> simplicity.
>> In particular, it leaves pattern matching to existing commands
>> [switch],
>> [regexp] and so on. So, for instance we won't down the line have
>> people
>> asking for catch -regexp or catch -nocase and so on (which would
>> almost
>> certainly happen if errorCode usage does take on).
>
> What is proposed can still permit such things with an enclosed
> [switch]
> as they can still match all errorcodes with *, but it should encourage
> people to make better use of what we've got.
-- Neil
This message has been checked for viruses but the contents of an attachment
may still contain software viruses, which could damage your computer system:
you are advised to perform your own checks. Email communications with the
University of Nottingham may be monitored as permitted by UK legislation.
|
|
From: Joe E. <jen...@fl...> - 2008-11-19 19:24:32
|
Neil Madden wrote: > Donal K. Fellows wrote: > > Neil Madden wrote: > >> I'd leave out entirely the glob-matching on errorCode, and let > >> people do > >> this with [switch] if they want it: > > No. That's advocating a lack of vision. If we make it *easy* to trap > > specific errors by errcode, people will do so. People don't now > > because > > it is too hard. > > I just don't see any evidence for this statement (the use-cases will > follow if we just implement this!). There has been a longstanding desire that people [*] make better use of -errorcode, but there's a vicious cycle: nobody bothers to use meaningful -errorcodes because nobody bothers to check -errorcode because there's no convenient way to do so. try/onerror would at least break that part of the cycle. (FWIW, I don't think try/onerror will be immediately useful either, and am not sure that it will _ever_ be useful -- perhaps meaningful -errorcodes wouldn't be that helpful even if we did have them -- but it's worth a try.) --Joe English [*] and by "people" I mean "including core maintainers", and by "including core maintainers" I mean "especially". |
|
From: <lm...@bi...> - 2008-11-19 21:27:11
|
On Wed, Nov 19, 2008 at 11:24:26AM -0800, Joe English wrote: > > Neil Madden wrote: > > Donal K. Fellows wrote: > > > Neil Madden wrote: > > >> I'd leave out entirely the glob-matching on errorCode, and let > > >> people do > > >> this with [switch] if they want it: > > > No. That's advocating a lack of vision. If we make it *easy* to trap > > > specific errors by errcode, people will do so. People don't now > > > because > > > it is too hard. > > > > I just don't see any evidence for this statement (the use-cases will > > follow if we just implement this!). > > There has been a longstanding desire that people [*] make > better use of -errorcode, but there's a vicious cycle: > nobody bothers to use meaningful -errorcodes because nobody > bothers to check -errorcode because there's no convenient > way to do so. Yeah, heaven forbid that there be a concept of NULL in tcl that could be trivially used to indicate EOF/ERROR/whatever. Much better to introduce language constructs (that will probably get used about as much as -errorcode). -- --- Larry McVoy lm at bitmover.com http://www.bitkeeper.com |
|
From: Twylite <tw...@cr...> - 2008-11-20 16:40:13
|
Awesome stuff Neil - thanks for the detailed analysis :)
>> Developers use the result (Tcl_SetResult, not return code) as an
>> error message, representing both the class/category and exact nature
>> of the error (in a human-readable form), but don't provide a way to
>> programatically identify the class of error making control flow based
>> on the type of error a hit-and-miss match on the error code.
> I believe this is already addressed by the presence of errorCode.
> Pattern-matching against this code is already quite simple, with
> [switch] and so on. It just hasn't caught on. It seems like wishful
> thinking to expect that it will suddenly catch on just because its
> usage is made slightly more convenient (we're talking about a
> reduction of about 1 line of code).
As you note, it hasn't caught on. I believe that is because (i) the
language feature to throw errors discourages (or at least does not
encourage) the use of errorCode, and (ii) there is no obvious feature to
branch based on errorCode. I intend to resolve that through this TIP by
making [try] support branching on errorCode, and [throw] reorder the
arguments of [error] to encourage the use of an errorCode.
> I think it is worth explicitly pulling out the separate concerns
> embodied in this TIP and tying them to use-cases. As I see it, there
> are a number of more-or-less separable concerns here:
>
> 1. Support for ensuring resource cleanup in the presence of
> exceptions/errors (i.e. "finally").
> 2. Better support for case-analysis based on exception return code.
> 3. Better support for case-analysis based on errorcode.
> 4. Ability to trap only those exceptions/errors that you are
> interested in/prepared to deal with, letting others propagate normally.
Most wicked - I had just made this list myself (well, the first 3 at
least). We have 3 orthogonal concerns here that we are trying to
shoehorn into one command.
> try { process $chan } finally { close $chan }
>
> This is a saving of two lines, but I think the improvement in
> readability is worth it. A possible improvement would be for any
> errors in the finally clause to not completely mask any errors in the
> main script -- perhaps by adding a -original field to the options dict
> (which itself contains the message and options dict of the original
> error).
>
> I'd be happy to see this part of the TIP dropped or separated into a
> different command however.
I wouldn't be particularly happy to drop/separate this functionality.
Doing so will result in more deeply nested code, because the need for
cleanup regularly interacts with the need to handle (some)
errors/exceptions (rather than let them all propagate).
Also, there are three orthogonal concerns, all wanting to be called
"try", which will just get messy.
My proposal (or at least implementation, I forget) chains errors to the
original in all cases (i.e. if you throw an error from a handler or from
finally).
> 2. Case-analysis based on return code
> An example here is that of implementing custom control structures. Of
> particular interest here is the use of standard exceptions like
> [break] and [continue]. For instance, we may want to write a version
> of [foreach] that operates asynchronously using the event loop.
> Currently, we might write this as:
> The try/handle (Twylite/JE) alternative would be:
>
> } handle {code msg opts} {
> switch $code {
> 3 { return }
> 0 - 4 { # ok/continue }
> default { return -options $opts $msg }
> }
> }
Umm ... no. JE said:
> } handle {code ?resultVar ?optionsVar??} {
> #
> # return code was $code.
> #
> } finally {
>
> where "code" is one of ok/error/return/break/continue
> or an integer literal, a la [return -code].
>
So the use would be
} handle {2 msg opts} {
# handle the case where the return code is 2 (TCL_RETURN)
} handle {1 msg opts} {
# handle the case where the return code is 1 (TCL_ERROR)
}
etc. And you can use the keywords ok/error/return/break/continue
instead of the integer literals.
On http://wiki.tcl.tk/21608 I proposed using the keyword 'except' and
matching on 'spec' which I hadn't defined, but was tentatively assuming
to be the return code.
In other words, our proposal is the same as the try/catch example you
present.
> proc async-foreach {varName list body} {
> if {[llength $list] == 0} { return }
> set list [lassign $list item]
> try {
> apply [list $varName $body] $item
> } catch break {} { return } catch continue {} {}
> after 0 [list async-foreach $varName $list $body]
> }
The difference is that you use
catch on_what_code {?emvar? ?optsvar?} ?body?
and we use
handle {on_what_code ?emvar? ?optsvar?} ?body?
> Note that dispatch based on exception code is a simple branch. There
> is no need for complex pattern matching, sub-match capture, or
> case-insensitive matching.
Mmm ... except you cheat in your example ;)
In the examples using [switch] you use a range (0 - 4), which try/catch
doesn't handle. And you put all the cases on one line ;p
IF a catch/handle can only match on exactly one return code, the
branching is simple. But you may want to match on a range.
Or even against a discrete list:
> The syntax of the catch part would be: catch "?exception-type? vars
> script", where exception-type is a non-empty *list* of "error",
> "continue", "break", "return", "ok", or numeric return codes. It
> defaults to "error".
> 3. Case-analysis based on errorcode
>
> For this, I'll use Twylite's example of trying different
> authentication schemes. We will assume that the API throws an error
> with code BADAUTH when the wrong scheme is used, and throws other
> errors such as NOCONN to indicate that a connection to the host
> failed. (Note: this example doesn't require glob-matching, but I don't
> think the changes in such a case are that great). The existing way to
> handle this would be:
man tclVars: " This list value represents additional information about
the error in a form that is easy to process with programs. The first
element of the list identifies a general class of errors, and determines
the format of the rest of the list."
You need to do pattern matching on errorCode, or select based on [lindex
$::errorCode 0] assuming that only the most general class information is
relevant.
> OK, in this case the try/onerror approach certainly is clearer, and
> the try/catch approach is little better than the existing way with
> [catch] alone. I think it still wins slightly over [catch] in
> readability. While it is more verbose, the control flow is easier to
> read -- the [return] is in an obvious place, and not tucked away in an
> inconspicuous "else" clause. There's also less punctuation. But the
> onerror approach is clearly better for this case.
Forgive me for leaving our the preceding 40 lines of example, and
focusing on this paragraph ;)
> The questions then, are whether "onerror" is sufficient for all/most
> such cases, and whether these cases actually arise often/ever in
> practice (or would arise given appropriate promotion). Regarding the
> first part, dispatching based on errorcode is more complex than based
> on return code as while the latter is a simple integer, the former can
> be an arbitrarily complex data structure. In particular, the following
> requirements may have to be considered:
>
> a. Different forms of pattern-matching (e.g. exact, glob, regexp,
> "algebraic" type matching etc). If we stick to one type only, will
> that be appropriate? Will it cause problems? (e.g. if glob-only
> matching, then we have problems specifying glob-special characters
> such as * or []).
> b. Case sensitivity -- is "arith" the same error as "ARITH" or "ARiTh"?
> c. Sub-match capture: an errorcode may contain detail fields which we
> want to extract. It seems pointless to match once and then perform a
> separate extraction when I could have just used [regexp] or some other
> matching facility and performed both operations in one go.
> d. Disjunctive matching: perform this action if the errorcode matches
> either *this* or *that*.
>
> I'm sure there are others. To me, the range of choices here suggests
> that pattern matching is best kept separate from error-handling.
> Otherwise there is a risk of duplicating [switch].
It seems to me that the [try] must not dictate/limit the matching, but
must facilitate it.
The problem with catch+switch and similar approaches is that they are
_ugly_. Your try/catch suggestion is no more powerful than
catch+switch, but it is more readable. When you scan the code you see
"try ... catch", and the intention of the code is clear. When you see
"catch ... switch" the intention is not clear -- you have no idea that
these two statements are related, and if they are then on what basis are
you switching, etc.
If one accepts that Tcl return codes should be used for flow control,
and that the Tcl return code 1 (TCL_ERROR) covers all errors (which are
called structured exceptions in languages like C++ and Java), and that
there is a need to distinguish between different structured exceptions
(Tcl errors) based on their cause/type/class/nature (whatever you want
to call it), then the inescapable conclusion is that there needs to be a
not-ugly control structure to handle branching based on return code, and
a not-ugly control structure to handle branching based on error cause.
They may or may not be the same control structure, but they are both
equally necessary.
> Perhaps I am wrong here, and glob-matching meets all requirements.
Maybe, maybe not. I'm honestly not sure either way. Assuming that
errorCode is constructed in a hierarchical manner (most general class
first, becoming progressively more specific) then I can't think of a
situation that a glob match can't handle (where an equivalent situation
exists in say Java or C++ that can be handled by their syntax).
Of course its entirely possible that some bright spark declares that if
the general class of errorCode is "OBJECT" then [lindex $errorCode 1] is
an oo::object that is a child of tcl::errorobj, and you want to do
class-based matching on said object. This is probably quite a strong
argument against glob matching (as the only option).
> Personally, if errorcode matching was to take off, I would use some
> form of algebraic types (tagged lists) both for constructing and
> pattern-matching errorcodes, as that seems to me to be the most
> appropriate tool for the job. Perhaps there is a way to keep the
> behaviour but to parameterise the matching command (with switch
> -glob/string match being the default).
Yes, there is, maybe. More below.
> The other question is whether these cases arise in practice. I can't
> think of a single existing API that requires this kind of errorcode
> pattern matching. Is such a design even appropriate? Clearly, if you
> controlled the authentication scheme interface then you could just
> return continue for BADAUTH and an error for anything else:
Depending on your understanding of "pattern matching" ... Java. Like
the whole Java Runtime Library.
Java obviously doesn't use globs to match exceptions, but it does throw
different exceptions and you can catch a group of exceptions based on a
pattern (specifically: the exception object is of a particular class).
errorCode is defined as being a list where the first element identifies
the general class of errors; assuming that this format is maintained
(for legacy compatibility) the most obvious approach to asking "is this
an IO error" is "IO *".
So I have been suggesting "string match" on a structured list as an
approximation of the 'isa' / 'typeof' operator.
My point here applies most specifically to cases where you don't control
the API (which is rather common), but also to cases where you don't want
to modify the API - because it will affect other working code that you
don't want to refactor, or because you believe that adding a flow
control statement like 'break' or 'continue' outside of a flow control
construct is an inherently dangerous code practice because developers
using APIs don't expect stuff like that.
> 4. Trapping only those errors/exceptions you are interested in
> It's clear from the above that the Twylite/JE approach achieves this
> for errors, but not for other exceptions. My approach achieves it for
> exceptions but not for more specific error cases.
I disagree -- is this statement based on a misunderstanding of what
'except' / 'handle' was doing?
> I'd really prefer things to be unified in name at least: "on error",
> "on break", etc. One possible unification that might please all would
> be to adopt the syntax "on exception-type ?pattern? ?vars? body". The
> pattern is an optional glob-style pattern that is matched against the
> -errorcode of the exception. (If the pattern is specified then so must
> the vars). Clearly this is mostly useful in the case of errors, but I
> believe it is possible for non-error exceptions to also set
> -errorcode, so it might be useful elsewhere. That would result in the
> following use-cases:
Anything can set return options (including -errorcode) by using
[return], but only a code 1 return ( [error] ) automatically adds the
-errorcode to the dict.
> proc connect {schemes host user pass} {
> foreach scheme $schemes {
> try {
> return [$scheme connect $host $user $pass]
> } on error BADAUTH {} { continue }
> }
> error "unable to authenticate"
> }
> To me this seems like a good compromise, and people who want more
> complex pattern matching can still do so. I'd like to be able to
> support lists of exception types. I still don't believe glob-style
> errorCode pattern matching is useful or particularly satisfactory, but
> I'm willing to concede it for the sake of compromise. As before,
> define "then" as "on ok", and possibly define "else/otherwise" as a
> catch-all default clause.
It looks promising, but I doubt that errorCode is every really
meaningful outside the context of TCL_ERROR, and I'm leaning towards
glob being insufficient as the only supported matching style. It's all
your fault for being right ;p
> <aside>
> I also see this meshing nicely with a hypothetical future
> continuation-based exception mechanism that allows
> resumable/non-destructive exception/warning notifications.
> </aside>
I've been glimpsing some opportunities here, but I don't work with CPS
enough to have a good idea of how to work it in.
>> On supporting widely-used idioms, it would be much more important IMO
>> to branch based on the result than on the return code. Branching
>> based on return code (i.e. exception handling) is useful for creating
>> new control structures, but if you want to handle errors then you
>> must branch based on some information that is available from a return
>> code = TCL_ERROR (2). That means either errorCode or result, and
>> right now most APIs are using result.
>> You make a good point about leaving pattern matching to existing
>> commands -- I have been thinking along those lines for how best to
>> exploit that (more in another mail).
> Branching based on the result just seems like a fragile nightmare.
> Localised error messages for instance could totally change the
> behaviour of code.
Oh, it is ;( But its also the only option available in some existing
libraries. I've encountered code that puts an error identified in the
result and the error message in errorInfo, for example.
The question then is: how widespread is such (bad) code. Would
developers regularly resort to another branching mechanism (try+switch,
catch+switch, etc.) or would they be able to use the new [try] for the
vast majority of their code?
>> Let's drop to Java-speak for a moment: the current practice of Tcl
>> developers is to "catch (Exception e) { // handle }". In C++ it is
>> "catch (...) { // handle }".
>> Return code is not a mechanism for exception/error typing, it is a
>> mechanism for implementing control flows. We need a typing mechanism.
> Hmmm... error handling needs case-analysis not typing per se. Clearly,
> any mechanism used for constructing error cases is strongly related to
> the mechanism that is used for distinguishing them, which is why I'd
> rather see these aspects separated/parameterised from the exception
> handling, at least until it is clear what the best mechanism for this is.
I'm going to take a stab at identifying what the best mechanism is.
Let's move back for a moment to what we're trying to achieve here.
Functionality:
a. Handle errors by class (errorCode)
b. Handle success continuation (i.e. don't limit the control structure
to only exceptional situations)
c. Handle other (non-error) exceptions
d. It is clear that (a), (b) and (c) are all specialisations of
matching the Tcl return code (and possibly some other return
information, like errorCode and result)
e. Clean up at the end of an operation, regardless of errors/exceptions
Look & Feel:
a. Looks like if/then/else (traditional structured try/catch as in
C++, Java, etc), OR
b. Looks like switch (I don't actually know of another language that
does this), OR
c. Hybrid of (a) and (b): choose between success/error with an
if/then/else like construct, then switch on the specific result/error
(e.g. Erlang)
Matching:
a. Match a single return code, a range of return codes, or a discrete
list of return codes
b. Match a specific error code, and an errorcode pattern (glob-like)
c. Possibly provide for more complex matches involving other fields
(result), regular expressions, disjunctive matching, case sensitivity,
relational calculus/algebra, whatever.
catch+something_else can provide for arbitrary complexity. Should [try]
also do so, or should its complexity be limited? If limited, what is a
reasonable limit? Exact match? Range match? Glob match?
It strikes me that there are two possible solutions:
(1) Make [try] an unholy union of [catch] and [switch] such that the
user can specify the information being switched on (e.g. "%C,%E,%R"
where %C is the return code, %E is the errorcode and %R is the result).
You delegate pattern matching to [switch] and allow the user to match
based on any combination of return code, errorcode and result.
try {
# ...
} thenwith -glob -- "%C,%E" {
"1,POSIX *" { handle posix errors }
"3,*" -
"4,*" { handle break/continue }
}
(2) Make [try] an unholy union of [catch] and [if]/then/else, and
provide helper functions/operations to match exception/error cases with
expr.
try {
# ...
} handle { [string match "POSIX *" $::errorCode] } {
handle posix errors
} handle { code() in {3 4} } {
handle continue
}
Asssuming some minor changes to expr that could look more like:
try {
# ...
} handle { errorcode() like "POSIX *" } {
} handle { code() in {3 4} } {
}
I found the question on Reddit at
http://www.reddit.com/r/programming/comments/7dgwy/ask_proggit_where_clauses_in_catch_declarations/?sort=old
quite interesting in this regard.
Ick ... time to get home.
Twylite
|
|
From: Neil M. <ne...@Cs...> - 2008-11-20 19:11:24
|
On 20 Nov 2008, at 16:39, Twylite wrote:
>> [...]
[on finally:]
>> I'd be happy to see this part of the TIP dropped or separated into a
>> different command however.
> I wouldn't be particularly happy to drop/separate this functionality.
My thought here was merely that a decision on "finally" could be
separated from considerations of try/catch. I.e., it could form a
separate TIP, either extending [try] or adding a separate capability
like your [finally] command or some [ensure] command or something.
Just an idea though -- I think "finally" as part of try is a well
established convention and fairly uncontroversial.
> Doing so will result in more deeply nested code, because the need for
> cleanup regularly interacts with the need to handle (some)
> errors/exceptions (rather than let them all propagate).
> Also, there are three orthogonal concerns, all wanting to be called
> "try", which will just get messy.
>
> My proposal (or at least implementation, I forget) chains errors to
> the
> original in all cases (i.e. if you throw an error from a handler or
> from
> finally).
OK, good.
>> 2. Case-analysis based on return code
>> An example here is that of implementing custom control structures. Of
>> particular interest here is the use of standard exceptions like
>> [break] and [continue]. For instance, we may want to write a version
>> of [foreach] that operates asynchronously using the event loop.
>> Currently, we might write this as:
>> The try/handle (Twylite/JE) alternative would be:
>>
>> } handle {code msg opts} {
>> switch $code {
>> 3 { return }
>> 0 - 4 { # ok/continue }
>> default { return -options $opts $msg }
>> }
>> }
> Umm ... no. JE said:
>> } handle {code ?resultVar ?optionsVar??} {
>> #
>> # return code was $code.
>> #
>> } finally {
>>
>> where "code" is one of ok/error/return/break/continue
>> or an integer literal, a la [return -code].
>>
> So the use would be
> } handle {2 msg opts} {
> # handle the case where the return code is 2 (TCL_RETURN)
> } handle {1 msg opts} {
> # handle the case where the return code is 1 (TCL_ERROR)
> }
> etc. And you can use the keywords ok/error/return/break/continue
> instead of the integer literals.
OK. The comment "return code was $code" made me think that "code" was
a variable, rather than a pattern.
>
> On http://wiki.tcl.tk/21608 I proposed using the keyword 'except' and
> matching on 'spec' which I hadn't defined, but was tentatively
> assuming
> to be the return code.
>
> In other words, our proposal is the same as the try/catch example you
> present.
Great. In that case, I think we're largely in agreement.
>> proc async-foreach {varName list body} {
>> if {[llength $list] == 0} { return }
>> set list [lassign $list item]
>> try {
>> apply [list $varName $body] $item
>> } catch break {} { return } catch continue {} {}
>> after 0 [list async-foreach $varName $list $body]
>> }
> The difference is that you use
> catch on_what_code {?emvar? ?optsvar?} ?body?
> and we use
> handle {on_what_code ?emvar? ?optsvar?} ?body?
>> Note that dispatch based on exception code is a simple branch. There
>> is no need for complex pattern matching, sub-match capture, or
>> case-insensitive matching.
> Mmm ... except you cheat in your example ;)
> In the examples using [switch] you use a range (0 - 4), which try/
> catch
> doesn't handle. And you put all the cases on one line ;p
It's not a range - the "-" in switch means "use the same body as the
next branch". i.e., the example means codes 0 and 4 (ok and
continue), not 0 to 4.
> [...]
> It seems to me that the [try] must not dictate/limit the matching, but
> must facilitate it.
> The problem with catch+switch and similar approaches is that they are
> _ugly_. Your try/catch suggestion is no more powerful than
> catch+switch, but it is more readable. When you scan the code you see
> "try ... catch", and the intention of the code is clear. When you see
> "catch ... switch" the intention is not clear -- you have no idea that
> these two statements are related, and if they are then on what
> basis are
> you switching, etc.
>
> If one accepts that Tcl return codes should be used for flow control,
> and that the Tcl return code 1 (TCL_ERROR) covers all errors (which
> are
> called structured exceptions in languages like C++ and Java), and that
> there is a need to distinguish between different structured exceptions
> (Tcl errors) based on their cause/type/class/nature (whatever you want
> to call it), then the inescapable conclusion is that there needs to
> be a
> not-ugly control structure to handle branching based on return
> code, and
> a not-ugly control structure to handle branching based on error cause.
> They may or may not be the same control structure, but they are both
> equally necessary.
OK. On reflection I'm willing to concede that adequate error case
analysis requires pattern matching of some sort built-in to [try].
>> Perhaps I am wrong here, and glob-matching meets all requirements.
> Maybe, maybe not. I'm honestly not sure either way. Assuming that
> errorCode is constructed in a hierarchical manner (most general class
> first, becoming progressively more specific) then I can't think of a
> situation that a glob match can't handle (where an equivalent
> situation
> exists in say Java or C++ that can be handled by their syntax).
> Of course its entirely possible that some bright spark declares
> that if
> the general class of errorCode is "OBJECT" then [lindex $errorCode
> 1] is
> an oo::object that is a child of tcl::errorobj, and you want to do
> class-based matching on said object. This is probably quite a strong
> argument against glob matching (as the only option).
Good point. I hadn't thought of that. That also could handle subtype-
based matching.
>> Personally, if errorcode matching was to take off, I would use some
>> form of algebraic types (tagged lists) both for constructing and
>> pattern-matching errorcodes, as that seems to me to be the most
>> appropriate tool for the job. Perhaps there is a way to keep the
>> behaviour but to parameterise the matching command (with switch
>> -glob/string match being the default).
> Yes, there is, maybe. More below.
>> The other question is whether these cases arise in practice. I can't
>> think of a single existing API that requires this kind of errorcode
>> pattern matching. Is such a design even appropriate? Clearly, if you
>> controlled the authentication scheme interface then you could just
>> return continue for BADAUTH and an error for anything else:
> Depending on your understanding of "pattern matching" ... Java. Like
> the whole Java Runtime Library.
> Java obviously doesn't use globs to match exceptions, but it does
> throw
> different exceptions and you can catch a group of exceptions based
> on a
> pattern (specifically: the exception object is of a particular class).
> errorCode is defined as being a list where the first element
> identifies
> the general class of errors; assuming that this format is maintained
> (for legacy compatibility) the most obvious approach to asking "is
> this
> an IO error" is "IO *".
> So I have been suggesting "string match" on a structured list as an
> approximation of the 'isa' / 'typeof' operator.
Which could be workable: the OO exception just dumps [$self info
ancestors] or whatever into the errorCode, so you have something
like: [list Error IOError HostUnreachable object12].
> My point here applies most specifically to cases where you don't
> control
> the API (which is rather common), but also to cases where you don't
> want
> to modify the API - because it will affect other working code that you
> don't want to refactor, or because you believe that adding a flow
> control statement like 'break' or 'continue' outside of a flow control
> construct is an inherently dangerous code practice because developers
> using APIs don't expect stuff like that.
Well, all exceptions affect flow control.
> [...]
>> proc connect {schemes host user pass} {
>> foreach scheme $schemes {
>> try {
>> return [$scheme connect $host $user $pass]
>> } on error BADAUTH {} { continue }
>> }
>> error "unable to authenticate"
>> }
>> To me this seems like a good compromise, and people who want more
>> complex pattern matching can still do so. I'd like to be able to
>> support lists of exception types. I still don't believe glob-style
>> errorCode pattern matching is useful or particularly satisfactory,
>> but
>> I'm willing to concede it for the sake of compromise. As before,
>> define "then" as "on ok", and possibly define "else/otherwise" as a
>> catch-all default clause.
> It looks promising, but I doubt that errorCode is every really
> meaningful outside the context of TCL_ERROR, and I'm leaning towards
> glob being insufficient as the only supported matching style. It's
> all
> your fault for being right ;p
:-) Sorry!
I agree that errorcode is pretty unlikely to be useful outside of
errors, hence making it optional. I still quite like the symmetry of
this proposal.
>> <aside>
>> I also see this meshing nicely with a hypothetical future
>> continuation-based exception mechanism that allows
>> resumable/non-destructive exception/warning notifications.
>> </aside>
> I've been glimpsing some opportunities here, but I don't work with CPS
> enough to have a good idea of how to work it in.
It's a nice vision, but not workable within a 8.x timeframe. To
really work, it would require changing the way C code reports
exceptions (e.g., calling some Tcl_Throw rather than returning an
error code). With the NRE changes though, this kind of thing begins
to look more feasible though.
>>> On supporting widely-used idioms, it would be much more important
>>> IMO
>>> to branch based on the result than on the return code. Branching
>>> based on return code (i.e. exception handling) is useful for
>>> creating
>>> new control structures, but if you want to handle errors then you
>>> must branch based on some information that is available from a
>>> return
>>> code = TCL_ERROR (2). That means either errorCode or result, and
>>> right now most APIs are using result.
>>> You make a good point about leaving pattern matching to existing
>>> commands -- I have been thinking along those lines for how best to
>>> exploit that (more in another mail).
>> Branching based on the result just seems like a fragile nightmare.
>> Localised error messages for instance could totally change the
>> behaviour of code.
> Oh, it is ;( But its also the only option available in some existing
> libraries. I've encountered code that puts an error identified in the
> result and the error message in errorInfo, for example.
> The question then is: how widespread is such (bad) code. Would
> developers regularly resort to another branching mechanism (try
> +switch,
> catch+switch, etc.) or would they be able to use the new [try] for the
> vast majority of their code?
I think in this case this is a common idiom we want to actively
discourage! :-)
>>> Let's drop to Java-speak for a moment: the current practice of Tcl
>>> developers is to "catch (Exception e) { // handle }". In C++ it is
>>> "catch (...) { // handle }".
>>> Return code is not a mechanism for exception/error typing, it is a
>>> mechanism for implementing control flows. We need a typing
>>> mechanism.
>> Hmmm... error handling needs case-analysis not typing per se.
>> Clearly,
>> any mechanism used for constructing error cases is strongly
>> related to
>> the mechanism that is used for distinguishing them, which is why I'd
>> rather see these aspects separated/parameterised from the exception
>> handling, at least until it is clear what the best mechanism for
>> this is.
> I'm going to take a stab at identifying what the best mechanism is.
>
> Let's move back for a moment to what we're trying to achieve here.
>
> Functionality:
> a. Handle errors by class (errorCode)
> b. Handle success continuation (i.e. don't limit the control
> structure
> to only exceptional situations)
> c. Handle other (non-error) exceptions
> d. It is clear that (a), (b) and (c) are all specialisations of
> matching the Tcl return code (and possibly some other return
> information, like errorCode and result)
> e. Clean up at the end of an operation, regardless of errors/
> exceptions
>
> Look & Feel:
> a. Looks like if/then/else (traditional structured try/catch as in
> C++, Java, etc), OR
> b. Looks like switch (I don't actually know of another language that
> does this), OR
> c. Hybrid of (a) and (b): choose between success/error with an
> if/then/else like construct, then switch on the specific result/error
> (e.g. Erlang)
>
> Matching:
> a. Match a single return code, a range of return codes, or a
> discrete
> list of return codes
> b. Match a specific error code, and an errorcode pattern (glob-like)
> c. Possibly provide for more complex matches involving other fields
> (result), regular expressions, disjunctive matching, case sensitivity,
> relational calculus/algebra, whatever.
Ha! Maybe subsumption based matching using a description logic
reasoner? :-)
It seems clear that matching on a the return code (or a list of) can
be done in O(1). It seems we agree that this can and should be
supported. How much to support narrowing down beyond that is the main
focus of debate. I'm prepared to agree with you that some sort of
errorCode matching would be beneficial. Glob-matching errorCode is
reasonably cheap and probably does cover most common cases. Another
cheap alternative would be to treat the pattern as simply a list of
constants and then match by finding longest common prefix between the
matches and the errorcode, which would have worst case O(N) where N
is the llength of $errorCode. That also has the advantage that you
can compile all catch blocks into a single pattern matching tree
(trie-like). While customised pattern matching is appealing, it seems
likely to involve extra complexity in both usage and implementation,
with probably performance impacts too. User-supplied predicates or
customisable pattern matching is likely to be much less efficient, so
should probably not be the default.
>
> catch+something_else can provide for arbitrary complexity. Should
> [try]
> also do so, or should its complexity be limited? If limited, what
> is a
> reasonable limit? Exact match? Range match? Glob match?
>
> It strikes me that there are two possible solutions:
>
> (1) Make [try] an unholy union of [catch] and [switch] such that the
> user can specify the information being switched on (e.g. "%C,%E,%R"
> where %C is the return code, %E is the errorcode and %R is the
> result).
> You delegate pattern matching to [switch] and allow the user to match
> based on any combination of return code, errorcode and result.
>
> try {
> # ...
> } thenwith -glob -- "%C,%E" {
> "1,POSIX *" { handle posix errors }
> "3,*" -
> "4,*" { handle break/continue }
> }
I don't think it matters whether [try] delegates to [switch] in the
implementation. This still results in [try] acquiring the interface
of [switch].
>
> (2) Make [try] an unholy union of [catch] and [if]/then/else, and
> provide helper functions/operations to match exception/error cases
> with
> expr.
>
> try {
> # ...
> } handle { [string match "POSIX *" $::errorCode] } {
> handle posix errors
> } handle { code() in {3 4} } {
> handle continue
> }
>
> Asssuming some minor changes to expr that could look more like:
>
> try {
> # ...
> } handle { errorcode() like "POSIX *" } {
> } handle { code() in {3 4} } {
> }
>
> I found the question on Reddit at
> http://www.reddit.com/r/programming/comments/7dgwy/
> ask_proggit_where_clauses_in_catch_declarations/?sort=old
> quite interesting in this regard.
I briefly considered something along those lines -- general
predicates as guards. I think it's getting too complex though. As you
say, [try] doesn't need to handle all cases -- it just needs to
handle the most common cases decently. Just ignore me and go with
glob-matching or prefix matching. So long as non-error exceptions are
handled well, I'll be happy.
-- Neil
This message has been checked for viruses but the contents of an attachment
may still contain software viruses, which could damage your computer system:
you are advised to perform your own checks. Email communications with the
University of Nottingham may be monitored as permitted by UK legislation.
|
|
From: Twylite <tw...@cr...> - 2008-11-20 22:14:14
|
Hi,
> It's not a range - the "-" in switch means "use the same body as the
> next branch". i.e., the example means codes 0 and 4 (ok and continue),
> not 0 to 4.
Ah. My bad. A violation of rule #2: never engage in a debate when the
coffee has run out.
I had been thinking about catching ranges of return codes (e.g. all
user-defined codes as opposed to Tcl-reserved codes) and wasn't paying
enough attention to the code.
> OK. On reflection I'm willing to concede that adequate error case
> analysis requires pattern matching of some sort built-in to [try].
Yay :)
>> Of course its entirely possible that some bright spark declares that if
>> the general class of errorCode is "OBJECT" then [lindex $errorCode 1] is
>> an oo::object that is a child of tcl::errorobj, and you want to do
>> class-based matching on said object. This is probably quite a strong
>> argument against glob matching (as the only option).
> Good point. I hadn't thought of that. That also could handle
> subtype-based matching.
>> So I have been suggesting "string match" on a structured list as an
>> approximation of the 'isa' / 'typeof' operator.
> Which could be workable: the OO exception just dumps [$self info
> ancestors] or whatever into the errorCode, so you have something like:
> [list Error IOError HostUnreachable object12].
That certainly has the potential to work. Constructing the error object
(or calling [errorobj throw]) - however you want it to work - would do a
[return -options {...}] putting the object into the options dict, but
also [$self info ancestors] into -errorcode.
>> My point here applies most specifically to cases where you don't control
>> the API (which is rather common), but also to cases where you don't want
>> to modify the API - because it will affect other working code that you
>> don't want to refactor, or because you believe that adding a flow
>> control statement like 'break' or 'continue' outside of a flow control
>> construct is an inherently dangerous code practice because developers
>> using APIs don't expect stuff like that.
> Well, all exceptions affect flow control.
I humbly submit that while one can do immensely cool things with Tcl,
the average developer (indeed, a good number of well-above-average
developers) will adopt the WTF face when confronted with the idea that a
procedure can throw a continue. We live in the Victorianesque era of
structure code, and one simply does not abide by such vulgarity in
civilised society.
>>> proc connect {schemes host user pass} {
>>> foreach scheme $schemes {
>>> try {
>>> return [$scheme connect $host $user $pass]
>>> } on error BADAUTH {} { continue }
>>> }
>>> error "unable to authenticate"
>>> }
> I agree that errorcode is pretty unlikely to be useful outside of
> errors, hence making it optional. I still quite like the symmetry of
> this proposal.
The symmetry is attractive, but I can't help feeling that if we are
trying to shoehorn exception handling and error handling into the same
construct then a reasonable amount of asymmetry is expected. In fact a
clear distinction between handling errors and handling exceptions may
make the intent of the code more evident.
>> Matching:
>> c. Possibly provide for more complex matches involving other fields
>> (result), regular expressions, disjunctive matching, case sensitivity,
>> relational calculus/algebra, whatever.
> Ha! Maybe subsumption based matching using a description logic
> reasoner? :-)
.. strange ... I though Girl Genius was in the other tab.
> It seems clear that matching on a the return code (or a list of) can
> be done in O(1). It seems we agree that this can and should be
> supported. How much to support narrowing down beyond that is the main
> focus of debate. I'm prepared to agree with you that some sort of
> errorCode matching would be beneficial. Glob-matching errorCode is
> reasonably cheap and probably does cover most common cases. Another
> cheap alternative would be to treat the pattern as simply a list of
> constants and then match by finding longest common prefix between the
> matches and the errorcode, which would have worst case O(N) where N is
> the llength of $errorCode. That also has the advantage that you can
> compile all catch blocks into a single pattern matching tree
> (trie-like). While customised pattern matching is appealing, it seems
> likely to involve extra complexity in both usage and implementation,
> with probably performance impacts too. User-supplied predicates or
> customisable pattern matching is likely to be much less efficient, so
> should probably not be the default.
Mmm ... the performance vs flexibility trade-off. The compromise is
usually to allow the user to select which they need for the particular
case. This one deserves more thought, possibly at a less reasonable
hour (like in the morning).
>> try {
>> # ...
>> } thenwith -glob -- "%C,%E" {
>> "1,POSIX *" { handle posix errors }
>> "3,*" -
>> "4,*" { handle break/continue }
>> }
> I don't think it matters whether [try] delegates to [switch] in the
> implementation. This still results in [try] acquiring the interface of
> [switch].
True, but also the full functionality of switch, and any enhancements
made to switch. Matching is not limited to a particular approach that
I/we think is appropriate right now, but has the flexibility to address
a wide range of needs.
>> (2) Make [try] an unholy union of [catch] and [if]/then/else, and
>> provide helper functions/operations to match exception/error cases with
>> expr.
>> try {
>> # ...
>> } handle { [string match "POSIX *" $::errorCode] } {
>> handle posix errors
>> }
> I briefly considered something along those lines -- general predicates
> as guards. I think it's getting too complex though. As you say, [try]
> doesn't need to handle all cases -- it just needs to handle the most
> common cases decently.
Oh, to have a convenient and provably accurate definition of "common
cases" ;)
> Just ignore me and go with glob-matching or prefix matching. So long
> as non-error exceptions are handled well, I'll be happy.
I'd prefer not to -- your approach is often different to mine and it
pays to consider all views and comments when designing a feature like
this. I had already started breaking down the separate concerns, look &
feel and matching options before this particular thread started, and
noted that expr matching provides the more flexible and potentially most
consistent [try] command, but is somewhat ugly around the actual match.
I had been letting this stew for a bit (paid-work time pressures) but it
looks like decisions need to be taken soon. Still, I prefer to take a
good look at all angles of a problem before taking decisions that become
permanent.
Regards,
Twylite
|