9 ms·
Avoid Null Checks by Replacing Finders with Tellers
- glurgh 13y agoIf you're particularly bothered by this you can just make your query interfaces uniformly return iterables. Which is what already typically happens in your average low-level DB API, in languages lambdaful and lambdaless.
- batterseapower 13y agoThis is just the Church encoding of Haskell's Maybe type. It is indeed a powerful way to simulate algebraic sum types when you only have product types in your language.
- addlepate 13y agoKeep Kosher by Eating Pork Through a Hole in a Sheet
- com2kid 13y agoAs someone who has only played with Ruby briefly, and at that many years ago, I feel that this article could have done more to explain the author's concepts to those not familiar with Ruby. From what I can see, how is this much different than a Null check? I agree it looks a bit better, but what is the real difference? Isn't there still a check for if the Person object exists? In terms of C++ or C#, is it correct to imagine this as an iterator over one object where inside my iteration loop I am passing my object into a lambda? (Seems overly complicated so I am guessing this is the wrong interpretation?) Indeed a post at the bottom of the page mentions using foreach, but since sane languages have closures now days (thankfully! Life is so much better now!), how would I apply this to, say, C++?
- Strilanc 13y agoThe difference is that the null check is front and center, instead of hidden. In C# or Java, the member access expression "a.b" contains a null check. There's no way to do a member access that doesn't do a null check (for reference types)! It's impossible to distinguish member accesses that were expected/assumed by the writer to not fail from member accesses where the check is relied upon. With an option type / a maybe monad / 'using telling instead of finding', there are no implicit null checks. Only explicit checks, and only when necessary. Either you use Option.map, to do a check and propagate nulls, or you don't, in which case there's no null path. You don't have to worry about null reference exceptions, making a check where you shouldn't, or missing a check where you should have, because the compiler prevents you from making those mistakes. Applying it to C++: https://github.com/simonask/simonask.github.com/blob/master/maybe.markdown https://github.com/simonask/simonask.github.com/blob/master/... Applying it to C# (as an augment over null): http://twistedoakstudios.com/blog/Post1130_when-null-is-not-enough-an-option-type-for-c http://twistedoakstudios.com/blog/Post1130_when-null-is-not-... Applying it to Java (as an alternative to null): http://java.dzone.com/articles/java-optional-objects http://java.dzone.com/articles/java-optional-objects
- topbanana 13y agoOff-topic but I find the color scheme of your blog very offputting
- enobrev 13y agoThe difference seems to be that instead of checking for whether a single object is returned, to actually check for the size of the collection returned, which (depending on your language, of course) the method used to iterate over that collection does automatically.
- dragonwriter 13y ago> From what I can see, how is this much different than a Null check? I agree it looks a bit better, but what is the real difference? The difference is that the "teller" function incorporates both the function of the finder (which may actually still exist and be used under the hood) and the function of the null check, so that you don't have the boilerplate null-check code every time you need to do a null check. (It may not actually be doing a null check per se, the finder-equivalent may be returning a set of 0 or 1, or 0 to many if using non-exclusive criteria, and the teller iterating over it, but for inherently unique criteria it could be a finder that returns a single object or null underneath. While whether the criteria inherently will always return at most one or potentially multiple objects to operate on may be important to the caller, the underlying implementation isn't.) > In terms of C++ or C#, is it correct to imagine this as an iterator over one object where inside my iteration loop I am passing my object into a lambda? The most general teller implementation would operate as an iteration over a collection returned by the finder; for finders with inherently unique criteria, that collection would always have either zero members or one member. Particular teller implementations may or may not use that general implementation. For unique cases, the teller could just be a call to the finder with a null check guarding a call to the lambda that passes the non-null result of the finder.
- gwright 13y agoThe difference is that the API has changed from: search_for_object_in_database which returns the result or a failure indication to: if_object_in_database_do { this_with_the_object } The handling of a failed search is now internal to the API instead of in the client code. In this example, 'this' is simply not executed if the search fails. The client code is passing instructions on what to do when the search succeeds instead branching on success or not. The idea of passing code in addition to data is a powerful one and when the language makes it easy (Ruby blocks, Javascript function objects, lisp functions) it really does change the way you think about the design of APIs.
- sageikosa 13y agoforeach (var myObject in dataSource.getObjectsByID(id)) { myObject.DoStuff(); } Twasn't so hard, as long as you don't make your data API return singletons.
- DanWaterworth 13y agoCongratulations, you invented monads.
- Strilanc 13y agoYou're probably being downvoted for being sarcastic about a good thing (at least... I think that's sarcasm?). Independently discovering Option.map and realizing its usefulness is good. Point at the existing work instead of making fun. http://en.wikipedia.org/wiki/Option_type http://en.wikipedia.org/wiki/Option_type (well.. maybe a bit more introductory than that..)
- DanWaterworth 13y agoI didn't mean to come off sarcastic. Though, reading it back, I can see how you could have thought that. I'm enthusiastic whenever anyone inadvertently discovers functional concepts.
- akkartik 13y agoCan you elaborate? I don't see how this pattern is a monad.
- pestaa 13y agoIt is a sequence. Step 1: find something. Step 2: if it is found, do something with it, and optionally find another thing based on that result. Sounds like programmable semicolon, or a monad, to me.
- DanWaterworth 13y agoIt's not quite defining a monad, but it's very close. The example given in the article is: data_source.person(id) do |person| person.phone_number = phone_number data_source.update_person person end You can abstract this pattern a little by putting it in a function: def maybe_bind(x, &blk) if !x.nil? blk.call(x) else nil end end Now consider the type of this function: nil or X, (X -> nil or Y) -> nil or Y A monad is any type for which you can define bind and return. They have the Haskell types: bind :: m a -> (a -> m b) -> m b return :: a -> m a "maybe_bind" is very similar to "bind"; just replace "m" with "nil or" and uncurry. "return" is trivial to implement.
- tbrownaw 13y agoWhy? What is so bad about having your conditionals be explicit? What happens when you need an "else" branch? Is not having a person for the given ID an error, is it a "do nothing", or does it mean to create a new person? What does the callback version do if you forget to make your index unique, and end up with multiple people with the same ID; does it get run multiple times? The version that returns a Maybe<Person> at least has an obvious proper response (throw an error).
- thedufer 13y agoComing from working with node.js, this feels very strange. How do I call a callback in the else case? It looks nice and all, but I don't see a way to make this work if db calls are asynchronous. Any ideas?
- anonymoushn 13y agoPass two callbacks, or make your callback begin with if(err). Which looks suspiciously like a null check.
- masklinn 13y ago> What happens when you need an "else" branch? Then you use one, but null checks generally aren't cases of if/else conditionals, they're more along the lines of "either I have one or mayday mayday". Sometimes slightly softened into "either I have one or use this default thing"
- tbrownaw 13y agomore along the lines of "either I have one or mayday mayday". That would be the Perl way of my $person = $data_source->get_person($id) or die "No person available"; ie, there's code to be executed (to raise the error) in the case that there is no person. The callback structure from the article doesn't allow for this.
- wpietri 13y agoWe make something explicit when we want to call attention to it. Otherwise, we should make it implicit. Attention is a limited commodity, and I think it's worth preserving for the important things. Sometimes it really is worth reminding people, "Hey, this could be null!" But a lot of the time that is boring. To me this often comes back to favoring meaning over mechanism. The higher up I go in code, the more I want it to be about what the system means, not the particular mechanism I've chosen to make it work. E.g., Computers may be implemented via impure silicon, but most of the time we can forget that. I try to apply that to any sort of mechanism.
- robotresearcher 13y agoAn idiomatic Java/C++ OO approach is to have an interface to the datastore that eliminates the need for the NULL. That way you don't need to know the internal details of the datastore object. datastore.ChangePhone( personId, number ); could return a bool to indicate success/failure or throw a "personId not found" exception according to taste. This is normal Java style and is in line with the article's recommended "tell-don't-get" approach. Requires boilerplate inside datastore object, but hey, that's not exactly news for OO.
- ajanuary 13y agoOO doesn't mean boilerplate. Just because there's a correlation in languages like C++ and Java doesn't mean there's causation. Most of their boilerplate derive from the type system.
- wwweston 13y agoAnybody who's written much JavaScript without a jQuery-like function should appreciate this too -- used to be much more common to have code littered with checks to see if a dom element existed, then do an operation if it did. Performing an operation over a set (including empty) of elements seems easier.
- ape4 13y agoYes I thought of jQuery also. jQuery always returns an array. If it has no elements that means the thing wasn't found.
- Locke1689 13y agoIf you did this everywhere your program would be slow as shit due to all the closure allocation in inner loops. Of course, this doesn't matter much for Ruby, which is already slow as shit, but C# and Java people would be pissed.
- munificent 13y ago> all the closure allocation in inner loops. Modern compilers for languages like JS that heavily use closures optimize for this.
- mrmekon 13y agoThis is the response I was looking for. We keep making computers 2x faster, and then making software architecture 4x more wasteful (given, it's also 20x faster dev time). "if (ptr == NULL)" is usually two instructions and an instruction pipeline flush. That sucks, but I can't even imagine what the processor ends up executing for the Ruby lambda callback. Slightly more readable code at what cost?
- masklinn 13y ago> That sucks, but I can't even imagine what the processor ends up executing for the Ruby lambda callback. That's not a very good question, `if object == nil` will already be a pretty huge number of instructions in ruby. Now if the question is "could you have this pattern compile down to little more than a null check", the answer is why not? A bit of flattening/inlining should be able to handle it correctly. And in most fields, the gain in safety way outweighs the almost unnoticeable (against background noise) loss in efficiency.
- masklinn 13y agoThis dovetails into the classical "tell, don't ask": without good support for anonymous functions or blocks, for a number of operations you'll have to ask an object for parts of its internal state, munge that state then shove it back into the object. Said object may return a command protocol instead, but it's generally verbose and exceedingly rare. Blocks provide a conduit to provide compound and possibly complex commands while leaving the object itself in charge.
- jlarocco 13y agoThis is a strange article. The biggest problem with null checks isn't that they make code difficult to read - it's that people forget to add them and that can cause bugs and security vulnerabilities. I've never known anybody to complain that they made code difficult to read. Unless they're mixed in with "real" program logic, like "if (obj != NULL && obj->value()<20)" then they're really not too hard to ignore. And the author doesn't seem familiar with many programming languages. Using his "finders and tellers" idiom isn't as special as he thinks, and doesn't require lambdas or blocks. Here's a similar idea in Python, not using its lambdas: for person in data_source.person(id): person.setPhoneNumber(phone_number) data_source.update_person(person) # And for other situations: with open("whatever.txt") as inf: for line in inf: print(line) C++ has had similar functionality in the STL using iterators since before C++98. Lambdas and blocks are convenient and nice features to have, but they're not required for this.
- masklinn 13y agoThe issue here is that the semantics are weird, an iteration block is expected to iterate 0..n times, not 0..1. I know I'd never expect a loop primitive to be used as a conditional. And the context manager can't handle this case, as it's not able to skip the block body.
- tbrownaw 13y agoI do this in my PL/SQL code occasionally, when I'm feeling lazy and either don't want to handle errors properly or want to take advantage of auto-defined iteration variables. Since it's a nasty dirty hack that tends to misbehave on bad data instead of failing cleanly, I generally restrict it to experimental/throwaway code. Seeing it recommended as a good idea is a bit WTF.
- jlarocco 13y agoI didn't mean to imply it's a good idea or not. In my own code I'd use the null check and throw an exception. It's suspicious that it's trying to update a non-existent record, so I'd usually want it to break so I can fix the underlying logic bug. I was simply pointing out that the technique is available in other languages without using lambda and/or blocks.
- munificent 13y agoThis is a pretty neat pattern. One nice bonus is that the code invoking the closure has a chance to do work after the closure. This lets you do nice "scoped" behavior automatically. For example, in his Ruby code: data_source.person(id) do |person| person.phone_number = phone_number data_source.update_person person end I would take that `update_person` call and have the `person(id)` method do that implicitly: data_source.person(id) do |person| person.phone_number = phone_number end (I'd likely optimize for the case where the person wasn't actually modified too.) This way, the caller doesn't have to remember to explicitly update the person. Another way to look at this pattern is as a poor-man's pattern match. For example, using the pattern-matching syntax of my language[1], you could do: match dataSource person(id) case person is Person then person phoneNumber = phoneNumber dataSource updatePerson(person) end Granted, that's more verbose here, but it lets you have other cases if that makes sense for your problem. Magpie has blocks too, so a literal translation would be: dataSource person(id) as person do person phoneNumber = phoneNumber end For very short blocks, you can use an implicit parameter name similar to Scala: dataSource person(id) do _ phoneNumber = phoneNumber The `do` notation is just syntactic sugar for passing a function as the last argument, so you can also do: dataSource person(id, fn(person) person phoneNumber = phoneNumber) How did I get derailed talking about Magpie? [1]: http://magpie-lang.org/ http://magpie-lang.org/
- stevepotter 13y agoSounds great except often something needs to be done when a value is null, like giving user feedback. If the answer then is to provide another block, just go back to the if statement.
- tieTYT 13y agoI love the Object Mentor guys' blog articles, but they get a new blog every 2 years so it's really hard to find their articles in one place. I remember reading one, I think by Michael Feathers, that was about how sometimes your code gets larger when you refactor and that's expected and good. He compared code to an orange. The rind is the class/method signatures. The pulp is the implementation. If the rind get larger by reducing the size and DRY violations of the pulp, you can consider that worthwhile. If anyone can find this article I'd be grateful. I haven't been able to track it down myself.
- tbrownaw 13y agoHe compared code to an orange. The rind is the class/method signatures. The pulp is the implementation. If the rind get larger by reducing the size and DRY violations of the pulp, you can consider that worthwhile. That sounds very very wrong, as you're making your code's clients do more work / be more complex just so you can simplify your implementation.
- tieTYT 13y agoThat's a huge assumption on your part. You should show me an example in code of what you think I'm saying.
- Pxtl 13y agoWouldn't simply returning a list and then using your language's favourite foreach construct have functionally the same semantics without the confusion of passing code-blocks around?
- dragonwriter 13y agoNever saw "passing code blocks around" as a source of confusion, and in many languages (Ruby, for a salient example) "passing code blocks around" is the same mechanism as is used in the languages main foreach construct.
- sambeau 13y agoI once wrote a programming language that had a localised 'this' called 'it' exactly for this pattern. if (something_exists) { do_something_to(it) } It was really useful (and readable). I believe Perl has something similar but it was less readable.
- draegtun 13y agore: Perl - You're probably thinking of $_, which most Perl programmers call it. However this only gets topicalised in bare for & while but not if. So you could do... for (something_exists) { do_something_to($_); } ... as long as something_exists doesn't return a list :) More common approach I've seen in Perl (and other languages where variable declarations return its value) is this: if (my $it = something_exists) { do_something_to($it); }
- anon1385 13y agoAnybody know why this got flagged off the front page? http://hnrankings.info/5893950/ http://hnrankings.info/5893950/
- bfung 13y agoI don't know why, but take it as a signal that it this isn't as interesting to the HN community. If you think about it, it's not avoiding null checks, it's just moving it to a different place. The null check still needs to happen in order to know when to call the block, it just happens to be in the datasource.whatever code.
- masklinn 13y agoThe point is to move the check out of the library user's way, and out of his ability to forget about it. Same as block-scoped resource patterns (RAII, context managers, unwind-protect, whatever).
- Rickasaurus 13y agoNice try secret functional programming guy.
- NateDad 13y agoThis does not seem to be fixing null checks. It seems to be fixing lack of proper error handling. The problem is not that you have to check for null, the problem is that you don't know if null is a valid value, and even if you know it's not, you don't know why the method failed. This is simply a lack of a channel for returning errors. There are a lot of ways to fix this problem... Many modern languages use exceptions for this. Some languages use out parameters. Languages with multiple return values often return a value and an error. They're all essentially equivalent, and yes, they're all better than just returning null. I don't really see a difference between: data_source.person(id) do |person| person.phone_number = phone_number data_source.update_person person end and data_source.updatePhoneNumber(id, phone_number) The latter one encapsulates the find, the error handling, and the setting. Yes, you might have a null check in there. Or you might have a try/catch, or you might be checking an out value or a multiply returned error value.... there's really no difference. You've changed the API to signify that the person might not exist and this function might not actually do anything. And that's fine, if that's fine. However, most of the time, if I go to set someone's phone number, I want to know if the system thinks that person doesn't exist.
- qznc 13y agoThat first code might just be the implementation of updatePhoneNumber. You are just moving the problem.
- NateDad 13y agoSomewhere there has to be a check to see if the person with that id exists, and if so, change its phone number. It could be a SQL statement running in the db, it could be a null check, or a try/catch or it could be inside data_source.person(id) do |person| I'm just saying, it has nothing to do with nulls and everything to do with error handling. This teller pattern handles errors by implicitly ignoring them. If that's your intended behavior, that's fine. It also evidently has some nice language support in ruby, and that's cool. But it doesn't really "solve" any "problem". Mostly because there's no problem in the first place. Error handling is not a problem, it's a fact of life in software development.
- 13y ago
- tmuir 13y agoCan we please dispense with the whole "How did these moron's get anything accomplished before I came along?" pattern? The article, comments on the article's page, and a lot of comments here are all variations on "This perfectly acceptable practice makes my eyes bleed, and is the calling card of shitty programmers everywhere. After I switch some semantics, rearrange the deck chairs, etc, its now perfect in every way, invulnerable to bugs or misuse." No pattern is universally applicable. There are lots of ways to accomplish a task. Coming up with a different solution does not invalidate what came before.
- nilliams 13y agoEh, seems like you exaggerated the article/point only to then tear down your own extreme interpretation of it. >> After I switch some semantics, rearrange the deck chairs, etc, its now perfect in every way, invulnerable to bugs or misuse I don't see anybody claiming that.
- SigmundA 13y agoSurprised no one has mentioned objective-c's sending message to nil which is relied on throughout apple frameworks which is basically what the article says is a lot of work to implement but given to you for free in obj-c: http://stackoverflow.com/questions/156395/sending-a-message-to-nil http://stackoverflow.com/questions/156395/sending-a-message-... C# kinda does this for nullable types, it is referred to as null lifting.
- mikeash 13y agoSadly, it's not too consistent there. For example, this is OK: [[obj find] doThing] But this will crash on nil: [array addObject: [obj find]] Nop-on-nil can be convenient, but without APIs that also nop on nil parameters, it can be a little annoying when you still have to check half the time.
- tome 13y agoThe astounding lengths languages will go to to avoid implementing sum types.
- utf8guy 13y agoThis is essentially the Maybe monad.
- radiowave 13y agoIt may be I'm missing something here. Do we really need to tack block handling onto every method that can change some state? I don't see any benefit over the following idiomatic Smalltalk: (self dataSource getPersonById: id) ifNotNil: [:person | "operate on person here" ]. (There's also ifNil:ifNotNil: for where you want to handle both cases.)
- fexl 13y agoThe problem I have with his example is that I would probably want to do this: Person person = dataSource.getPersonById(personId); if (person != null) { person.setPhoneNumber(phoneNumber); dataSource.updatePerson(person); } else { user.notify("That's an invalid ID."); } So I need an "else", in effect, regardless of whether it's implemented as a conditional, block, lambda, exception, or whatever.
- bandushrew 13y agothat has got nothing to do with blocks. you could have pretty much the same code in any language by doing something like: List personList=database.personListForID(id) for(person in personList) { person.setPhoneNumber(phoneNumber) } Personally I prefer that idiom over null checks, it is a lot easier to read. I do like the OP names for 'Finder' vs 'Teller' though, that is a good way to describe the difference, that I haven't heard before.
- ambrop7 13y agoAll these attempts to eradicate nulls are getting ridiculous. The possibility of nulls is most often rooted in the essential complexity of a problem. If his function is intended to be called with a possibly incorrect personId, the null check is perfectly fine, it's just expressing his problem. His "solution" just hides that for no benefit - actually, it makes it less clear what's happening. On the other hand I think what he really needs is "assert personExists(personId)" at the beginning of the function. People who complain about nulls are either lazy or incompetent programmers, and the blame just happens to fall on null due to null being an inherent feature of a programming language. Dealing with nulls is exactly like dealing with any other case where you need to think about multiple possibilities, and possibly assert that some of those can't happen.
- orclev 13y agoImplicit nulls are the underlying problem. If you default to non-nullable types and only allow nulls when explicitly annotated then it becomes much simpler to spot and handle all the cases where nulls might creep into your code. There are multiple ways of accomplishing that of course. Rust forces you to explicitly declare a variable as nullable and won't allow you to assign a nullable value to a non-nullable variable without performing a null check first. Haskell simply doesn't have a built in null value instead handling the problem via the Maybe type which represents a nullable value. I'm sure there are other examples in other languages, but those are the two I'm familiar with. The concept of null is fine, but it shouldn't be the default state of everything. Force the programmer to declare his intention ahead of time and have the compiler help him by trying to find and validate all the edge cases, or at least as many as reasonably possible.
- ambrop7 13y agoTrue, some language features can indeed make defensive code bit less verbose, but I believe their value is overestimated. My approach in C code is usually to assume no pointer argument is permitted to be null unless explicitly stated, and to not add null assertions (since if is null program will most likely crash which serves the purpose of the assertion). I don't remember seeing a null dereference bug in my code within the last year.