I can't help but feel that even the "good" example makes things unnecessarily complicated for the sake of saving one line of code.
# original
start_date, end_date = ["24 Dec 2011", "23 Jan 2013"].map {|d| Date.parse(d) }
# better
start = Date.parse("24 Dec 2011")
end = Date.parse("23 Jan 2013")
This really depends. If your data already comes in tupels (which often happens in this case), deconstructing assignment can express very well what you are doing.
For decent Rubyists, reading a map is very easy. On the other hand, both versions are acceptable - I wouldn't bother discussing about either of the options.
Quite probably. I suck at making up "good" examples without taking them from code - couldn't think of anything suitable to borrow from last night.
I think my thought process was reading a CSV from stdin with two fields, using String#split instead of the CSV library and ending up with an array with two elements. That would've complicated it even more though. :-)
I actually like and use the last one. I don't see why it falls into the "don't do this" category of tricks.
I consider
def foo(bar=(default_given = true; nil)
puts "default value given" if default_given
end
a quite readable solution to the problem. Actually, it's the only solution if you need to know if an explicit value was passed and you don't have any restriction on what values are acceptable.
I think I've only ever used it once, and that was in a place where refactoring my way around needing to know that was more trouble than it was worth at that point.
I label it as evil, because people I've shown it to range from "that's amazing, I'd never allow it in production code" to flat out "that can't work". So it's not a common idiom in my experience, nor do people usually grok it on first pass so to me it fails "write unsurprising code" as someone who is part of a team.
Each to their own of course, the "evil" positioning of the post was a bit of sarcasm to me really. I would use those techniques in situations appropriate to them. For the most part, I'll avoid them though, especially in code that I know won't be a simple one-off script.
I agree that this trick is a bit hard to really understand if you first see it the first time and even harder to come up with, but I think it's quite readable even if you don't understand what exactly is going on under the hood. So I'd have no qualms about using or allowing it in production code. It's safer than any proposed alternative, readable and does the job. Whether it's actually good architecture to depend on that knowledge is another discussion, but there's cases where it's required (see the inject example in this thread)
I'd never allow the instance_eval trick though, because that obfuscates the code for sake of saving a local variable.
This one is a matter of perspective. It's really not all that difficult to parse, but it feels icky depending on how you view the scope of "appropriate" behavior in method argument definitions.
Like many viewpoints, the "wrongness" of this example is not black & white; it's shades of gray. On one hand, you have the "anything that will eval is valid Ruby" view, and on the other you have the "If it's not immediately obvious to a beginner, you shouldn't do it" view. There may be better ways to express those two sides of the matter, but that's the general idea.
The problem with this code (from the latter viewpoint) is that it crams too much program logic in to the argument definitions. This example uses parenthesis to force the evaluation of default_given = true; nil` in the argument definition list. That's only two statements, but it violates some common expectations:
A) We generally expect argument definitions to be clear and readable, so that method definitions are self documenting (to some degree); this approach clutters the argument definitions
B) We expect argument definitions to sometimes assign default values
C) We expect program logic to appear in the body of a method, or to be DRY'd up in separate methods
In this way, the example is not "incorrect" but awkward. To borrow an idea from the literate programming camp, I'd say that just because you can write awkward sentences with valid grammar, it doesn't mean you should.
Methods that simply behave differently depending on whether or not an argument is given. For example, passing nil (or any other object) to inject is different from passing nothing.
The one time I've used it seriously was a method that was given the returned body of a HTTP GET call. If I remember rightly it was automatically parsed if it was a JSON body, so you could quite rightly get a nil body back, and that wasn't considered an error. However, if the body failed to parse, it invoked this method without an argument (bad decision further up the call stack, but by the point I hit it I didn't have time to refactor around it right then. AFAIK it was changed later to be less shit code and do stuff properly.)
So I wanted to know in that method if nil was passed in by argument (as the parsed body was nil), or if no argument was passed, at which point the argument was set to nil as the default value by ruby. Having ruby set a `default = true` if no argument was passed was pretty much the only way to get myself out of that hole right then.
Of course, having been coded into that corner once, I'd never do it that way again, so I'm not sure I'll ever be in a corner I need to use it to get out of again. Still useful to have the technique in the back pocket though, just in case!
* One to do the lookup and potentially explode; this always takes an argument.
* A second to provide the default.
Have the caller call the appropriate one. User provides params[:search] (or some other "explicitly trying to search"), then call the first. If not, call the latter.
Agreed, instance_eval is definitely disgusting, but this trick scores well on readability, it's no more of a trap than some other Ruby idioms, and it serves a good purpose (robustness!).
def foo(bar, baz=(default = true; 'default'))
# it still looks separated from other arguments
if default
puts "#{bar}.times { puts #{baz} }"
else
bar.times { puts baz }
end
end
To anyone who knows more Ruby than I - is there a good reason against that I'm missing?
> To anyone who knows more Ruby than I - is there a good reason against that I'm missing?
I probably know less Ruby than you but these are my thoughts.
1) I took me a while to work out what was happening but I finally figured out that the default value is only evaluated when the argument isn't present.
2) If the default value is indicated in the docs it would be wrong for the function to behave differently depending on whether that default was relied on explicitly sent. If you need to know this you are probably doing something wrong elsewhere.
The first example's use case has benefits beyond just shorter code. When writing ruby, I try to code as functionally as possible, and so I built myself a Ruby version of a construct stolen from Lisp, which I use for this exact purpose:
def let; yield; end
let do |b = ["24 Dec 2011", "23 Jan 2013"].map {|d| Date.parse(d) }|
puts "#{b.first} to #{b.last} is #{(b.last - b.first).to_i} days"
end
---
Unfortunately Ruby's parser doesn't understand multiple assignment and default values in block argument declaration when used simultaneously, so you can't do:
let do |(first, last) = ["24 Dec 2011", "23 Jan 2013"].map {|d| Date.parse(d) }|
Though you can do this:
let do |both = ["24 Dec 2011", "23 Jan 2013"].map {|d| Date.parse(d) },
first = both.first, last = both.last|
puts "#{first} to #{last} is #{(last - first).to_i} days"
OS X default ruby is 1.8 and therefore didn't have Object#tap till the last few versions. I still forget that I can use it in one-liners there without it throwing an error. :-)