9 ms·
Erlang and code style
- rubiquity 12y agoWoah, this article is full of awesome bits. Coding towards a single flow path and crashing whenever you go off that path has been the hardest and most enjoyable adjustment I've made as I've been learning the Erlang VM (through Elixir). It's definitely weird at first, but very refreshing when you get to leave out all those if/elses and exception handling. I'm still quite bad at Erlang/Elixir, but I'm enjoying it a lot more.
- dozzie 12y agoI find myself writing libraries (like xmerlrpc for XML-RPC). The rule of "just let it die" doesn't apply too much in such code. You typically don't want to decide to die in library code. You want to postpone the decision until the actual application/service is written. It's easy to convert error reported by returned value to error reported by exception: just cause badmatch with guard. But to convert an exception to a value is more troublesome. You need to use try..catch. To properly catch errors in library, one could either use global try..catch hidden in the library code or be much more precise on errors and carefully intercept all the errors that could happen. The latter approach gives an opportunity to give very specific error messages and allows hypothetical bugs in code used by the library to bubble up instead of being disguised as "invalid argument". As the article says, there are cases when rule of a thumb needs to be broken. Writing a library is such case.
- jacquesm 12y agoThe idiomatic erlang way to deal with such problems is to generate an exception so that the caller can deal with the problem at the next level up that is set up to handle trouble. This could be the level immediately above the library, but conceivably it is much higher up, or the (erlang)process may in fact terminate due to an uncaught exception. The decision to bail or try to recover should be made at the lowest level capable of making that decision and libraries are simply not empowered to make that decision. You also don't want to force people to catch exceptions around every call to every function in the library. See section 3.5: http://www.erlang.org/download/armstrong_thesis_2003.pdf http://www.erlang.org/download/armstrong_thesis_2003.pdf
- lostcolony 12y agoIs it? A lot of the library code I've seen in Erlang (especially in the standard library) is to avoid exceptions, and instead return either {ok, Val}, or {error, Reason}. Edit: To clarify tone, I actually would like to know, because I've run into this exact dilemma before. Per the original poster, it seems bad form to throw an exception that has to be handled somewhere in the calling process if I want to be able to send that data to another process. That is, process A makes a library call, cares nothing about the return, just wants to pass it to process B. If it's an error tuple, it can just do that. If it's an exception, it has to explicitly handle it, wrap it in an error tuple, and pass ~that~ along instead, which seems inelegant from library code. It also prevents the library from being able to declare what it returns in the event of a problem via a Dialyzer spec; an error tuple you can fully enumerate what sorts of errors you can return.
- rdtsc 12y agoSometimes Erlang standard library code is not the prototypical Erlang development code. For example file_open() is a library function returning an error tuple since it is one of the obvious results file_open() will return. Now in your code it depends on what you expect or what your guarantees are. If this is a library that expects to open a config file to that is always there, well then a match is better and if it doesn't work an exception is thrown, report written and maybe your process gets restarted. But now imagine your are writing a configuration file parser. Now you code acts like a library so the input file not being there is a common case and maybe code above needs to decide if it should blow up or not on it. Anyway that is my amateurish understanding of it. Maybe someone who knows more can chime in and correct me.
- e12e 12y ago> This could be the level immediately above the library Or in the library, surely. I think you sort of imply this with: > You also don't want to force people to catch exceptions around every call to every function in the library. ?
- plainOldText 12y agoOn a tangent line, one thing I dislike about Go is the error handling. I find it really annoying to have to check for errors at every single step. Erlang has probably the most elegant error handling mechanism. Just let if fail, and then restart the whole process/subsystem.
- thezilch 12y agoCertainly you can do this with Go by letting it panic and having your preferred supervisor (eg. supervisord) supervise the process?
- jacquesm 12y agoYes, but that kills all the other go-routines in that process as well. In Erlang that would just affect the one thread.
- lgas 12y agoYou could build an erlang/OTP style supervision tree using recover at the top level to restart panic'd goroutines.
- jacquesm 12y agoYou're going to have a hard time maintaining consistent state but if you manage your bookkeeping very well that might work in a long lived process. If not you might end up leaking resources. Go routines and erlang processes are not mapping 1:1 with each other and I (maybe naively) assume that resources such as file descriptors and other subtle state modifications could easily survive a naive implementation of such a scheme causing eventual resource depletion. Erlang is bulletproof in this respect.
- lgas 12y agoThat is true, and I suppose the fact that third party go libraries won't be written with this philosophy in mind means that you would have to audit all third party library use pretty carefully, which would make using this approach on projects of significant size untenable.
- thrillscience 12y agoAnother great Erlang essay by jlouis. The Erlang practice of letting things fail is also SOP for experienced implementors of any large SOA system. You just let things fail. If some process getting messages from a queue can't talk to the Database, don't retry; just exit and let the supervisor deal with it. Erlang is great for writing robust software because this type of error handling is a first-class feature of the language and runtime.
- thinkpad20 12y agoExcellent writing; it really makes me want to pick up some Erlang at some point. It's also giving me some interesting ideas in regards to the language I'm currently working on. I'll also echo a sentiment brought up by another poster which is that the Go style of error handling seems really unattractive. I'm not sure I'm totally on-board with the Erlang style of "just let it die" (although it seems to work great for Erlang), but being forced to deal with exceptions right away in all cases seems like it would clutter your code and cause a lot of headache. I'm also curious as to what happens if you neglect to handle the error? If you wrote something like file, err := os.Open("file.go") return file.Read() What would happen if an error occurred? Would `file` be nil? If so, wouldn't that easily propagate up, even to functions which, on their surface, shouldn't be expected to fail?
- lostcolony 12y agoIs this a question on Go's error handling (which I know nothing about), or Erlang's (which I am moderately proficient in)?
- a_bonobo 12y agoFirst of all, that wouldn't work in go, it would complain that you don't use the err variable, you could change it to: file, _ := os.Open("file.go") The underscore tells the compiler to ignore the error. And yes, file is <nil> now! As a sidenote, it would be more idiomatic to do this: if _, err := os.Open("file.go"); err != nil { //handle error } // Do stuff with file This has bitten me before - if you use a non-existent key on a map of strings (or dictionary, or hash, however you call it) you get "" and an error back. my_hash := make(map[string]string) value, ok := my_hash["no_key"] value is "", ok is 'false'. What type you get back exactly from a bad request depends on the type of call, but you always get an error back! Maps storing ints return 0, maps storing strings return "", etc. Go always forces you to look at each error, since errors are always different.
- jamwt 12y agofile, err := os.Open("file1") // complains if you don't check err file2, err := os.Open("file2") // no problem, no need to check err.
- rdtsc 12y agoGreat work Loius! These essays should become a little book some day. > This is called intentional programming. I am stealing that phrase. Often when trying to explain the coding style in Erlang, I kind of describe what you don't do "don't handle error" in band, but this makes it into a positive prescription -- do what the intent of the code is. And note that these things are very easy and straight forward in Erlang. Erlang is one of the few language run-time built with this in mind. Fault tolerance was at the top of the todo list. That is what makes it stand out of the crowd. But if you want, you can still copy this pattern in our system. For example in Python use a green thread and a queue to emulate an actor. If an exception is thrown (and you don't use linked custom C modules, which will screw you over in Erlang as well!), signal a supervisor thread and let it restart the original thread. You can apply this to a large system. Use OS processes. This is the good 'ol Unix way. Build watch dogs that watch your processes and restart them on failure. But now you'll be building also the messaging system. So there is some work you must do. But you can if you want to. You know how they say "learn functional programming because it will help you program better". Well the same can be said about Actor and Fault Tolerant programming. Learn it because it will help you program better. Even if you don't end up using Erlang.
- jlouis 12y agoI stole the "intentional" part from Joe Armstrong in turn :)
- loxs 12y agoI have been writing Erlang professionally for 3 years now. Your writings have helped me immensely. Thanks!
- deleted 12y ago[deleted]
- mcguire 12y agoYou, the author, and Joe Armstrong may confuse some of us. I know "intentional programming" from Charles Simonyi[1] and I suspect the two uses are completely unrelated. On the other hand, this use is more useful and in my opinion, better. [1] http://research.microsoft.com/apps/pubs/default.aspx?id=69540 http://research.microsoft.com/apps/pubs/default.aspx?id=6954...
- _mhr_ 12y agoHow does Erlang's error mechanism compare to Lisp's condition/restart system (this question from someone who knows very little about both)?
- ketralnis 12y ago> this question from someone who knows very little about both Then I'm not sure anyone can answer this adequately without also explaining both. But the shortest answer is that they're totally unlike each other in almost every way.
- davidw 12y agoThese are excellent points, and I have passed them along to my colleagues. There's something that a lot of these essays and tutorials about Erlang kind of gloss over that I'd like to see, though: Ok, I let it crash. Now what? So many of them seem to stop here and think that everything is great. It isn't: if I have some kind of long-running program, like, say, a web site, I should probably do more than just happily let everything crash as the error propagates its way up the call chain (no, it doesn't do that immediately, but as each thing starts and fails again enough times, it does propagate). For instance, displaying something human readable on a UI, or sending email or something other than logging a difficult to read Erlang error. More examples of what real world programs do after the crash, please!
- exo762 12y agoCrashing in Erlang is a strategy that lets you handle unexpected errors. In your web server example it corresponds to HTTP 500. Other problems (e.g. 404) you need to handle yourself, not relying on crashing.
- davidw 12y agoOk, right, but that means that something is catching that crash and returning a 500, and not just crashing the entire web server. I'd like to see more people delve into that aspect of Erlang architecture. It's covered some here: http://jlouisramblings.blogspot.it/2010/11/on-erlang-state-and-crashes.html http://jlouisramblings.blogspot.it/2010/11/on-erlang-state-a... (by the same author, jlouis!) and here: http://learnyousomeerlang.com/building-applications-with-otp http://learnyousomeerlang.com/building-applications-with-otp with the term "error kernel", but not a lot of space is dedicated to practical examples, compared to the amount of writing dedicated to how great it is to let things crash.
- loxs 12y agoIn fact, it is actually crashing the whole "web server". As the whole "web server" in Erlang is an Erlang process. And you can have millions of them. Every single request is handled by its own process "web server" spawned on demand for that request. And it's still reasonably fast.
- mrcwinn 12y agoGreat job on the write-up! I have precisely no experience with Erlang but I always enjoy a good programming article. I won't speak to Erlang's philosophies or paradigms, but I would like to respond to the bit about Go's handling of errors. I recall Rob Pike making an argument that errors are not, or should not be, special things, and so Go doesn't treat them as such. If you don't care about an error and don't want to deal with it, simply ignore it: image, _ = jpeg.Decode(r) If you care about an error and need to do something to handle it (a log, a panic, or something more complicated), you can signal that to the compiler. One advantage of this is you still have access to the returned bit of data, however mangled or incomplete it _might_ be, which can be useful in certain instances. I'm not sure I consider Go errors "silly" unless all you're doing is a panic every few lines. In actual practice, though, I have found I can pick and choose when I care about an error, and if I do care about it, there's an action specific to that error I want to perform. Hardly silly - cumbersome perhaps. As for your statement, "On the Go side, it mostly sticks errors in your face and makes damn sure you do something sensible with them ... " I reject the notion that in Go you are forced to deal with errors, but I also reject the notion you shouldn't do something sensible when you are dealing with them. :) Great work all the same! I come in peace.
- jcizzle 12y agoGreat article, very much appreciate this guy and his blog as I continue to learn Erlang. A question: the pattern of let it crash makes sense to me. However, I struggle with it when implementing RESTful web services. Letting the process crash will typically yield a 500 - the monitor on the connection process in the web server library ensures that. Clearly though, a status code and some additional information is a more appropriate response to the client. An example is returning a 400 for "missing arguments". In a contrived example of idiomatic Erlang, I feel like I'd write this: handle_request(Request) -> % username and password are required {ok, Username} = request:get_argument(<<"username">>, Request), {ok, Password} = request:get_argument(<<"password">>, Request), UserRecord = db:find(my_database, users, #{<<"username">> => Username}), Password = maps:get(<<"password">>, UserRecord), request:respond(200, UserRecord, Request). And if the key username did not exist in Request, request:get_argument/2 would return undefined or {error, Reason}, giving me a bad match error. Or if the password didn't match, I'd get the same. In order to intercept that and return a reasonable status code, I would have to catch that in handle_request. My question, then, is am I missing a best practice on how to handle this? Or is this just the place where I do need to catch errors and process them? And if that is so, isn't it at odds with the whole concept of writing intentional code?
- iamartnez 12y agoOr is this just the place where I do need to catch errors and process them? And if that is so, isn't it at odds with the whole concept of writing intentional code? No it's not. The author touches on this: Note the word intentional. In some cases, we do expect calls to fail. So we just handle it like everyone else would, but since we can emulate sum-types in Erlang, we can do better than languages with no concept of a sum-type. So handle errors you expect explicitly and let the rest crash. case user:authenticate(Username, Password) of {ok, UserRecord} -> ...; {error, notfound} -> ...; {error, badpass} -> ... end,
- DogeDogeDoge 12y agoThink about the client point of view. It is much better to return "401 Unauthorized" or some other reason rather than 500 . i would do a case on db:find and proceed to check password if you get user or fail with 401 and same with password case and if its correct go on and if not 401 :)
- superduper33 12y agoCareface.