DEV Community

Phillip Wright
Phillip Wright

Posted on • Originally published at sixbitproxywax.com on

Initial Thoughts On Ruby

I had never had a compelling reason to learn Ruby, but the last semester I taught my Scripting Languages course, I wanted to extend the breadth of coverage to include a wider variety of languages. I also try to make sure that my own breadth of knowledge in the area of programming languages stays up to date, so I finally decided to start learning Ruby. This is a summary of my initial impressions of the language.

Full Disclosure

Before sharing my thoughts on Ruby, I should first mention that my _a priori_view of Ruby was that it was a giant trash fire. Most of my professional career I have worked with statically typed languages like Java and C#, and my free time interests are aimed in the direction of functional programming languages like Scala and Haskell. Accordingly, I acknowledge that I may be biased.

At the same time, Python is the first language that I truly learned to program with, and I still consider it to be a very nice language (relatively speaking). In other words, a lack of static typing is definitely going to illicit an impulsive, negative response from me, but trash fire status requires greater sins in my book.

First Steps

Once I set out to learn Ruby, I immediately read that it was meant to be an improvment on Perl. This was a huge red flag for me, personally, because basing a language on Perl sounds like the worst idea possible. Ultimately, my analysis of Ruby started to resemble my general view of Perl, which is not a good thing.

TIMTOWTDI

Incorporating the Perl ethos that “there is more than one way to do it” ( TIMTOWTDI ), Ruby provides what is, in my opinion, a tragically large number of ways to do even the most simple things.

In some sense, all languages have many ways to accomplish the same end goal, but in languages like Perl and Ruby the issue is the wide variety of redundant primitives_and _syntax.

Branching

For instance, if we simply want to branch, what should we do? Like most mainstream programming languages, we are provided with the rather pedestrian if statement

if x % 2 == 0
    puts "even"
else 
    puts "odd"
end

We are already running into a minor issue, though, if we want to branch on one line, because there is no delimiter for the beginning of the code block. Accordingly, if we want to write a one line if statement, we have to add the keyword then.


if x % 2 == 0 then puts "even" end

Having to add this delimiter is not really a huge issue, but it seems to add an unnecessary level of difficulty to the language to make the delimiter optional. (This unnecessary addition of mental overhead will be a common theme in this summary.)

Ruby also adopts the Perl idea of suffixing statements with a branch condition as well

puts "even" if x % 2 == 0

The argument for such constructs (at least in the Perl community) is that providing these variations allows you to express concepts in a fashion more similar to a natural language. However, it doesn’t seem convincing to me that this actually makes programs any easier to understand or to write.

unless

Adding to this, Ruby also imports Perl’s complementary branching statement unless. Using this we could write

unless x % 2 == 0
    puts "odd"
end

Of course, we can also write this as a suffixed branching statement

puts "odd" unless x % 2 == 0

So now, instead of simply requiring a developer to learn a single construct, we instead require an understanding of four different flavors of this simple concept.

Hashes

The creation of hashes in Ruby provide another interesting example of this problem. When learning Ruby, you may encounter the_hash rocket_ style of hash initialization

colors = {'green' => 'verde', 'black' => 'preto'}

where the hash rockets are the arrows which pair the corresponding keys to values.

It seems, though, that some people decided hashes should be more JSON-like, so you may also encounter the following syntax

colors = {green: 'verde', black: 'preto'}

which is nothing more than syntactic sugar for

colors = {:green => 'verde', :black => 'preto'}

You may note that we use :green instead of ‘green’, because this special syntax only works with keys which are symbols and can not be used with strings or any other type (a great example of Ruby’s tendency to provide _almost interchangeable, but not really_language structures).

Blocks

An even more bizarre case of TIMTOWTDI in Ruby is the notation for Blocks, which are a riff on the concept of anonymous functions (one of the three flavors of anonymous function like things which Ruby provides, of course!)

When using iterator methods which require a code Block, you can write them in one of two ways. You can use curly braces

10.times { |x| puts x }

or you can use a do Block

10.times do |x| puts x end

In other words, Ruby provides two semantically identical constructs that differ only in the lexemes of the syntax! You may also note that, even though Ruby methods use parentheses to delimit parameter lists, Blocks delimit parameters with pipes.

Blocks, Procs, and Lambdas

Ruby suffers from the TIMTOWTDI illness, but it goes well beyond TIMTOWTDI when we get to the topic of Blocks, Procs, and Lambdas which are not so much “more than one way to do” the same thing as they are “more than one way to almost do” the same thing (with potentially disasterous results).

