Thursday, October 25, 2007

Ruby Iterators And The Law Of Demeter

When you iterate over a collection, you're interacting with the collection in an OO manner. Using integer indices to access a collection's contents sequentially violates the Law of Demeter.

Here's a simple Ruby collection iteration:

collection.each {|element| do_something.with(element)}

Of course this comes straight from Smalltalk:

Collection select: [:each | each doSomething]

Compare this to a typical JavaScript iteration idiom, which has very close parallels in Java, C, and many other languages:

for (i = 0 ; i < collection.length ; i++) {do_something_with(collection[i])}

Obviously as a Rubyist I'm going to say the Ruby/Smalltalk approach is much cleaner than the classic Algol-style syntax, which tangles the implementation of iteration into the act of iterating itself. But syntax doesn't matter here. It's what syntax gives you that matters. In this case, what matters is the habit of mind this syntax trains you in.

Nobody sits at their desk going, hmm, Law of Demeter. Should I break it? Or obey it? The value is in the fact that once an object conforms to the Law of Demeter, you can start to think of it as self-contained. Which means that you don't think about it at all - you think with it.

In practical terms, Ruby programmers generally only step through collections sequentially, using integers, when the problem space involves integers. If the problem space requires you to go through an array, you just say "go through an array" and let the Array handle the details. It's kind of like subliminal training to always write clean code. The more you build the habit of matching problem space to terminology, the cleaner your code gets.

9 comments:

  1. The Smalltalk code snippet you have there is not equivalent to the Ruby.

    If the Ruby code says:

    collection.each {|element| do_something.with(element)}

    then, the equivalent Smalltalk should be:

    aCollection do: [:each | each doSomething].

    The #select: method returns a new collection with all the elements that satisfies the condition specified in the block.

    ReplyDelete
  2. Doh! My bad. That's right.

    There is a Ruby equivalent to original (mistaken) Smalltalk example though:

    collection.select {|element| element.matches_criteria?}

    ReplyDelete
  3. My job is with C# and I've gotten very frustrated with collections that don't have the higher-order functions implemented. So much that just last week I wrote this little gem..

    public static IEnumerable<R> Map<T, R>(IEnumerable<T> source, Converter<T, R> converter) {
    foreach(T item in source)
    yield return converter(item);
    }

    public static T Find<T>(IEnumerable<T> source, Predicate<T> match) {
    foreach (T item in source)
    if (match(item))
    return item;
    return default(T);
    }

    ReplyDelete
  4. There is a similar method in Java:

    for(Objtype obj : aCollection){
    do_whatever(obj);
    }

    I assume C# has a similar construct.

    ReplyDelete
  5. @hardwareguy: for and foreach in Java/C# aren't really the same. They're built-in keywords hard-wired to IEnumerable/IEnumerable (in C#) instead of methods on collection objects.

    In fact, the C# List generic class has a ForEach and some other methods... except that more often than not you have an object that implements one or more collection interfaces, and not an honest List class to use ForEach on.

    ReplyDelete
  6. @hardwareguy: That's similar, but not really the same sort of thing. It's basically equivalent to the Ruby #each or Smalltalk #do: in function, but the difference is the Ruby/Smalltalk versions are methods of the object, not a built-in keyword. The objects themselves know how to perform basic common operations themselves. Consider the #select, I imagine in Java you would do something like:

    CollectionType result = new CollectionType();
    for (ObjType obj: aCollection) {
     if (obj.matches_criteria) {
      result.Add(obj);
     }
    }

    Which is a whole different way of thinking about it. And that was the point of the post to begin with.

    I don't know Java well enough to know if you can have higher-order functions, but I don't think so. In C# you do this sort of thing like:

    aCollection.FindAll(delegate(ObjType obj) {return obj.matches_criteria;});

    But most collection types don't actually have these methods available. Also notice that the syntax isn't nearly as expressive as the Ruby/Smalltalk. And, yes, C# has a loop for collections:

    foreach (ObjType obj in aCollection) {
     do_whatever(obj);
    }

    ReplyDelete
  7. IIRC higher-order functions are basically impossible in Java, or at least very very difficult. I do know there's a prominent blogger who went to great lengths to essentially add method_missing to Java but then remained quiet about it instead of blogging his discoveries for some reason (probably perfectionism, i.e., I'm assuming he wasn't satisfied with the implementation).

    These days I'm kinda fuzzy on the latest Java features. I did read a blog post where somebody added Ruby-like iterators to either Java or C#, similar to Brennan's comment. I think this implementation subclassed Array, but don't quote me on that.

    ReplyDelete
  8. Well JavaScript happens to have higher order functions (but people didn't notice until now).
    This week I found that Mozilla JavaScript implements useful iteration methods were you can pass functions to operate on the collection:
    filter, forEach, every, map, and some
    Then you can easily write:

    funtion doSomethig(element){ /*whatever*/}
    and then:
    aCollection.forEach(doSomething)

    Suddenly, JavaScript coolness factor has increased for me :-)
    (also, they provide the JS source code, and is small enough to include it if you're using another browser)

    ReplyDelete
  9. I've seen this done elsewhere as well, but that's very cool. especially every() and some(). some() is like an include? that takes a block and every() is like

    array.include?(arg).uniq == array

    ReplyDelete

Note: Only a member of this blog may post a comment.