The Next Ruby

DZone 's Guide to

The Next Ruby

In this article, the author suggests several ways to improve the Ruby language.

· Web Dev Zone ·
Free Resource

Ruby turned out to be the most influential language of the last few decades. In a way that's somewhat surprising, as it didn't come up with that many original ideas — it mostly extracted the best parts of Perl, Lisp, Smalltalk, and a few other languages, polished them, and assembled them into a coherent language.

The Great Ruby Convergence

Nowadays, every language is trying to be more and more like Ruby. What I find most remarkable is that features of Perl/Lisp/Smalltalk which Ruby accepted are now spreading like wildfire, and features of Perl/Lisp/Smalltalk which Ruby rejected got nowhere.

Here are some examples of features which were rare back when Ruby got created:

  • Lisp - higher order functions - Ruby accepted, everyone does them now
  • Lisp - everything is a value - Ruby accepted, everyone is moving in this direction
  • Lisp - macros - Ruby rejected, nobody uses them
  • Lisp - linked lists - Ruby rejected, nobody uses them
  • Lisp - s-expression syntax - Ruby rejected, nobody uses them
  • Perl - string interpolation - Ruby accepted, everyone does them now
  • Perl - regexp literals - Ruby accepted, they're very popular now
  • Perl - CPAN - Ruby accepted as gems, every language has it now
  • Perl - list/scalar contexts - Ruby rejected, nobody uses
  • Perl - string/number unification - Ruby rejected, nobody uses them except PHP
  • Perl - variable sigils - Ruby tweaked them, they see modest use in Ruby-style (scope indicator), zero in Perl-style (type indicator)
  • Smalltalk - message passing OO system - Ruby accepted, everyone is converging towards it
  • Smalltalk - message passing syntax - Ruby rejected, completely forgotten
  • Smalltalk - image based development - Ruby rejected, completely forgotten

You could make a far longer list like that, and correlation is very strong.

By using Ruby you're essentially using future technology.

That Was 20 Years Ago!

A downside of having a popular language like Ruby is that you can't really introduce major backwards-incompatible changes. The Python 3 release was very unsuccessful (released December 2008, today it's about an even split between Python 2 and Python 3), and Perl 6 was Duke Nukem Forever level fail.

Even if we knew for certain that something would be an improvement, and usually there's a good deal of uncertainty before we try. But let's speculate on some improvements we could do if we weren't constrained by backward compatibility.

Use Indentation Not End

Here's some Ruby code:

class Vector2D

  attr_accessor :x, :y

  def initialize(x, y)

    @x = x

    @y = y


  def length

    Math.sqrt(@x**2 + @y**2)



All the ends are nonsense. Why can't it look like this?

class Vector2D   
  attr_accessor :x, :y   
  def initialize(x, y)     
    @x = x     
    @y = y   
  def length
Math.sqrt(@x**2 + @y**2)

It's much cleaner. Every lexical token slows down code comprehension. Not character - it really makes no difference between endvs }, but all the extra tokens need to be processed even if they're meaningless.

Ruby dropped so much worthless crap like semicolons, type declarations, local variable declarations, obvious parentheses, pointless return statements etc., it's just weird it kept pointless end.

There's minor complication that chaining blocks would look weird, but we can simply repurpose {} for chainable blocks, while dropping end:

ary.each do |item|
  puts item



  item.price > 100




  puts name


This distinction is fairly close to contemporary Ruby style anyway.

If you're still not sure, HAML is a Ruby dialect which does just that. And Coffeescript is a Ruby wannabe, which does the same (while going a bit too far in its syntactic hacks perhaps).

Autoload Code

Another pointless thing about Ruby is all the require and require_relative statements. But pretty much every Ruby project loads all code in a directory tree anyway.

As Rails and rspec have shown - just let it go, load everything. Also make the whole standard library available right away - if someone wants to use Set, Pathname, URI, or Digest::SHA256, what is the point of those requires? Ruby can figure out just fine which files are those.

Files often depend on other files (like subclasses on parent classes), so they need to be loaded in the right order, but Rails autoloader already solves this problem.

That still leaves out files which add methods to existing objects or monkeypatch things, and they'll still need manual loading, but we're talking about 1% of use cases.

Module Nesting Needs to Die

Here's some random Ruby code from some gem, rspec-expectations-3.5.0/lib/rspec/expectations/version.rb:

module RSpec
  module Expectations
    module Version
      STRING = '3.5.0'

That's an appalling ratio of signal to boilerplate. It could seriously be simply:

module Version

  STRING = '3.5.0'

With the whole fully qualified name being simply inferred by autoloader from file paths.

The first line is technically inferrable too, but since it's usually something more complex like Class Foo < Bar, it's fine to keep this even when we know we're in foo.rb.

Module Nesting-based Constant Resolution Needs to Die

As a related thing - constant resolution based on deep module nesting needs to die. In current Ruby:

Name = "Alice"
module Foo
  Name = "Bob"

module Foo::Bar
  def self.say_hi
    puts "Hi, #{Name}!"

module Foo
  module Bar
    def self.say_hello
      puts "Hello, #{Name}!"

Foo::Bar.say_hi     # => Hi, Alice!
Foo::Bar.say_hello  # => Hello, Bob!

This is just crazy. Whichever way it should go, it should be consistent - and I'd say always fully qualify everything unless it's in the current module.

New Operators

