Metaphysical Developer

Improving Ruby

Posted in Languages by Daniel Ribeiro on March 31, 2010

Some may say Ruby is a bad rip-off of Lisp or Smalltalk, and I admit that. But it is nicer to ordinary people.

Yukihiro “Matz” Matsumoto, LL2

When I heard the Smalltalk traits of Ruby, I was intrigued. When I learned more, I enjoyed Ruby’s similarities with one of the most beautiful and powerful languages I’ve known. As I dug deeper, I enjoyed more of its wonderful metaprogramming abilities, which makes Ruby’s classes a lot more dynamic and easy to declare than Smalltalk’s. After reading this in-depth comparison of both, and Kent Beck’s article on the incompatibilities of Smalltalk’s VM implementations, I realized that I was generally more in favor of Ruby than Smalltalk (even though I fear that What Killed Smalltalk Could Kill Ruby, Too).

But Ruby is not perfect. In all fairness, its creator claims that it’s just plain impossible to design a perfect language. But I do believe it could be a bit better. People do point out some controversial rough edges, but these seem a bit trifle when compared to what really bothers me:

  • No scoped open classes. It is an issue that is actually being considered to be solved on 2.0 (and there are some branches of ruby that enable it), but, for the time being, there is no way to make the changes made by opening an existing class only apply to objects while being used on the a lexical scope. This is not the same as adding and removing the changes as, in this case, calls that fall out of scope (such as to another module or file) still see the changes. It would be nice to have both ways of open classes: scoped and not scoped.
  • Method arguments do not interleave with the method’s name (like in Smalltalk). Example: Instead of calling: File.fnmatch(‘*’, ‘/’, File::FNM_PATHNAME) you’d be to call it like: File.fn match: ‘*’ path: ‘/’ flags: File::FNM_PATHNAME. This seems weird, but it is a very powerful feature that allows method invocation to be descriptive, similar to Python’s named arguments or even Ruby’s named arguments with a hash. On the other hand it has a cleaner syntax than the former, and does not require checking hash keys as the later (the later is still useful for methods that want to receive an arbitrary list of named arguments). It would change a bit of the syntax of method_missing and how to deal with varargs and blocks for each parameter, but these can be dealt with as Scala does with its parameter lists.
  • There is no method called (). Procs (who are the obvious beneficiaries of such change ) use the method call, but one could be an alias to the other. There would be a mild ambiguity here, when you call the function that was just returned. For instance, imagine func is a method that receives no arguments and returns a function (that is: an object that implements the () method) wich also receives no arguments. Then func() could mean 1. call func, and 2. call the function returned by func. I am aware this is kinda of a sensitive topic, but the Scala approach to this issue is very simple: func and func() are the same (provided func can be called with no parenthesis). If you want to call the returned function in the same expression, use func()(). With the alias, you’d be able to call it like func.call() , func().call() or even func.call
  • No way to create simple blocks. It would be nice to have something similar to Scala’s underscore or Groovy’s it, which allows method invocations like: collection.map(get_square_of(_)) in Scala and collection.map(get_square_of(it)) in Groovy. Ruby’s symbol coercion to proc (&:method) does not really work on anything besides methods of the arguments of the block.
  • Difficulties of composing callback methods. You could be sure to always invoke super on them, and even meta-program all the classes/objects that do not do such thing. However, it is not easy to actually see which methods will be called, or even manipulate/re-prioritize the blocks of code on runtime (kinda like a Chain of Responsibility), which can be very bad, as these methods can modify a lot of behaviour throughout the Object Space.
  • The reflection API could be more thorough. For instance, you can’t get the source code of a method/class, etc (as you can in Python). You can use Parse Tree and Ruby2Ruby to do it, but Parse Tree is not portable (does not even work on ruby 1.9) and the output can be formated differently than the actual source code (which can be critical on DSLs). Also, methods added do not have information on which line of code they were added (which is less important when adding methods the recommended way: extending/including Modules), and properties created with class methods (such as those created by attr_reader, or some other libraries equivalents) can’t be discovered on runtime (they are like any other method, with no other meta-data whatsoever). Ruby also seems to be missing some helper methods, such as #metaclass.
  • No support for immutability. This is kinda nitpicking, but using recursive freeze (as noted by Dean Wampler) is not really practical (as it is really slow). Neither does it encompass immutable local variables. This is not only useful for concurrency and functional programming issues, but is also useful when writing code that is side-effect free so that it is easier to reason about.
  • The return value of a setter method (that is: one that ends with the equals symbol) is the argument, not the return value of the method. This is an issue that matters more when using immutable objects, as the only way for them to “mutate” is to return a new object. Therefore you can’t use a setter method on an immutable object, as, even if it returns a new one, the runtime will ignore the return value and set to the variable the argument that was received. On the other hand, I don’t think this can be changed without breaking a lot of existing code.

