11 ms·
Don't return null; use a tail call
- mmariani 15y agoIoC is a very useful concept for any OO language. For instance, ObjC uses it a lot through protocols. More info on that subject can be found here: http://martinfowler.com/bliki/InversionOfControl.html http://martinfowler.com/bliki/InversionOfControl.html The above link has some very interesting links about OO design and SmallTalk right at the end.
- gurraman 15y agoCommon idiom in Erlang. Using message passing (untested code): do_something(N) -> Pid = self(), spawn(fun() -> find_x(Pid, N) end), receive {found, Item} -> ...; not_found -> ... end. find_x(Pid, N) -> find_x (Pid, N, [1, 3, 5]). find_x(Pid, _N, []) -> Pid ! not_found; find_x(Pid, N, [N|_T]) -> Pid ! {found, N}; find_x(Pid, N, [_H|T]) -> find_x(Pid, N, T). You can also handle timeouts and stuff.
- mononcqc 15y agoUnless you specifically need a timer, I wouldn't use that and would simply return {ok, N} or {error, not_found}; that would avoid useless copying and message passing. Something like: find_x(N) -> find_x (N, [1, 3, 5]). find_x(N, []) -> {error, not_found}; find_x(N, [N|_T]) -> {ok, N}; find_x(N, [_H|T]) -> find_x(N, T). If what you want is to get out of deep recursion and the above doesn't work especially well, that's what 'throw/1' is for: Res = try find_x(N) of Val -> {ok, Val} catch throw:not_found -> {error, not_found} end. This will allow to avoid the stack, special cases for returning values and give you the code result you need. The spawning of a process for a simple use case like this is only worth it if you need a timer to give a maximum execution time for a given request. Otherwise, it's not very useful, clear or efficient. I'd mark it as 'concurrency for the sake of it'.
- wlievens 15y agoCommon idiom in javascript/jquery too, where the callback is called after the asynchronous execution. That's the advantage of this technique of course: synchronous or asynchronous implementations are interchangeable.
- leppie 15y agoFunny article. He mentions 'tail call elimination' once, and then never mentions 'tail call' again. Also a 'tail call' and 'tail call elimination' are 2 completely different things. The former being an implementation detail (to provide proper tail recursion for languages that require it, ie Scheme), the latter an optimization technique (to remove (expensive) tail calls!).
- jonmrodriguez 15y agoNonetheless, the term "tail call" should apply to what he is talking about. If "tail call recursion" means recursing as the last thing you do before returning, then he is saying to send a message as the last thing you do. AKA send a message as a tail call instead of returning the value of that message.
- gnosis 15y ago"a 'tail call' and 'tail call elimination' are 2 completely different things." Indeed. For those seeking more detailed information about this issue, see: http://c2.com/cgi/wiki?ProperTailRecursion http://c2.com/cgi/wiki?ProperTailRecursion http://c2.com/cgi/wiki?TailCallOptimization http://c2.com/cgi/wiki?TailCallOptimization http://lambda-the-ultimate.org/classic/message1532.html http://lambda-the-ultimate.org/classic/message1532.html
- dhx 15y agoRemember to keep track of the call stack in your head as you write code. The trap to avoid is to invert control and then fail to invert (return) back to the main program flow. In the example referenced, the following call stack could be created: 0. authenticateUser 1. login 2. authSucceeded If authSucceeded continues to call other functions as if it were the new main flow of control you could end up with a very deep call stack (it's not uncommon to see software with a call stack 40 calls deep). This is why I would have preferred "authSucceeded" to be renamed as "registerUserSession" or something else that screams "this function will do a well defined bit of work and _return quickly_" rather than "this is a loosely defined event callback that may do anything".
- Encryptor 15y agonot necessarily. you could implement these as function calls with no return addresses (think goto-s or JMP instructions). If you never need to return from some of those functions, there's no need to keep an entry in the call stack for them.
- chalst 15y agoIn the context of the OA's first sentence ("Why should an object-oriented programmer care about tail-call elimination?"), I think the author is thinking more of Scala and Ruby than Java, and there's no expectation that the tail call will return at all.
- batterseapower 15y agoAKA double-barrelled continuation passing style
- wickedchicken 15y agoWhich is annoying as all hell to write directly. I know it's a cliche to bitch about how 'ugh, language X doesn't have feature Y' but many callback-spaghettisms of Node would go away with call/cc.
- dustingetz 15y agoif anyone's curious, i wrote a presentation about how call/cc can fix callback-hell per parent. slides: lets make a blocking API not block. wait, what? (a practical introduction to continuations) [https://docs.google.com/present/view?id=dv9r8sv_82cn7dngcq https://docs.google.com/present/view?id=dv9r8sv_82cn7dngcq]
- antihero 15y ago" In his post, the caller passes itself as an argument (using a suitable interface type, of course), and the callee responds by invoking either a “found” or “not found” method on the caller." Isn't this pretty much what you do in Twisted already?
- andrewcooke 15y agothe difference is that twisted uses trampolining - there's a top level scheduler that manages everything. if twisted worked exactly as described in this article then you'd run out of stack space (python doesn't eliminate tail calls) (also, of course, the trampoline allows other work to happen while processes yield, which is what makes twisted useful). from another pov: this article describes how the designers of twisted want you to think (ie continuation passing style), but the actual implementation is slightly different. http://en.wikipedia.org/wiki/Trampoline_(computing)#High_Level_Programming http://en.wikipedia.org/wiki/Trampoline_(computing)#High_Lev... http://en.wikipedia.org/wiki/Continuation-passing_style http://en.wikipedia.org/wiki/Continuation-passing_style
- ufo 15y agoThe important thing here is not that passing onOK and onError callbacks is a good idea. The real issue is that NULL pointers are the most dangerous thing invented in the 20th century and should be avoided like the plague. From an [expression problem](http://c2.com/cgi/wiki?ExpressionProblem http://c2.com/cgi/wiki?ExpressionProblem) point of view using a discriminated union (in the same spirit as returning null but safer, since you can you peek at the value after doing a null check) would probably be just fine anyway. You still get to avoid the danger of NULL pointers but you still get a real value to work with (unlike the callback case that allways immediately splits it) so you can store it, pass it around, etc. By the way, astute readers might have recognized all that callback passing as just the [visitor pattern](https://en.wikipedia.org/wiki/Visitor_pattern https://en.wikipedia.org/wiki/Visitor_pattern) in disguise.
- VMG 15y ago> The real issue is that NULL pointers are the most dangerous thing invented in the 20th century Not sure if serious
- ufo 15y agoI am somewhat serious about this. Tony Hoare even called them his "Billion Dollar mistake" in a talk he gave a while ago (http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare http://www.infoq.com/presentations/Null-References-The-Billi...)
- barrybe 15y agoThat talk gets quoted pretty often, but it's a bit silly for Hoare to take all the blame/credit. Null references were discovered, not invented. They are so easy (from the point of view of someone implementing a non-functional language) that they were inevitable.
- lubutu 15y agoUsing this approach you would often also need to maintain state about what was requested, using closures or similar to avoid race conditions. Alternatively, Haskell offers the Maybe type, where Nothing means "not found" and Just x means "found x". Using a callback seems more prone to disaster than just encoding the possibility of null into the type system.
- troygoode 15y agoC# offers this via Nullable<T>: Nullable<int> someNumber = foo.TryAndGetSomeNumber(); if(someNumber && someNumber.HasValue) doSomething( someNumber.Value );
- arethuza 15y agoWhy don't you use int? rather than Nullable<int> - aren't they equivalent?
- lallysingh 15y agoThe point is, all values of the int are available. Null is stored somewhere outside the underlying int.
- arethuza 15y agoIt was just a question about the syntax used - why type Nullable<int> when there is direct syntactical support in C# for nullable types - in this case you could simply type int? rather than Nullable<int>
- endianswap 15y agoAt the very least Nullable<T> is a lot friendlier to search than T? if you want to learn more on the subject.
- bfung 15y agonot everyone is familiar with C#, and Nullable<int> conveys better meaning in writing to that kind of audience.
- noblethrasher 15y agoOr return an iterator (e.g. IEnumerable<T> in C#).
- strictfp 15y agoI don't think that this article focuses on the right problem at all. The real problem in my eyes is that the semantics of 'null' is not agreed upon. It can mean anything, including but not limited to: * Wrong username * Wrong password * Login procedure aborted due to external factor * Programming error This causes a headache, since the client might forget to check for the magic value 'null' and the symptom might therefore accidentally be deferred to any later time. Instead of using a callback method, why not just return an instance of some more sophisticated class, i.e something like this: public AuthenticationResult authenticateUser(User user) { ... } with a possible usage: AuthenticationResult authResult = authenticateUser(user); if (authResult.failed()) { notifyUser(authResult.getFailureReason()); } If the client then tries to use a failed AuthenticationResult as a successful one, or use a successful AuthenticationResult as a failed one, the programmer is free to throw any informative exception. Example: class AuthenticationResult { ... public FailureReason getFailureReason() { if (!failed) { throw new IllegalStateException("you cannot fetch a failure reason from a sucessful AuthenticationResult!"); } } public AuthToken getAuthToken() { if (failed) { throw new IllegalStateException("you cannot fetch a auth token from a failed AuthenticationResult!"); } } } If one instead would use 'null' as a magic value for 'auth failed', the dreaded NPE would show up instead!
- politician 15y agoI'm not bashing your idea, but throwing an authentication exception covers this almost entirely. See, the calling code probably can't correctly proceed unless authentication was successful, and there's no chance that the caller can simply forget to handle an exception -- at the very least, it'll hit a top-level exception handler and/or crash the program.
- strictfp 15y agoThat is another viable strategy if you don't want to handle the login failure in any graceful way.
- politician 15y ago
- dustingetz 15y agoOP argues that callbacks are less complex than type-checked null because it eliminates checking success at the call site. Branching on success at the call site is similar complexity as implementing onSuccess and onError. I don't think its the check itself that matters, its remembering the check, and both a success/failure interface and type-checked null meet this goal. As to which is better, type-checked null is referentially transparent, which is a nice objective measurement of complexity, and referentially transparent functions are generally more reusable than those that aren't. It also keeps my mental model of the stack tidy, tail-call optimization or no. QED, right? have I missed anything?
- kinofcain 15y agoI agree that it doesn't make things easier but makes it easier to remember. One thing that the callback style enables is that you can have more than just success and failure and/or you can provide more information about why it failed. I don't particularly like this callback style though. I do like the multi return value style in languages like Go* where, by convention, an error is returned as the last parameter. You still have to check whether the error itself was nil, but at least you get a reason why; It is also part of the method signature, meaning you can ignore it, but you have to explicitly ignore it. While that doesn't make it referentially transparent, it does force the programmer to acknowledge the possibility of error, even if they don't deal with it. It also keeps the flow more sequential and often times easier to read than the callback style. *C#, Ruby and others also have some form of multiple-return values, but it's rare to see them used in this way.
- dustingetz 15y agothe whole point of type-checked null is that the type system (compiler) forces you to acknowledge potential error states. it doesn't care if you deal with it; you can ignore the error states, or not, but its not possible to forget.