Blocks

We already saw the use of Blocks which are passed in as the final parameter to iterator methods. For instance, to increment all of the items in a list, we can do the following

[1,2,3,4].each do |x| x + 1 end

This example may seem familiar to you based on your prior programming experience and lead you to believe that a Block is the Ruby equivalent of a lambda expression.

Of course, that would be way too easy!

In reality, Blocks can only be used as parameters to iterator methods and are not first class objects (they actually seem to be the only thing in Ruby that is not an object.)

Procs

If we feel the desire to return a Block or to assign a Block to a variable, we have to instead use a Proc, which seems to essentially be a wrapper around a given Block. To create a Proc, we just pass a Block to the Proc constructor

p = Proc.new do |x| x + 1 end

That’s right. Instead of simply making Blocks inherently first class objects, you have to pass the Blocks to another object.

So, Procs are our beloved lambdas in Ruby?

Of course not!

Lambda

Ruby provides a third construct for lambdas which can be written as

l = lambda {|x| x + 1}

or, naturally, as

l = lambda do |x| x + 1 end

At this point, it would make sense to believe that lambdas and Procs are simply another TIMTOWTDIesque lexeme swap, but this is not the case.

It turns out that lambdas actually are Procs with subtle, but critical differences which prevent them from being treated as interchangeable concepts.

Procs vs Lambdas

The first difference is silly, but not an absurd difference: Procs do not care how many arguments you pass to them, while lambdas will throw an error if you provide the wrong number of parameters

p = Proc.new do |x| x + 1 end
l = lambda do |x| x + 1 end

p.class => Proc
l.class => Proc

p.call(1,2,3) # ok!
l.call(1,2,3) # error!

The second major difference is the return semantics of Procs and lambdas. For instance in the following code

def test_method
    l = lambda do return end
    l.call
    puts "this will actually print"
end

test_method

the statement printing to the console will actually execute as expected, because the return statement in the lambda expression causes the flow of control to continue at the the point immediately following the initial call. However, in the following code

def test_method
    p = Proc.new do return end
    p.call
    puts "I never get printed"
end

test_method

the line printing to the console never gets executed, because the return statement in the Proc returns to…somewhere.

Many posts online seemingly imply that the return in the Proc will cause the block from which it was called to return, but the following experiment

def test_method
    p = Proc.new do return end
    l = lambda do
        p.call
        puts "in lambda"
     end
    l.call
    puts "do I print?"
end

test_method

will not print anything to the console which would seem to indicate that the return statement pops everything off the stack from the context where the Proc is defined , however deeply nested the call might be! (I haven’t really confirmed the exact behavior yet, though.)

Indeed, if you define the process as the top level of your code in the Ruby interpreter, calling it (at any other location in your code) will attempt to escape the interpreter and throw an exception.

return is optional(ish)

To add to this confusion, Ruby has the reasonable semantics of a method returning the value of the last expression that was executed in the absence of an explicit return. Why is this confusing? Because the difference in return semantics for Procs and lambdas which was described above only holds when an explicit return is used! In other words, the following code

def test_method
    p = Proc.new do end
    l = lambda do
        p.call
        puts "in lambda"
     end
    l.call
    puts "do I print?"
end

test_method

will result in both lines being printed to the console, simply because we did not include an explicit return statement.

So remember, the return keyword in Ruby is optional… except when it isn’t.

Conclusion

Of course, this doesn’t go into all of the questonable design decisions made in Ruby, but I think it’s a decent selection of “features” which demonstrate why I still think Ruby is a trash fire.

Ruby does some things that are nice, like the way it allows you to provide Blocks as a final parameter to iterators in a way that seems more natural by providing currying like behavior

def curried(a,b, &f)
    f.call(a,b)
end

a = 5
b = 6

curried(a,b) do |x,y| x+y end => 11

This gives the feel of creating your own syntax when combined with iterators, etc.

However, my general feeling about the language is that someone took a bunch of concepts, put them in a bag and shook it up to create the language.

Most critically, there is not just more than one way to do many things, there are many unnecessary and non-beneficial ways to do many things, and subtle details which make it difficult to know with certainty if the different ways you might do something actually are doing the same thing.

In my opinion Ruby requires a developer to know too many things (especially for a language which many people choose to “get things done” quickly) without providing a worthwhile return on that learning investment.

Top comments (0)