[erlang-questions] Must and May convention
Loïc Hoguin
essen@REDACTED
Wed Sep 27 12:46:19 CEST 2017
On 09/27/2017 11:08 AM, Joe Armstrong wrote:
> For several years I've been using a convention in my hobby
> projects. It's what I call the must-may convention.
>
> I'm wondering if it should be widely used.
>
> What is it?
>
> There are two commonly used conventions for handling bad arguments to
> a function. We can return {ok, Val} or {error, Reason} or we can
> return a value if the arguments are correct, and raise an exception
> otherwise.
>
> The problem is that when I read code and see a function like
> 'foo:bar(a,12)' I have no idea if it obeys one of these conventions or
> does something completely different. I have to read the code to find
> out.
>
> My convention is to prefix the function name with 'must_' or 'may_'
I've been debating this in my head for a long time. I came to the
conclusion that 99% of the time I do not want to handle errors.
Therefore 99% of the functions should not return an error.
What happens for the 1% of the time where I do want to handle an error
and the function doesn't allow it? Well, I catch the exception. And
that's why I started using more meaningful exceptions for these cases.
For example, Cowboy 2.0 has the following kind of code when it fails to
validate input:
try
cow_qs:parse_qs(Qs)
catch _:_ →
erlang:raise(exit, {request_error, qs,
'Malformed query string; application/x-www-form-urlencoded
expected.'
}, erlang:get_stacktrace())
end.
99% of the time I don't care about it because Cowboy will properly
notice it's an input error and will return a 400 automatically (instead
of 500 for other crashes). It still contains the full details of the
error should I wish to debug it, and if it is necessary to provide more
details to the user I can catch it and do something with it.
(The exception probably won't make it as a documented feature in 2.0 due
to lack of time but I will rectify this in future releases.)
This strategy also helps with writing clearer code because I don't need
to have nested case statements, I can just have one try/catch with
multiple catch clauses to identify the errors I do want to catch, and
let the others go through.
try
Qs = cowboy_req:parse_qs(Req),
Cookies = cowboy_req:parse_cookies(Req),
doit(Qs, Cookies)
catch
exit:{request_error, qs, _} ->
bad_qs(Req);
exit:{request_error, {header, <<"cookie">>}, _} ->
bad_cookie(Req)
end
Write for the happy path and handle all errors I care about in the same
place. Goodbye nested cases for error handling!
I have also been using exceptions in a "different" way for parsing
Asciidoc files. Asciidoc input is *always* correct, there can not be a
malformed Asciidoc file (as far as parsing is concerned). When input
looks wrong it's a paragraph.
I can therefore in that case simply write functions for parsing each
possible elements, and try them one by one on the input until I find a
parsing function that doesn't crash. If it doesn't crash, then that
means I found the type of block for the input. If it crashes, I try the
next type of block.
So I have a function like this defining the block types:
block(St) →
skip(fun empty_line/1, St),
oneof([
fun eof/1,
%% Section titles.
fun section_title/1,
fun long_section_title/1,
%% Block macros.
fun block_id/1,
fun comment_line/1,
fun block_macro/1,
%% Lists.
fun bulleted_list/1,
fun numbered_list/1,
...
And then one of those parse functions would be like this for example:
comment_line(St) →
«"//", C, Comment0/bits» = read_line(St),
true = ?IS_WS(C),
Comment = trim(Comment0),
%% Good!
{comment_line, #{}, Comment, ann(St)}.
If it crashes, then it's not a comment line!
The oneof function is of course defined like this:
oneof([], St) →
throw({error, St}); %% @todo
oneof([Parse|Tail], St=#state{reader=ReaderPid}) →
Ln = asciideck_line_reader:get_position(ReaderPid),
try
Parse(St)
catch _:_ →
asciideck_line_reader:set_position(ReaderPid, Ln),
oneof(Tail, St)
end.
This allows me to do some parsec-like parsing by abusing exceptions. But
the great thing about it is that I don't need to worry about error
handling here again, I just try calling parse functions until one
doesn't crash.
So to go back to the topic at hand, I would say forget about the
distinction between must and may, and truly embrace "happy path"
programming and make smart use of exceptions. Deal with errors in one
place instead of having nested cases/many functions. There are of course
other ways to do this, but only exceptions let you do this both in the
local process and in a separate process, depending on your needs.
(I will now expect horrified replies from purists. Do not disappoint.)
Cheers,
--
Loïc Hoguin
https://ninenines.eu
More information about the erlang-questions
mailing list