This article can be seen as a very good explanation of why exceptions are a valuable feature for a language. In python, mentioning one of the languages exposed in the article, I would return the created object and throw an exception on error.
This has the same properties as B in his examples though - one can simply fail to catch the exception and the same problems arise. I actually much prefer the B solution (or a variation, where instead of returning error codes, you pass in an exception handler function), as opposed to exceptions which are "hidden" from the API and rely on users to read documentation (hint: most people don't, until they find out the hard way).
It is different. An excepction decouples the place where errors must be handled from where the required object will be used. A closer example would be if the function received a function pointer or, even better, a continuation to be invoked if an error ocurrs.
I'm personally fond of filling an error buffer, but a bit more explicitly than just an int.
typedef struct APIError {
int code;
char *description;
};
/**
* Returns NULL and fills the error parameter if creation was unsuccessful.
*/
Something APICreateSomething(bool param1, int param2, APIError *error);
I think something that he has missed is the design of passing functions as params for success and failures.
Of course these are generally used when there are external API calls which are async, but nowadays those kind of API calls are quite common.
void createSomething(bool param1,int param2, Something something, success: function (something) {}, error: function (error){});
Of course this would be in those languages that support passing of functions as params.
One of the unmentioned advantages of style "A", passing in a pointer to where you want the result, is that the caller can trivially switch allocators if they need that speed (e.g. a specialized slab allocator is much faster than malloc). If you want to support this with the other styles, you end up adding a parameter (or generic parameter) for the allocator.
Style "E" is also known as an error monad, and has a lot of advantages when writing functional-style code. For example, you can use the usual List.map on a function that returns an error monad but not on one that has out parameters or throws exceptions.
I'd argue that adding the parameter for the allocator is almost always what you want to do anyway - the idea of the caller allocating the memory to be filled in by the constructor completely violates the idea of encapsulation - and it basically assumes that the memory required by the object is contiguous, of known size, or that the caller knows the exact layout - usually not the case.
For example, if I gave you the following API and expected you to use the constructor (requiring pre-allocated list), how would you begin?
struct list_t;
int list_new(list_t*);
int list_add(list_t*, Object);
int list_free(list_t*);
You have no idea whether I've implemented a linked list, an array, vector, vlist, or anything else - you simply don't know how to allocate the required memory.
A constructor taking a pointer to an already allocated object can serve one useful function though - it can be the equivalent of a copy constructor in plain C. You still need the second argument or return value for the new object though, so the above API example is unfit for purpose.
On the "error monad", I wish you wouldn't call it this, because it's potentially scaring away people who don't understand what a monad is, or think it is something complex. Error (or Either) in Haskell has nothing inherently to do with monads - it's just a data type with two constructors, one to construct successful values, and another to construct errors. The example you gave, of using "map" has nothing to do with a monad either, but is just a plain Functor for the Error type. Sure, there exist an instance of Monad for Error/Either, and they're extremely useful, but that's not all there is - it shouldn't be defined as "error monad".
The real value of the Error/Either type comes from the fact that it's a tagged union, where only one of the constructors can be inhabited at any time, and to find out which, you must pattern match over the possibilities, with the compiler warning you if you don't do so exhaustively. Functional languages also don't allow you to simply ignore result of a "function call".
Attempts to simulate this like style E in the blog post fall short, because one can simply ignore the result or the "errorCode" and attempt to use the object instead - and the compiler won't complain. It's not that these languages don't have monads or functors - it's that they don't have the unit type, tagged unions and (exhaustive) pattern matching. Monad and functor instances for Error are bonuses once these are in place.
I checked out the LLVM source [0] and what they do is slightly different (their is a tagged union) since their HasError is not part of the union which makes more sense since it does not rely on memory alignment. It does not really matter since it seems that the values in the struct are aligned appropriately anyway but it'd still be pretty wonky if there was just the union.
I really enjoyed this article. I think I personally prefer the struct return. It's explicit, if maybe a tiny bit wasteful of memory. Certainly there will be people whom this affects, but I think its good overall.