Chalain

Mar. 4th, 2008 08:10 pm Never Send a Symbol to do a Method's Job Chris Shea is destroying Ruby. (It's okay though. Everybody's doing it.) Chris just wrote a blog post called a better try -- chaining methods and nil. I can't really get behind his solution, but he raises some very good points, and I think he's asking the right questions. Before I continue, I should point out that I don't have a better solution, so I don't mean to be dinging Chris. I'm mostly writing this to point out a new aspect of the problem space here.



The problem space, at least, is clear: writing @person && @person.name is not DRY. He dislikes Object#andand because it's too wordy (and I admit, I'm having trouble adopting it myself, even though I like it about the best of all the available options.)



Chris' solution is to write a method called Object#try . You give it the name of a method as a symbol. If it works, it returns the result. Otherwise it politely returns (a non-whiny) nil. For example: @person.try(:name) returns the person's name... or nil. You don't have to guard the access to the person object. Nice and DRY!



The problem is that it changes the syntax. You're no longer calling a method on the object, you're calling try and sending it a symbol. I look at that think, "why is person trying that symbol?".



Hmm. But isn't that what we're doing behind the scenes with Object#andand ? So maybe Chris' code is a bit more honest? No. It's just more explicit, and that's not the same thing. If sending a message is always more honest than calling a method, why call methods at all, ever? Because they express intent clearly. I don't like sending symbols when I could be calling methods if what I'm supposed to be doing is calling methods.



While we're at it, I don't like Symbol#to_proc, either. But now I'm just being crotchety.



It seems like the real problem here is that whiny nil is just too darn whiny. Wouldn't it make more sense to just make whiny nil shut up? The guys over at rubyenrails.nl had this idea a few days ago, too. Unfortunately, I don't like their solution any better.



Actually, at its heart, their solution is identical to turtles, a bit of code I wrote a month ago. Only my version was way better: It could be toggled, it installed cleanly, it had a full suite of RSpec specifications, and everybody knew it was a joke.



Who knows? Maybe my joke will turn out to be... well... practical. Current Mood: thoughtful

Current Music: Dark Designs - Podrunner

20 comments - Leave a comment

