The Next Ruby
The Next Ruby
In this article, the author suggests several ways to improve the Ruby language.
Join the DZone community and get the full member experience.
Join For FreeAccess over 20 APIs and mobile SDKs, up to 250k transactions free with no credit card required
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
end
def length
Math.sqrt(@x**2 + @y**2)
end
end
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
versus
ary.select{|item|
item.price > 100
}.map{|item|
item.name.capitalize
}.each{|name|
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'
end
end
end
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"
end
module Foo::Bar
def self.say_hi
puts "Hi, #{Name}!"
end
end
module Foo
module Bar
def self.say_hello
puts "Hello, #{Name}!"
end
end
end
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
60*self.time:minutes
def time:minutes
60*self
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
60*self
def hours
60*self.minutes
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.
#1 for location developers in quality, price and choice, switch to HERE.
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.linkDescription }}
{{ parent.urlSource.name }}