Several of this annoyances can be solved with a heavy dose of open classes, s-expressions manipulation (using Parse Tree) and meta-programming in general. Knuth has said that: Language designers also have an obligation to provide languages that encourage good style, since we all know that style is strongly influenced by the language in which it is expressed. Fully agreeing with such Sapir-Whorf-esque sentence, I feel it would be a good thing if the underlying listed solutions were built into the language itself (and supported across implementations, such as JRuby, Iron Ruby, Rubinius, Maglev), as it would not only improve, even if a little bit, the language itself, but also they way its users write code.

Update: Thanks Michael Fellinger for noting that ruby blocks are fully adherent to method definitions on 1.9, as they allow both default parameters and blocks as arguments.

Advertisements

21 Responses

Subscribe to comments with RSS.

  1. […] This post was mentioned on Twitter by kicauan and Thomas Buck, Hacker News. Hacker News said: Improving Ruby: http://bit.ly/bkrbUF Comments: http://bit.ly/dCCUXh […]

  2. Guyren Howe said, on April 1, 2010 at 1:39 am

    Another thing is that it would be very useful to call a simple function with declared arguments with a hash. I believe this is why Rails for example resorts to passing state around in instance variables.

    It seems reasonable to want something like this:

    def f(a, b, c)

    end

    h = {a: 1, b: 2, c: 3}

    f.call_with_hash h

  3. Kurtis Rainbolt-Greene said, on April 1, 2010 at 2:17 am

    There are some aesthetic changes that could be made as well:

    The keyword “def” to “method”, similar and inline to “class” and “module”:

    method find_file x, y, z

    end

    The keyword “elsif” to “otherwise” or “elseif”.

    And so on. These sort of readability issues could really improve the use of Ruby by “ordinary people”.

  4. Chris Wanstrath said, on April 1, 2010 at 5:48 am

    Really great post. I agree with pretty much every point on here — especially, after doing Objective-C, File.fn match: ‘*’ path: ‘/’ flags: File::FNM_PATHNAME

  5. Jeff said, on April 1, 2010 at 6:13 am

    You might be interested in trying out Clojure. Just about all of these features you request could be added through macros, so you could do it yourself and try it out without requiring changes to the parser, language runtime, VM, etc… Without the ability to define new syntactic constructs like you can with macros, I don’t see how a language will be able to keep up as things continue to evolve faster and faster. Ruby is moving too slow.

    • Daniel Ribeiro said, on April 1, 2010 at 11:59 am

      Thanks Jeff for the comment. I’ve been aware of Rich Hickey’s wonderful job with Clojure for a while now, but Ruby does have the ability to make macros with Parse Tree, and I even suggested having it as a native library, not only as a hack for MRI. However, Clojure’s syntax is far simpler than Ruby’s (due to the huge difference of keywords on both languages) and therefore it is easier to manipulate the AST in the former than it is in the later.

      Granted that, not everything I mentioned can be changed with Macros, mostly the Smalltalk/Objective-c’s (which inherited from Smalltalk, but I digress) way of method invoking, as it is not actually valid ruby syntax (it would be like using macros to replace parenthesis with indentation on Clojure).

      And ruby does not need macros everywhere, you can do a lot of things you need macros in Clojure with open classes and other meta-programming facilities in ruby, which are far easier to use and reason about.

      And the biggest problem with this whole ast manipulation is their composition: applying three different manipulations you have not wrote yourself to the same code can have quite surprising effects. But Paul Graham’s On Lisp discuss this issue much better.

      In the end, using all this meta-programming effect for simple things kinda breaks all the benefits tools could have (refactoring, completion, type inference, etc) for what I consider very basic questions on language. You should be able to trade the tools for power, and in fact you are, but I think it should be only needed on things bigger than a method invocation

      • Erik said, on December 22, 2012 at 7:14 pm

        Personally, I think Lisp style macros are far easier to reason about than hacking around with runtime class/instance evaluation and such. Macros don’t interfere with tools in my experience. Typed racket has type inference, and I use completion all day long in Emacs with slime.

        As for refactoring, that’s what macros are for. Have a bunch of ugly code that you can’t avoid writing? Wrap it up into a macro (or function, if possible).

        And of course, you could replace parenthetical syntax with reader macros in Common Lisp, but it’s usually not done.

  6. Matthew said, on April 1, 2010 at 7:02 am

    I believe the solution to your “No way to create simple blocks.” problem is:
    collection.map(&method(:get_square_of))

    • Daniel Ribeiro said, on April 1, 2010 at 12:11 pm

      Great hint Mathew! However, it somehow does not feel as simple as collection.map(get_square_of _).

      Upudate: thanks Michael Fellinger for noting that the coercion of symbols to procs does in fact allow more than one arguments. See comment below, with the example of &:+

  7. Michael Fellinger said, on April 1, 2010 at 1:31 pm

    [1,2,3].reduce(:+) # => 6

    lambda{|a=1| a }.call # => 1

    lambda{|a=1| a }.call(2) # => 2

    lambda{|a=1| a }.(3) # => 3

    RUBY_DESCRIPTION # => “ruby 1.9.1p378 (2010-01-10 revision 26273) [x86_64-linux]”

    • Daniel Ribeiro said, on April 1, 2010 at 2:07 pm

      I really appreciate for the heads up. Updated the post to remove the point, as the whole issue is solved on 1.9

  8. Michael Fellinger said, on April 2, 2010 at 3:54 am

    Also note the last example, there is a syntax for `()` on procs, but it’s used with `.()`, just like other methods (although you cannot get at it like other methods).
    `[]` is also an often-used alias for `Proc.call`, but it turned out to be confusing that something commonly used for Array/Hash access now executes other code.

    • Daniel Ribeiro said, on April 2, 2010 at 1:28 pm

      Thanks for the notes Michael. But the issue of “()” instead of “.()” or even “[]” may seem like a small deal, but it is actually about readability and invocation transparency. With it, you can switch from using a reference for a method without actually changing the code. For instance, if get_square_of is a variable pointing to a function, you can later on remove it and start using a method called get_square_of, without having to change the calling syntax.

      This is also important when you want to wrap a function with a object, and want this object to be duck type compatible (what smalltalkers call implementing the same interface) with the wrapped function.

      It is a step to making functional programming more natural in ruby.

  9. roger said, on April 2, 2010 at 1:08 pm

    Note that you can have named arguments (by using a library that uses parse-tree or ruby_parser)

    http://github.com/rdp/arguments

    and you can get the code of methods by using the ri_for gem (in 1.9–using Method#source_location)

    http://github.com/rdp/ri_for

    >> require ‘ri_for’
    >> require ‘pathname’
    >> Pathname.

    >> Pathname.ri_for :glob
    at C:/Ruby19/lib/ruby/1.9.1/pathname.rb:965
    See Dir.glob. Returns or yields Pathname objects.
    sig: Pathname.glob arity -1
    def Pathname.glob(*args) # :yield: p
    if block_given?
    Dir.glob(*args) {|f| yield self.new(f) }
    else
    Dir.glob(*args).map {|f| self.new(f) }
    end
    end

    You will be able to get the metaclass in 1.9.2 using #singleton_class (recently added).

    also note that with 1.9 you can call procs with #call http://eigenclass.org/hiki.rb?Changes+in+Ruby+1.9#l10

    For your other changes, bring them up on ruby talk and ruby core–see if you can make a difference :)

    -rp

    • Daniel Ribeiro said, on April 2, 2010 at 2:58 pm

      I really appreciate the discussion roger. I’ll comment more in details your points:

      1. named arguments: I said named arguments as a feature somewhat forward to what ruby has, but not really useful when you have method call as in smalltalk’s. This is because the “named” part of named arguments are really part of the method. From the example above: File.fn match: ‘*’ path: ‘/’ flags: File::FNM_PATHNAME.

      In this case, the method name would be fnmatch:path:flags: (probably some other convention on the underlying implementation, as this is not a valid ruby symbol)

      2. ri-for and Parse Tree: I mentioned that Parse Tree/Ruby2Ruby can be used to get the source, but it is good to know that there is an alternative on 1.9. The issue with the former is that it is not portable across implementations and it ignores the original formatting. The problem with the later is that it does not encompass metaprogrammed methods (or even meta-meta programmed method, such as methods created by methods created with meta programming). This is really a problem when composing recompilations/instrumentations of the same method (in a lispy way of macro-type metaprogramming).

      3. Glad to hear that there is an effort in making the reflection api more complete.

      4. Michael Fellinger mentioned the ‘.()’ above, and I commented on it.

      5. I really did not expect this post to receive such a response. The intention of this post was more of an aggregation of a personal wish-list of all things, and the common motif behind them. This is why I did not bring them on a proper forum, where they’d need to be scattered around. But I’ll consider the suggestion.

  10. Top Posts — WordPress.com said, on April 2, 2010 at 9:14 pm

    […] Improving Ruby Some may say Ruby is a bad rip-off of Lisp or Smalltalk, and I admit that. But it is nicer to ordinary people. – […] […]

  11. Daniel said, on April 5, 2010 at 5:20 pm

    I guess the parens would be required, thus introducing a special case? (Otherwise you can’t pass the proc around without calling it.) And does a special case like that really improve readability?

    • Daniel Ribeiro said, on April 5, 2010 at 5:53 pm

      Only for zero arg or variable arguments (accepting a zero argument possibility) procs. The improvement in readability might be worth it. In a more functional style of programming, where functions are first order citizens, you cannot avoid the possiblity of ambiguity. If you always require the delimiter (parenthesis is the most common), then it goes away. If not, then you have to solve the zero arg problem. If you don’t want the parenthesis, you can awlays invoke the old style: ‘.()’ or ‘.call’. However, there would be a mismatch in zero-arg function call and zero arg-method-call. Which is already happens today, but in the current form the mismatch occurs on function calls with one or more arguments as well.

  12. roger said, on June 3, 2010 at 7:57 pm

    True that meta programmed methods are unavailable. You can get their iseqs though, using rubyVM::disassemble or something like that, which gives you a clue as to what they do :)

    • Daniel Ribeiro said, on November 1, 2010 at 10:48 pm

      Nice, Will look into it. Perhaps it will be possible to have a gem that will encapsulate all this mri/jruby/yarv incompatibilities and offer a single unified sane interface.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: