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