Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Eric Kidd: Why Ruby is an Acceptable Lisp (randomhacks.net)
22 points by raganwald on Sept 16, 2009 | hide | past | favorite | 16 comments


Having hacked a (poorly implemented, bug riddled) macro system into Ruby because I needed it, I can say with certainty that Ruby is not an acceptable Lisp. It just turns out that some of the (easiest) things you can use macros for can be faked with a combination of closures and fully executable class code (both good ideas, both present in Ruby and in Python). Try to do any of the most interesting/great things with macros and you'll run into a brick wall.

Of course this can be overcome - he mentions two projects which could turn Ruby from most of a Lisp into 100% of a Lisp with direct access to the parse tree. But those are not used right now in every day Ruby programming.


What are "the most interesting/great things with macros" that you would like to do?


I want to be able to write:

  cache(key_name, :ttl => 30.seconds, :dependent => ModelClass) do 
     model.expensive_operation
  end
and have that cache expire whenever any instance of ModelClass is updated.

With macros, every call to cache() can be expanded into:

  ModelClass.register_expiration_callback(keyname)
  do_cache(...)
Without macros, there's no way to catch deeply nested cache calls. I fake this by scanning the source for calls to cache() on startup right now.


I'm not following. Why does "ModelClass.register_expiration_callback(keyname)" have to be in a macro expansion, rather than just the first statement of the cache() method? It doesn't appear to use local scope at all. Can you flesh out the example a bit more?

FWIW, I believe that Ruby or (especially) Smalltalk-style syntax for method calls and closures covers such a large set of the things macros are used for that their complexity cost (esp when building tools) outweighs their benefit.


Sorry, re-reading that it wasn't totally clear. The ModelClass.register_expiration_callback(keyname) has to be run at parse time, because there's no guarantee that the cache statement will ever be executed before a ModelClass instance is updated. You can't run things at parse time in Ruby (without exotic extensions); that's the whole point of macros.

I completely agree that there are a large number of uses for macros that "SmallTalk style" syntax completely replace. It turns out that a lot of things don't really require you to run code at parse time. Take anaphoric-if for example, which is classically implemented as a macro; all you really need is the ability to manipulate the local variables of a closure from the outside. Parse-time execution is not required.


'Recursive Functions of Symbolic Expressions and Their Computation by Machine', McCarthy, 1960

http://www-formal.stanford.edu/jmc/recursive/recursive.html

Does that sound like 'Ruby'?

I don't think so.


What confuses me in Ruby is that functions and executable blocks (or what they are called) seem to be different things.

So def my_fun(a,b,c) is different from {|a,b,c|...}

I find it very confusing and as a matter of fact, I can not remember what I have to instantiate when to pass around (there is a Proc object, but does it refer to a fn or a block? And what would be the name for the other thing?).

Just a reminder that shortness is not the only criterion - it also has to be intuitive/logical.


This is a method:

  def nuts(a, b, c)
  end
A method can take a block (a block is not an object, it's syntax):

  nuts { |a, b, c| ... }
You can capture a block as a Proc:

  foo = proc { |a, b, c| ... }
  foo.call(1, 2, 3)
  
  def nuts(&proc)
    proc.call(1, 2, 3)
  end
  
  nuts { |a, b, c| ... }
There is also a Method object:

  method(:nuts).call(1, 2, 3)
And you can turn a method into a Proc:

  method(:nuts).to_proc.call(1, 2, 3)
Why does it makes sense to have these as different things? Because we want to use return in the blocks:

  def nuts
    @vars.each do |var|
      case var
      when String
        # Returns the method, not the block
        return "Test"
      end
    end
  end


Also, they behave slightly different when it comes to arity:

  def nuts(a, b)
  end
  
  foo = Proc.new { |a, b| }
  
  nuts(1, 2, 3)     # => ArgumentError: wrong number of arguments (3 for 2)
  foo.call(1, 2, 3) # works fine
We also have lambda, which is a Proc that works (almost) as a method: http://samdanielson.com/2007/3/19/proc-new-vs-lambda-in-ruby


I see the problem with return, but is it really important enough to have the confusing multitude of options? I wonder if it could at least be approximated with labels in languages without "blocks" (like break, next, retry and so on).

Also, what will the return do if the block has been defined outside of the method, will it also return from the method?

Anyway, thanks for the clarification. I guess to_proc was what I had been looking for back then when I ran into the problem.


Procs, blocks, and methods are actually all different from each other.

Of those three, Procs are the only ones that are actually objects (i.e., can be assigned to variables and can receive messages).


    puts_method = method(:puts)
    def get_block(&block) block end
    my_block = get_block { puts_method.call('hello!') }
    my_block.call #=> hello!


    puts puts_method.class #=> Method
    puts my_block.class #=> Proc
Method and Proc are types; blocks are a syntactic construct.


You're right, captured blocks are instances of Proc. However I still claim that methods are objects that can be passed around, assigned to variables, and sent messages. Method and UnboundMethod are both classes that inherit from Object. It's right there in proc.c with the Proc class.

    Method.ancestors #=> [Method, Object, Kernel]


I don't disagree with you there.


Didn't know that. Ruby, you crazy.




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

Search: