Search This Blog

Loading...

Tuesday, March 15, 2011

Named Arguments in Ruby are All Wet and How to Keep DRY

One of the more interesting features in Ruby 1.9.n and up is the new syntax for passing hashes as arguments. You can still do it the old way, which is clearly a hash, but there's a new syntax that pretends to be a series of named parameters. This gives you the flexibility of not being tied down to positional parameters and allows a great deal of leeway in setting default values, not to mention improved readability of complex method calls. So, you can have calls that look like this:

foo(anything: 22,  something: "it")
foo(nothing: true, anything: 0)
foo(everything: 'almost', something: 'unknown', anything: 0)

Which is awesome on the calling side! However, in every example I've ever seen, foo ends up looking a bit ugly in order to support this syntactical sugar.

def foo(arguments = {})
    something = arguments[:something] || 'some'
    everything = arguments[:everything] || 'all'
    nothing = arguments[:nothing] || false
    anything = arguments[:anything] || 100
   
    # do something with the passed arguments

    puts arg.anything

end # def foo   

What's wrong with this? There are several bad points here.
  • It violates DRY (Don't Repeat Yourself), and it does it twice! Every line references the arguments array, and every line defines the argument name twice - once as the key into the argument array, and once as a variable to hold the argument value. If you have a large number of possible arguments, the boilerplate becomes tedious - which means error prone.
  • It has no checking of the passed variables. If the caller passes an invalid argument, the method will silently ignore it. If this is a minor misspelling of a valid argument, this could lead to a very difficult bug to catch.
  • The defaults are not easy to see. They're at the end of a series of operators, and they're not lined up. They could be lined up, of course, but they'll end up in the far right of the screen - it's a pretty long line of code.
  • It handles defaults poorly - more on that later.

My solution is a helper method that collects the named arguments into a single structure, does some basic validity checking, and sends the whole kit and kaboodle back to the caller as an anonymous Struct.

def collect_named_args(given, expected)
    # collect any given arguments that were unexpected
    bad = given.keys - expected.keys

    # if we have any expected arguments, raise an exception.
    # Example error string: "unknown arguments sonething, anyhting"
    raise ArgumentError,
        "unknown argument#{bad.count > 1 ? 's' : ''}: #{bad.join(', ')}",
        caller unless bad.empty?
       
    Struct.new(*expected.keys).new(
        *expected.map { |arg, default_value| given[arg] || default_value }
    )
end # def collect_named_args


How does that work in our new foo? Pretty nice, actually.

def foo(arguments = {})
    arg = collect_named_args(arguments,
        something:  'nothing',
        everything: 'almost',
        nothing:     false,
        anything:    75)

    # Do something with the arguments 

    puts arg.anything
end # def foo


Much cleaner! It has one more line of code, but the lack of redundancy makes up for that. In fact, it's so much less redundant, we can afford to put in some overhead and fix another problem of the original foo.

Let's look at this line:

    nothing = arguments[:nothing] || false



"false" is a bad choice for nothing's default, don't you think? The default for nothing is nil, not the opposite of true. Let's fix that:


    nothing = arguments[:nothing] || nil



Cool! I can now call foo(nothing: 'something'), or just foo() and I'll get 'something' or nil respectively - just like I want! But there's a trap for the unwary here. What if I call:



    foo(nothing: false)


Uh-oh.  I want nothing to be false, but I end up with it being set to nil. That's probably not working as intended. Both false and nil trigger the or (||) side of the assignment which results in the default being assigned, but we only want nil to do so.

This could be even uglier! Look!

        something = arguments[:something] || true
    .
    .
    .
    foo(:something: false)



This results in the variable something being true! We're getting the exact opposite of what we were asking for!

It's easy to fix, of course:

    nothing = arguments.has_key?(:nothing) ? arguments[:nothing] : nil
    something = arguments.has_key?(:something) ? arguments[:something] : true


Now only nil (which is returned by the arguments hash when the key is not found) will trigger the assignment of the default. If we want the default to be nil, that works, and if we pass nil, we get nil back! Booleans return what is passed, and their defaults when nothing is passed. It just works!

It should be our new boilerplate. But look at that statement! "arguments" appears twice, "nothing" appears (in some form or another) three times! We've got an assignment operator and a ternary operator all going at once. We need the method call has_key? for every assignment. What a mess! Let's rewrite the pertinent line of collect_named_arguments.

    Struct.new(*expected.keys).new(
        *expected.map { |arg, default_value| 
            given.has_key?(arg) ? given[arg] : default_value
        }
    )


It's still a mess, but it's a mess that you write once, and it's hidden from view forever after.

This paradigm could easily be expanded to include argument validation with the use of lambdas, but that's beyond the scope of this discussion. As an exercise, try writing a validate_named_args function that works on the same principle. For simplicity, you can use the results of collect_named_args as your data source.

While playing with this, I found that it was fairly easy to create method helper functions that allowed for nesting arguments like so:

def window...

def box(arguments = {})
    collect_named_args(arguments,
        top:    0,
        left:   0,
        height: 1,
        width:  1)
end # def box
 

window(
    layer: :top,
    coordinates: box(
            height: 480,
            width: 640))


I'm curious as to what other interesting applications can be derived from this. Is there an even neater way to wrap up this package? It should be possible to do this in a more object-oriented way, certainly. Thoughts?

Actually Toto, we may still be in Kansas after all

Oz, like most fantasy lands, are only strange on the surface. When you dig below the surface, they usually turn out to be our world dressed up in a bizarre outfit, and maybe just a little drunk.

Which is only to be expected. We may enjoy the novelty of visiting a strange place, but nothing beats the thrill of looking at ourselves in a whole new way.We are an endless source of fascination to ourselves.

The key to reading the Wizard of Oz (made painfully explicit in the movie), is to realize that Oz will always turn out to have been Kansas after all whenever we visit with Dorothy.

So it is with blogs. I said last time that I had made decisions on what this blog would be about. Well, it's about me and my life. But at the same time, it will be about you and your life. If it isn't, at least in part, you won't read it for very long.


In the finest traditions of Oz, Wonderland, Neverland, Erewhon, Freedonia and other fantastic places, we'll sometimes be looking at ourselves and the things we find interesting through a series of kaleidoscopes and fun house mirrors - and sometimes we'll be looking at them straight on.

But what things? Here's the list, and we'll talk a bit about  each over the coming weeks and months: Computer Programming [Android (Phone OS), Ruby (Script Programming), AI (Artificial Intelligence)], Doctor Who, C-PTSD (Complex Post Traumatic Stress Disorder), Philosophy [Atheism, Ethics], Politics, Science[Cognitive Science, Cosmology, Fundamental Physics].

If you found yourself yawning, this may not be for you. If you found yourself salivating - er, could you pick up that napkin and lay it over the keyboard? Good, good. You'll thank yourself later for that. Stick around - especially if you like any or all of the above blended and mixed in various degrees. I'm an interdisciplinary kind of person!

The guacamole dip? Well, if there's enough demand, I may start a second blog for creative writing efforts. I'm kind of curious myself, now.


Next time, we'll start with a precious gem and a discussion of my struggle not to repeat myself.

Until then, there's no place like home!

Monday, March 14, 2011

Starting Thoughts

Beginnings are a special time. There's really nothing quite like them - filled with potential and those first steps that set you on your course. A time of momentous decisions.

It really is a problem.

Decisions are exclusionary. When you decide for one thing, you necessarily are deciding against everything else that could be made real. So, when I start writing this blog, I need to think about what messages I want to send, what goals I hope to accomplish, and how I want to approach it.

It's a formula for procrastination. If I make this a creative writing blog,  I lose out on much practical discussion and commentary and the opportunity to ruminate on interesting ideas and concepts. If I go for a topical blog, I'll need to limit the creative elements, since people showing up to see "what happens next" are unlikely to want to wade through the non-fiction to see what Philip Gasglow intends to do with that artichoke he found in his mother's library, and why Detective Matthews (and only Detective Matthews) of the Chicago Police Dept. has sworn to stop him by any means necessary.

Hint: That artichoke dip? It may be green, but it's not guacamole.

If I go topical, what topics? How many? Too few, and I may have trouble find enough to write about, too many, and I might be so diffuse that I'll have no depth! If I include controversial and stigmatizing material, should I be concerned about my privacy? Future employers and my neighbor's underage kid my find this blog one day, after all. Oh, the angst!

So many decisions - but if you don't make any, you'll end up not having any beginning at all. So, decisions are made, commitments happen, and actions are executed. The end result of which is - my next blog, where all is revealed!

Admit it - you're mostly coming back because you hope I'll tell you about the dip.