Evil Ruby
caiustheory.com
caiustheory.com
# 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") # original
start_date, end_date = timeframe.map { |d| Date.parse(d) }
# better?
start_date = Date.parse(timeframe[0])
end_date = Date.parse(timeframe[1])
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.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 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 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'd never allow the instance_eval trick though, because that obfuscates the code for sake of saving a local variable.
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.
The method itself shouldn't care; it returns the same result regardless whether an argument was passed explicitly or not.
What am I missing? :(
[1, 2, 3].inject(:+)
[1, 2, 3].inject(0, :+)
[1, 2, 3].inject(nil, :+) # doesn't work
Rubinius uses a magic ``undefined`` value for this: def inject(initial=undefined, sym=undefined, &block)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!
It's problematic though, because writing a thin wrapper becomes a pain in the ass.
* 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.
NULL = Object.new.freeze
def foo(bar = NULL)
if bar.equal?(NULL)
# default value
end
end 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?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.
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" ["24 Dec 2011", "23 Jan 2013"].map(&Date.method(:parse))Playing around these examples led to a fun discovery: I'm 5 days away from begin 10000 days old. Maybe I'll take Monday off!
# Add method foo only to instance x.
(x = Object.new).instance_eval { def foo ; puts 'bar' end } x = Object.new
x.extend Module.new { def foo; puts 'bar'; end }
x.foo
# >> bar x = Object.new
def x.foo
puts 'bar'
end a = (b ? throw TypeError "buuut b!")-> b