From: jerith Date: March 5th, 2008 05:03 am (UTC) (Link) The nil.method_missing way was my first thought on encountering this, but it's too wide-reaching. What I actually want is for Ruby to know when I'm expecting a particular class and getting nil instead. Perhaps some kind of type annotation? That will have to happen deeper in the language, though. Reply ) ( Thread

From: jerith Date: March 5th, 2008 05:46 am (UTC) (Link)



It's essentially andand, but with a much shorter method name. One of the comments on your second link points to the solution I like best so far: http://coderrr.wordpress.com/2007/09/15/the-ternary-destroyer/ It's essentially andand, but with a much shorter method name. Reply ) ( Parent ) ( Thread

From: reaverta Date: March 5th, 2008 07:04 am (UTC) (Link) Tell me again how Turtles! solves this particular problem.



...Other than as an IRC script, I mean. Reply ) ( Thread

From: chalain Date: March 5th, 2008 03:09 pm (UTC) (Link)

with_turtles { puts @person.name puts @city.state.country.name } The form you are familiar with, turtles!/no_turtles! is the global variant, that essentially makes the universe¹ run in turtles² mode.



¹ for sufficiently small universes, e.g. the Ruby environment or (Chalain's IRC client)



² more specifically, nils-all-the-way-down mode



P.S. turtles! on IRC does not actually enabled nils-all-the-way-down mode in my client, since my client isn't actually written in Ruby. The IRC script is, if you will, the movie based on the book of the same name. If you follow the turles link in the post, it takes you to an LJ post where I talk about actual semi-useful Ruby code called turtles. It lets you do things like this, without errors, even if any and all objects mentioned are nil:The form you are familiar with,is the global variant, that essentially makes the universe¹ run in turtles² mode.¹ for sufficiently small universes, e.g. the Ruby environment or (Chalain's IRC client)² more specifically, nils-all-the-way-down modeP.S. turtles! on IRC does not actually enabled nils-all-the-way-down mode in my client, since my client isn't actually written in Ruby. Reply ) ( Parent ) ( Thread

From: ext_88386 Date: March 5th, 2008 04:45 pm (UTC) turtles and train-wrecks aside (Link)



so perhaps ensuring that it is initialized _correctly_

(that is not to nil) is perhaps the issue rather than being constantly on guard.



a NilObject class is one approach. It implements method_missing as desired.

e.g. it logs the caller and returns self



- eager init

@person = NilObject.new



lazy init

@person ||= NilObject.new



(person=NilObj ect.new)



NilObject can be implemented as a _singleton_

(just to be green!)



This is an approach which has been used variously - Smalltalk was one context.



Saves messing with the base system.



Just another view.



Cheers!

sinclair



@person being nil is the problem right?so perhaps ensuring that it is initialized _correctly_(that is not to nil) is perhaps the issue rather than being constantly on guard.a NilObject class is one approach. It implements method_missing as desired.e.g. it logs the caller and returns self #initialize - eager init@person = NilObject.new #person lazy init@person ||= NilObject.new #my_method_with_person_arg (person=NilObject.new)NilObject can be implemented as a _singleton_(just to be green!)This is an approach which has been used variously - Smalltalk was one context.Saves messing with the base system.Just another view.Cheers!sinclair Reply ) ( Parent ) ( Thread

From: chalain Date: March 5th, 2008 05:58 pm (UTC) Re: turtles and train-wrecks aside (Link) so perhaps ensuring that it is initialized _correctly_ (that is not to nil) is perhaps the issue rather than being constantly on guard.



I don't know if this is "the issue", but I certainly would see this as another approach to the solution. We could instruct all developers to guard against this when they initialize their objects. Or we could create an alternative initialization path that always returns a valid. E.g. instead of using ActiveRecord's find(), which can return nil, you use some other factory method that is always guaranteed to return a valid Person object--even if it's a dummy/placeholder object. Like maybe ActiveRecord#find_or_fake ? :-)



That can cause implicit problems later, of course, when other developers assume that a valid Person object is the same thing as a real one from the database, and start doing bigger things with it (beyond simply trying to print its name). But that's what communication is for, I guess--some ratholes you mark off with caution tape and a sign that says "this is a rathole, don't fall down it". Reply ) ( Parent ) ( Thread

From: reaverta Date: March 6th, 2008 12:22 am (UTC) (Link) So it's basically a 'turn off annoying messages about nil' command? Reply ) ( Parent ) ( Thread

From: chalain Date: March 6th, 2008 12:27 am (UTC) (Link) Yes, if by "annoying messages" you understand me to mean "all messages", e.g. also potentially useful debugging information explaining why your webserver just hung. :-) Reply ) ( Parent ) ( Thread

From: jfargo Date: March 5th, 2008 10:30 am (UTC) (Link) writing @person && @person.name is not DRY



I have a much simpler and more elegant solution.



Use paper towels.



*rimshot*



Thank yew, I'll be here all week! Reply ) ( Thread

From: si_yang Date: March 5th, 2008 12:28 pm (UTC) (Link) with_turtles is about the best and cleanest way to do this, I agree. It's better to set this sort of thing as task specific - but the issue I can think of is that there're often times where we want to have turtles all the way down for a single case, in multiple places.



Then it gets messy when we have with_turtles {some.job.here :abcdef, ...} all over the place, with all these blocks created for once-off usages here and there. Reply ) ( Thread

From: si_yang Date: March 5th, 2008 07:38 pm (UTC) (Link) There's that too, but then I'd be worried about the global change silencing bugs in not our code. Reply ) ( Parent ) ( Thread

From: (Anonymous) Date: March 5th, 2008 04:38 pm (UTC) It's not like I MEAN to destroy Ruby (Link) Part of the reason I wrote A better try (http://ruby.tie-rack.org/53/a-better-try-chaining-methods-and-nil/) was because I thought Chris Wanstrath’s implementation (http://ozmm.org/posts/try.html) was a little deficient in its truthiness.



My REAL contention, though, is that I think the need for a try or an andand or turtles is much, much less than all the recent talk about it suggests. I think as code matures, or requirements change, or you want to know more about where things are failing, the less often you'll want to just be swallowing NoMethodErrors on nil. I wrote about that here (http://ruby.tie-rack.org/54/you-might-be-able-to-stop-trying-to-call-methods-on-nil/).



In the meantime, I'll try to let Ruby heal some before I do more damage ;)



Chris Shea Reply ) ( Thread

From: chalain Date: March 5th, 2008 06:03 pm (UTC) Re: It's not like I MEAN to destroy Ruby (Link) Hi Chris!



First off, mostly the comment about destroying Ruby was a nod of agreement that the whole "X is destroying Ruby" is a titch on the histrionic side. I enjoyed your post and have started reading your blog, so I intended no disrespect.



I also agree with you on the need not matching the talk. (Is it hype yet?) I think there's a metathread of "how do we get around whiny nil" and a metareply is emerging of "is this really such a big deal?"



Oh, and by the way, "letting Ruby heal some" is destroying Ruby. :-P Reply ) ( Parent ) ( Thread

From: msmercenary Date: March 6th, 2008 12:13 am (UTC) (Link) > @person && @person.name is not DRY



Maybe I'm just not pragmatic enough, and maybe I'm just not in-tune enough with the Ruby Way of forcing developers out of their comfort zone and into radical new thought patterns, but I'm really not seeing how, in this case, creating a brand-new idiom with which people are unfamiliar is in some way superior to this known idiom.



The point to DRY is to increase maintainability. When you repeat knowledge, the likelihood of a change occurring in only one place but not the other increases. This leads to bugs introduced during maintenance. Hence DRY. Now, I agree, that a violation of DRY such as "person && person.name" does have a nonzero maintenance hit, but practically speaking, that hit is very low. If later, you need to include non-people, even the most boneheaded maintenance programmer will probably recognize that changing to "sapient && person.name" is most likely wrong.



What seems to be missing here is that when you introduce an entirely new idiom, you increase the chance that the next person to maintain your code will not understand the idiom, and will use it wrong. I submit to you that the risk of introducing a bug by Making Up Clever Solutions To Nonexistent Problems is significantly higher than the risk of causing maintenance problems with "person && person.name"



As with all arguments, this could be extended a bit too far. I do indeed think there is a maintenance nightmare present in using "person && person.name && person.name.firstletter && person.name.firstletter.consonant && person.name.firstletter.consonant.font && person.name.firstletter.consonant.font.p itch && person.name.firstletter.consonant.font.p itch.pixelcount".



However, if you ever find yourself writing "person.name.firstletter.consonant.font.p itch.pixelcount" into your code, then I suggest you need to have a long talk with Demeter. Reply ) ( Thread

From: chalain Date: March 6th, 2008 12:58 am (UTC) (Link) Maybe I'm just not pragmatic enough, and maybe I'm just not in-tune enough with the Ruby Way of forcing developers out of their comfort zone and into radical new thought patterns, but I'm really not seeing how, in this case, creating a brand-new idiom with which people are unfamiliar is in some way superior to this known idiom.



Actually, I think you've identified the Fundamental Disconnect there. What I'm noticing more and more is that Ruby has a very low barrier to entry for new idioms. If an existing, well-known idiom is painful, it gets reexamined and a different idiom gets tried. It is expected of programmers to test like crazy and communicate like crazy, both of which reduce the pain of adopting new idioms. This does not make new idioms free, but it does make entertaining a potential new idiom very cheap.



As a result, we've already seen most of the new idioms for this problem get rejected, most on the basis of splash damage (like turtles! pithing your whole codebase), but a few have also been dismissed simply because the new idiom has been tasted and found to not improve the flavor appreciably. (Like replacing a method call with a symbol send.)



I would agree that the Demeter-angering call chain is Of Teh Debbil, but I would further say that so is the "person && person.name && ..." version. If you're doing that 20-level indirect, even with guards, you are writing some pretty fragile code.



Then again, the last time I suggested that calls like this should be proxied, I got shouted down and told that Demeter was too old school to be useful to modern programmers. So I dunno. I'm starting to think that you're all bunch of knuckledragging bastards who feel that writing code should be considered harmful. Reply ) ( Parent ) ( Thread

From: (Anonymous) Date: March 6th, 2008 12:48 am (UTC) no more whiny nil (Link)

class NilClass def method_missing *args nil end end try: Reply ) ( Thread

From: (Anonymous) Date: March 6th, 2008 10:14 am (UTC) Re: no more whiny nil (Link) Please don't ever do this in a production environment. NilClass is a pretty fundmental class in Ruby -- it has its own keyword -- and altering its behavior globally to quietly eat all errors is just plain stupid.



If you're experiencing a lot of NoMethodError exceptions on nil you need to figure out why your code is executing in an inconsistent environment. Why can't you rely on @person existing, and why is your first instinct to just make it be quiet about this failed assumption? Reply ) ( Parent ) ( Thread