1 | Design of Exceptions and EHM in Cforall:
|
---|
2 |
|
---|
3 | Currently this is a combination of ideas and big questions that still have to
|
---|
4 | be addressed. It also includes some other error handling options, how they
|
---|
5 | interact and compare to exceptions.
|
---|
6 |
|
---|
7 |
|
---|
8 | What is an Exception:
|
---|
9 |
|
---|
10 | In other words what do we throw? What is matched against, how does it carry
|
---|
11 | data with it? A very important question that has not been answered.
|
---|
12 |
|
---|
13 | Option 1: Strutures
|
---|
14 |
|
---|
15 | Considering the current state of Cforall the most natural form of the
|
---|
16 | exception would likely be a struture, implementing a trait repersenting the
|
---|
17 | minimum features of an exception. This has many advantages, including arbitray
|
---|
18 | fields, some polymorphism and it matches exceptations of many current systems.
|
---|
19 |
|
---|
20 | The main issue with this is matching, without OOP inheritance there is no
|
---|
21 | exception hierarchy. Meaning all handling has to happen on the exact exception
|
---|
22 | without the ease of grouping parents. There are several ways to attempt to
|
---|
23 | recover this.
|
---|
24 |
|
---|
25 | The first is with conditional matching (a check after the type has been
|
---|
26 | matched) which allows for matching particular values of a known type. However
|
---|
27 | this does not dynamically expand and requires an extra step (as opposed to
|
---|
28 | mearly allowing one). I would not recomend this as the primary method.
|
---|
29 |
|
---|
30 | Second is to try and use type casts/conversions to create an implicate
|
---|
31 | hierachy, so that a catch clause catches anything of the given type or
|
---|
32 | anything that converts to the given type.
|
---|
33 |
|
---|
34 | Plan9 (from what I know of it) would be a powerful tool here. Even with it,
|
---|
35 | creating a hierarchy of types at runtime might be too expencive. Esecially
|
---|
36 | since they are less likely to be tree like at that point.
|
---|
37 |
|
---|
38 | Option 2: Tags
|
---|
39 |
|
---|
40 | The other option is to introduce a new construct into the language. A tag
|
---|
41 | repersents a type of exception, it is not a structure or variable (or even
|
---|
42 | a normal type). It may be usable in some of those contexts.
|
---|
43 |
|
---|
44 | Tags can declare an existing tag as its parent. Tags can be caught by handlers
|
---|
45 | that catch their parents. (There is a single base_exception that all other
|
---|
46 | exceptions are children of eventually.) This allows for grouping of exceptions
|
---|
47 | that repersent similar errors.
|
---|
48 |
|
---|
49 | Tags should also have some assotiated data, where and on what did the error
|
---|
50 | occur. Keeping with the otherness of exception tags and allowing them to be
|
---|
51 | expanded, using a parameter list. Each exception can have a list of paramters
|
---|
52 | given to it on a throw. Each tag would have a declared list of parameters
|
---|
53 | (which could be treated more like a set of fields as well). Child tags must
|
---|
54 | use their parent's list as a prefix to their own, so that the parameters can
|
---|
55 | be accessed when the child tag is matched against the parent.
|
---|
56 |
|
---|
57 | Option N: ...
|
---|
58 |
|
---|
59 | This list is not complete.
|
---|
60 |
|
---|
61 |
|
---|
62 | Seperating Termination and Resumption:
|
---|
63 |
|
---|
64 | Differentating the types of exceptions based on exception would be hard with
|
---|
65 | exceptions as structures. It is possible with exceptions as tags by having
|
---|
66 | two base exceptions, one for each type of throw. However recompining them
|
---|
67 | to dual types would be harder.
|
---|
68 |
|
---|
69 | Reguardless, using different keywords would also be useful for clarity, even
|
---|
70 | if it does not add functality. Using the C++ like keywords would be a good
|
---|
71 | base. Resumption exceptions could use a different set (ex. raise->handle) or
|
---|
72 | use resume as a qualifier on the existing statements.
|
---|
73 |
|
---|
74 |
|
---|
75 | Conditional Matching:
|
---|
76 |
|
---|
77 | A possible useful feature, it allows for arbitrary checks on a catch block
|
---|
78 | instead of merely matching a type. However there are few use cases that
|
---|
79 | cannot be avoided with proper use of a type hierarchy, and this shrinks even
|
---|
80 | further with a good use of re-throws.
|
---|
81 |
|
---|
82 | Also it assumes one sweep, that might also be a problem. But might also give
|
---|
83 | it an advantage over re-throws.
|
---|
84 |
|
---|
85 |
|
---|
86 | Alternatives: Implicate Handlers & Return Unions
|
---|
87 |
|
---|
88 | Both act as a kind of local version of an exception. Implicate handlers act as
|
---|
89 | resumption exceptions and return unions like termination exceptions. By local
|
---|
90 | I mean they work well at one or two levels of calls, but do not cover N levels
|
---|
91 | as cleanly.
|
---|
92 |
|
---|
93 | Implicate handles require a growing number of function pointers (which should
|
---|
94 | not be used very often) to be passed to functions, creating and additional
|
---|
95 | preformance cost. Return unions have to be checked and handled at every level,
|
---|
96 | which has some preformance cost, but also can significantly clutter code.
|
---|
97 | Proper tools can help with the latter.
|
---|
98 |
|
---|
99 | However, they may work better at that local level as they do not require stack
|
---|
100 | walking or unwinding. In addition they are closer to regular control flow and
|
---|
101 | are easier to staticly check. So even if they can't replace exceptions
|
---|
102 | (directly) they may still be worth using together.
|
---|
103 |
|
---|
104 | For instance, consider the Python iterator interface. It uses a single
|
---|
105 | function, __next__, to access the next value and to signal the end of the
|
---|
106 | sequence. If a value is returned, it is the next value, if the StopIteration
|
---|
107 | exception is thrown the sequence has finished.
|
---|
108 |
|
---|
109 | However almost every use of an iterator will add a try-except block around the
|
---|
110 | call site (possibly through for or next) to catch and handle the exception
|
---|
111 | immediately, ignoring the advantages of more distant exception handling.
|
---|
112 |
|
---|
113 | In this case it may be cleaner to use a Maybe for both cases (as in Rust)
|
---|
114 | which gives similar results without having to jump to the exception handler.
|
---|
115 | This will likely handle the error case more efficiently and the success case a
|
---|
116 | bit less so.
|
---|
117 |
|
---|
118 | It also mixes the error and regular control flow, which can hurt readablity,
|
---|
119 | but very little if the handling is simple, for instance providing a default
|
---|
120 | value. Similarly, if the error (or alternate outcome) is common enough
|
---|
121 | encoding it in the function signature may be good commuication.
|
---|
122 |
|
---|
123 | In short, these errors seem to be more effective when errors are likely and
|
---|
124 | immediate. High failure operations, especially ones with failures that can
|
---|
125 | be handled locally, might be better off using these instead of exceptions.
|
---|
126 |
|
---|
127 | Also the implicate handlers and return unions could use exceptions as well.
|
---|
128 | For instance, a useful default might handler might be to throw an exception,
|
---|
129 | seaching up the stack for a solution if one is not locally provided.
|
---|
130 |
|
---|
131 | Or here is a possible helper for unpacking a Result value:
|
---|
132 | forall(otype T, otype E | exception(E))
|
---|
133 | T get_or_throw (Result(T, E) * this) {
|
---|
134 | if (is_success(this)) {
|
---|
135 | return get_success(this);
|
---|
136 | } else {
|
---|
137 | throw get_failure(this);
|
---|
138 | }
|
---|
139 | }
|
---|
140 | So they can feed off of each other.
|
---|