8 ms·
Evil Ruby
- Xylakant 14y agoI 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.
- shokwave 14y agoAgreed, 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?
- josephlord 14y ago> 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.
- caiusdurling 14y agoI 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.
- Xylakant 14y agoI 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.
- damncabbage 14y agoI'm still trying to think of a situation where I want to know if someone has passed the default value or not. The method itself shouldn't care; it returns the same result regardless whether an argument was passed explicitly or not. What am I missing? :(
- caiusdurling 14y agoThe 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!
- Mon_Ouie 14y agoMethods 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. [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)
- philh 14y agoYou might want a lookup method to usually throw an exception if it doesn't find anything, but sometimes return a user-specified default. It's problematic though, because writing a thin wrapper becomes a pain in the ass.
- judofyr 14y agoYou can also do: NULL = Object.new.freeze def foo(bar = NULL) if bar.equal?(NULL) # default value end end
- Xylakant 14y agoright, that would work to. I'd still prefer the original solution since it avoids creating a magic constant.
- bradleyland 14y agoThis 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.
- quarterto 14y agoI've used similar to the last one in LiveScript: a = (b ? throw TypeError "buuut b!")-> b
- judofyr 14y agoFYI, evil.rb was also a script that hooked you into Ruby's internals, so you could e.g. change the class of an object, unfreeze objects and do all sorts of funky things: https://github.com/yugui/evil-ruby/blob/master/lib/evil.rb https://github.com/yugui/evil-ruby/blob/master/lib/evil.rb
- deleted 14y ago[deleted]
- ballard 14y ago# Add method foo only to instance x. (x = Object.new).instance_eval { def foo ; puts 'bar' end }
- caiusdurling 14y agoI find a better way of doing this is to use a module. x = Object.new x.extend Module.new { def foo; puts 'bar'; end } x.foo # >> bar
- Spakman 14y agoAnother option (that I prefer): x = Object.new def x.foo puts 'bar' end
- xentronium 14y agoWhy not #tap with some one-letter variable for the first example?
- caiusdurling 14y agoOS 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. :-)
- wub 14y agoI 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")
- caiusdurling 14y agoQuite 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. :-)
- Argorak 14y agoThis 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. # 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.
- glenntzke 14y agoIf you find these sort of ruby (ab)uses interesting, take a look through the ruby Koans solutions at http://rubykoans.com/ http://rubykoans.com/ 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!
- caiusdurling 14y agoGood suggestion. I love the koans.
- jayferd 14y agoYou forgot this one, using point-free style: ["24 Dec 2011", "23 Jan 2013"].map(&Date.method(:parse))
- SeoxyS 14y agoThe 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"