Every DSL is abusing the handful of predefined operators like <<, [], and friends.

But there's seriously no reason not to allow them to create more.

Imagine this code:

class Vector2DTest

  def length_test

    v = Vector2D.new(30, 40)

    expect v.length ==? 50

That's so much cleaner than assert_equal or monkeypatching == to mean something else.

I expect that custom operators alone would go halfway through making rspec style weirdness unnecessary.

Or when I have a variables representing 32-bit integers for interfacing with hardware, I want x >+ y and x >! y for signed and unsigned comparisons instead of converting it back and forth with x.to_i_signed > y.to_i_signed and x.to_i_unsigned > y.to_i_unsigned.

This obviously will be overused by some, but that's already true with operator overloading, and yet everybody can see it's a good idea.

We don't need to do anything crazy - OCaml is a decent example of fairly restrictive class of operator overloading that's still good enough - so any operator that starts with + parses like + in expressions etc., and parsers don't need to be aware of which library it uses.

a +!!! b *?% c would always mean a.send(:"+!!!", b.send(:"*?%", c)), regardless of those operators meaning anything or not.

Real Keyword Arguments

Ruby hacks fake keyword arguments by passing extra Hash at the end - it sort of works, but really messes up more complex situations, as Hashes can be regular positional arguments as well. It will also get messed up if you modify your keyword arguments, as it will happily modify Hash in the caller.

We don't check if last argument is a Proc, we treat them as a real thing. Same should apply to keyword arguments.

Ruby is currently built around send operation:

object.send(:method_name, *args, &block_arg)

We should make it:

object.send(:method_name, *args, **kwargs, &block_arg)

It's a slightly incompatible change for code that relied on a previous hacky approach, and it makes method_missing a bit more verbose, but it's worth it, and keyword arguments can help clean up a lot of complex APIs.

Kill #to_sym #to_s Spam

Every codebase I've seen over last few years is polluted by endless #to_sym / #to_s, and hacks like HashWithIndifferentAccess. Just don't.

This means {foo: :bar} syntax needs to be interpreted as {"foo" => "bar"}, and seriously it just should. The only reason to get anywhere close to Symbols should be metaprogramming.

The whole nonsense got even worse than Python's list vs tuples mess.

Method Names Should Not Be Globally namespaced String

This is probably the biggest change I'd like to see, and it's somewhat speculative.

Everybody loves code like (2.hours + 30.minutes).ago because it's far superior to any alternatives, and everybody hates how many damn methods such DSLs add to common classes.

So here's a question - why do methods live in global namespace?

Imagine if this code was:

class Integer

  def time:hours


  def time:minutes


  def time:ago

    Date.now - self

And then:

  (2.time:hours + 30.time:minutes).time:ago

This would let you teach objects how to respond to as many messages as you want without any risk of global namespace pollution.

And in ways similar to how constant resolution works now with include you could do:

class Integer

  namespace time

    def minutes


    def hours


    def ago

      Date.now - self

And then:

  include time

  (2.hours + 30.minutes).ago

The obvious question is - how the hell is this different from refinements? While it seems related, this proposal doesn't change object model in any way whatsoever by bolting something on top of it - you're still sending messages around - it just changes object.foo() from object.send("foo".to_sym) global method namespace to object.send(resolve_in_local_lexical_context("foo")), with resolution algorithm similar to the current constant resolution algorithm.

Of course, this is a rather speculative idea, and it's difficult to explore all consequences without trying it out in practice.

Unified Matching/Destructuring

Here's a feature which a lot of typed functional programming languages have, and which Ruby sort of has just for  Strings and regular expressions - you can test for a match and destructure in a single expression:

case str

when /c:[wubrg]/

  @color = $1

when /t:(\S+)/

  @type = $1

Doing this kind of matching on anything else doesn't work because $1 and friends are some serious hackery:

  • $1 and friends are accessing parts of $~ - $1  is $~[1] and so on.
  • $~ is just a regular local variable - it is not a global, contrary to $ sigil.
  • =~ method sets $~ in caller's context. It can do it because it's hacky C code.

Which unfortunately means it's not possible to write similar methods or extend their functionality without some serious C hacking.

But why add a mechanism to set caller $~, and then we could create our own matchers:

case item

when Vector2D

  @x = $~x

  @y = $~y

when Numerical

  @x = $0

  @y = $0

To be fair, there's a workable hack for this, and we could write a library doing something like:

case s = Scanner(item)

when Vector2D

  @x = s.x

  @y = s.y

when Numerical

  @x = s.value   @y = s.value

And StringScanner class in standard library which needs just a tiny bit extra functionality beyond what String / Regexp provide goes this way.

But even that would still need some kind of convention with regards to creating scanners and matchers - and once you have that, then why not take one extra step and fold =~ into it with shared syntactic sugar?

Let the Useless Parts Go

Here's an easy one. Ruby has a lot of crap like @@class_variables, protected visibility (pop quiz: what it actually does, and how it interacts with method_missing), Perl style special variables like $=, method synonyms like #collect for #map, flip flop operator, failed experiments like refinements etc.

Just let it all go.

Wait, That's Still Ruby!

Yeah, even after all these changes the language is essentially Ruby, and backward incompatibility shouldn't be that much worse than Python 2 vs 3.

nesting ,perl ,ruby ,web dev

Published at DZone with permission of Tomasz Wegrzanowski , DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}