aboutsummaryrefslogtreecommitdiff
path: root/ltn013.md
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--ltn013.md191
1 files changed, 191 insertions, 0 deletions
diff --git a/ltn013.md b/ltn013.md
new file mode 100644
index 0000000..9c56805
--- /dev/null
+++ b/ltn013.md
@@ -0,0 +1,191 @@
1# Using finalized exceptions
2### or How to get rid of all those if statements
3by DiegoNehab
4
5
6## Abstract
7This little LTN describes a simple exception scheme that greatly simplifies error checking in Lua programs. All the needed functionality ships standard with Lua, but is hidden between the `assert` and `pcall` functions. To make it more evident, we stick to a convenient standard (you probably already use anyways) for Lua function return values, and define two very simple helper functions (either in C or in Lua itself).
8
9## Introduction
10
11Most Lua functions return `nil` in case of error, followed by a message describing the error. If you don't use this convention, you probably have good reasons. Hopefully, after reading on, you will realize your reasons are not good enough.
12
13If you are like me, you hate error checking. Most nice little code snippets that look beautiful when you first write them lose some of their charm when you add all that error checking code. Yet, error checking is as important as the rest of the code. How sad.
14
15Even if you stick to a return convention, any complex task involving several function calls makes error checking both boring and error-prone (do you see the "error" below?)
16```lua
17function task(arg1, arg2, ...)
18 local ret1, err = task1(arg1)
19 if not ret1 then
20 cleanup1()
21 return nil, error
22 end
23 local ret2, err = task2(arg2)
24 if not ret then
25 cleanup2()
26 return nil, error
27 end
28 ...
29end
30```
31
32The standard `assert` function provides an interesting alternative. To use it, simply nest every function call to be error checked with a call to `assert`. The `assert` function checks the value of its first argument. If it is `nil`, `assert` throws the second argument as an error message. Otherwise, `assert` lets all arguments through as if had not been there. The idea greatly simplifies error checking:
33```lua
34function task(arg1, arg2, ...)
35 local ret1 = assert(task1(arg1))
36 local ret2 = assert(task2(arg2))
37 ...
38end
39```
40
41If any task fails, the execution is aborted by `assert` and the error message is displayed to the user as the cause of the problem. If no error happens, the task completes as before. There isn't a single `if` statement and this is great. However, there are some problems with the idea.
42
43First, the topmost `task` function doesn't respect the protocol followed by the lower-level tasks: It raises an error instead of returning `nil` followed by the error messages. Here is where the standard `pcall` comes in handy.
44```lua
45function xtask(arg1, arg2, ...)
46 local ret1 = assert(task1(arg1))
47 local ret2 = assert(task2(arg2))
48 ...
49end
50
51function task(arg1, arg2, ...)
52 local ok, ret_or_err = pcall(xtask, arg1, arg2, ...)
53 if ok then return ret_or_err
54 else return nil, ret_or_err end
55end
56```
57
58Our new `task` function is well behaved. `Pcall` catches any error raised by the calls to `assert` and returns it after the status code. That way, errors don't get propagated to the user of the high level `task` function.
59
60These are the main ideas for our exception scheme, but there are still a few glitches to fix:
61
62* Directly using `pcall` ruined the simplicity of the code;
63* What happened to the cleanup function calls? What if we have to, say, close a file?
64* `Assert` messes with the error message before raising the error (it adds line number information).
65
66Fortunately, all these problems are very easy to solve and that's what we do in the following sections.
67
68## Introducing the `protect` factory
69
70We used the `pcall` function to shield the user from errors that could be raised by the underlying implementation. Instead of directly using `pcall` (and thus duplicating code) every time we prefer a factory that does the same job:
71```lua
72local function pack(ok, ...)
73 return ok, {...}
74end
75
76function protect(f)
77 return function(...)
78 local ok, ret = pack(pcall(f, ...))
79 if ok then return unpack(ret)
80 else return nil, ret[1] end
81 end
82end
83```
84
85The `protect` factory receives a function that might raise exceptions and returns a function that respects our return value convention. Now we can rewrite the top-level `task` function in a much cleaner way:
86```lua
87task = protect(function(arg1, arg2, ...)
88 local ret1 = assert(task1(arg1))
89 local ret2 = assert(task2(arg2))
90 ...
91end)
92```
93
94The Lua implementation of the `protect` factory suffers with the creation of tables to hold multiple arguments and return values. It is possible (and easy) to implement the same function in C, without any table creation.
95```c
96static int safecall(lua_State *L) {
97 lua_pushvalue(L, lua_upvalueindex(1));
98 lua_insert(L, 1);
99 if (lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0) != 0) {
100 lua_pushnil(L);
101 lua_insert(L, 1);
102 return 2;
103 } else return lua_gettop(L);
104}
105
106static int protect(lua_State *L) {
107 lua_pushcclosure(L, safecall, 1);
108 return 1;
109}
110```
111
112## The `newtry` factory
113
114Let's solve the two remaining issues with a single shot and use a concrete example to illustrate the proposed solution. Suppose you want to write a function to download an HTTP document. You have to connect, send the request and read the reply. Each of these tasks can fail, but if something goes wrong after you connected, you have to close the connection before returning the error message.
115```lua
116get = protect(function(host, path)
117 local c
118 -- create a try function with a finalizer to close the socket
119 local try = newtry(function()
120 if c then c:close() end
121 end)
122 -- connect and send request
123 c = try(connect(host, 80))
124 try(c:send("GET " .. path .. " HTTP/1.0\r\n\r\n"))
125 -- get headers
126 local h = {}
127 while 1 do
128 l = try(c:receive())
129 if l == "" then break end
130 table.insert(h, l)
131 end
132 -- get body
133 local b = try(c:receive("*a"))
134 c:close()
135 return b, h
136end)
137```
138
139The `newtry` factory returns a function that works just like `assert`. The differences are that the `try` function doesn't mess with the error message and it calls an optional "finalizer" before raising the error. In our example, the finalizer simply closes the socket.
140
141Even with a simple example like this, we see that the finalized exceptions simplified our life. Let's see what we gain in general, not just in this example:
142
143* We don't need to declare dummy variables to hold error messages in case any ever shows up;
144* We avoid using a variable to hold something that could either be a return value or an error message;
145* We didn't have to use several "if" statements to check for errors;
146* If an error happens, we know our finalizer is going to be invoked automatically;
147* Exceptions get propagated, so we don't repeat these "if" statements until the error reaches the user.
148
149Try writing the same function without the tricks we used above and you will see that the code gets ugly. Longer sequences of operations with error checking would get even uglier. So let's implement the `newtry` function in Lua:
150```lua
151function newtry(f)
152 return function(...)
153 if not arg[1] then
154 if f then f() end
155 error(arg[2], 0)
156 else
157 return ...
158 end
159 end
160end
161```
162
163Again, the implementation suffers from the creation of tables at each function call, so we prefer the C version:
164```lua
165static int finalize(lua_State *L) {
166 if (!lua_toboolean(L, 1)) {
167 lua_pushvalue(L, lua_upvalueindex(1));
168 lua_pcall(L, 0, 0, 0);
169 lua_settop(L, 2);
170 lua_error(L);
171 return 0;
172 } else return lua_gettop(L);
173}
174
175static int do_nothing(lua_State *L) {
176 (void) L;
177 return 0;
178}
179
180static int newtry(lua_State *L) {
181 lua_settop(L, 1);
182 if (lua_isnil(L, 1))
183 lua_pushcfunction(L, do_nothing);
184 lua_pushcclosure(L, finalize, 1);
185 return 1;
186}
187```
188
189## Final considerations
190
191The `protect` and `newtry` functions saved a "lot" of work in the implementation of [LuaSocket](https://github.com/lunarmodules/luasocket). The size of some modules was cut in half by the these ideas. It's true the scheme is not as generic as the exception mechanism of programming languages like C++ or Java, but the power/simplicity ratio is favorable and I hope it serves you as well as it served [LuaSocket](https://github.com/lunarmodules/luasocket).