Hacker News new | past | comments | ask | show | jobs | submit login
Evil Ruby (caiustheory.com)
39 points by wlll on Jan 23, 2013 | hide | past | favorite | 27 comments



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.

    # 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.


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. :-)


FYI, 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


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.


I'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? :(


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.

     [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)


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!


You 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.


I'd argue that it's really two functions:

* 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.


You can also do:

    NULL = Object.new.freeze
    def foo(bar = NULL)
      if bar.equal?(NULL)
        # default value
      end
    end


right, that would work to. I'd still prefer the original solution since it avoids creating a magic constant.


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"


You forgot this one, using point-free style:

    ["24 Dec 2011", "23 Jan 2013"].map(&Date.method(:parse))


If you find these sort of ruby (ab)uses interesting, take a look through the ruby Koans solutions at 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!


Good suggestion. I love the koans.


Why not #tap with some one-letter variable for the first example?


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. :-)


    # Add method foo only to instance x.
    (x = Object.new).instance_eval { def foo ; puts 'bar' end }


I 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


Another option (that I prefer):

  x = Object.new
  def x.foo
    puts 'bar'
  end


I've used similar to the last one in LiveScript:

  a = (b ? throw TypeError "buuut b!")-> b




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: