A personal introduction to Ruby with ruby koans

Having some free time in my hands, I wanted to try Ruby.
I was reading some RoR code, a couple of years ago, but I have never worked with it. To be honest, it did not seem that interesting: more like a messy spaghetti code, but it’s very possible that was a specific codebase issue.

So, I want to learn it better.

To begin, I installed it using rbenv – essentially because I was already used to something similar, with pyenv.
However it’s different: in pyenv I was used to

pyenv virtualenv 3.12 project_name

and then to manually create the .python_version file – usually by copying from another one. With Ruby, instead, I have to simply select the environment, and it automatically writes the .ruby_version file; but there is no virtual environment concept. At least, not that I know about.

brew install rbenv ruby-build
rbenv local 3.3.0

Then I started the koans training. I am not sure that I fully like it, but I am following the flow.
Most of the times, Ruby is very similar to Python, with few differences.
In general, ruby’s philosophy seems to favor flexibility and “expressivity”, much more than python. There are pro and cons in both aspects; but for sure, python seems “smaller” and easier to manage.
There are also a bunch of specific exceptions that I am going to list here – note however that this is the list coming from the koans, I am pretty sure that a complete list is much larger.

Naming

Ruby allows some conventions to make the code a bit easier to read. For instance,the convention for boolean methods is to end them with ?. However, in some cases the naming take a different weight, and names starting with an uppercase letter define a constant value.

True/False

In python, the false condition is a very common shortcut, as quite a lot of things are falsey; this is not true in ruby, where essentially only nil values evaluate as false.

Strings

In python, there is no difference between " and ' to define a string. Usually, most people will use ", but python core (and debuggers and so on) seem to use '. Ruby follows the PHP approach, where the " strings have a richer behavior on their initialization. I like this approach a bit more, because it leaves less room for discussions on coding style :)
Another thing I really like, is how regular expressions are part of string methods, and they even look like “normal” ranges!

"abbcccddddeeeee"[/ab*/]  # => "abb"

Surprisingly, strings are mutable, e.g. using shovel operator. This is quite a difference from python; I actually like strings being immutable, so this might lead me to write a bit less idiomatic ruby code.
As a side effect, while in python short strings are interned, and it’s possible to mark a string as interned, ruby strings are never interned (note that other objects, for instance for small integers, are interned).

Symbols

Well, python does not have symbols. hey seem quite powerful and the right compromise of flexibility of a string and the type safety of an enum. I like them, and I need to get used to them.
It’s also interesting the equivalence between names (e.g. methods) and symbols: for instance, you can declare a method as private by accessing its name as a symbol:

class A
  def method_name
    42
  end
  private :method_name
end

Arrays and Ranges

Arrays are somewhat similar to python lists, even if I suspect their implementation might be more similar to a C array.
Ranges are a bit weird; especially, the syntax array(n, m) is a bit confusing to me, but I think I just need to get used to it.
Unpacking is nicer than in python; python is a bit more rigorous, but ruby is simply more practical. I like that.
Of the examples, I don’t really understand why, of an array of n elements, sometimes asking the n+1 does not give nil:

a = [1, 2]  # => [1, 2]
a[0]        # => 1
a[1]        # => 2
a[2]        # => nil
a[1, 1]     # => [2]
a[2, 1]     # => []
a[3, 1]     # => nil

This is simply baffling.

Looping

As usual, ruby is here richer than python. For instance, not only you can loop over a sequence using a for loop; you can achieve the same result in a more “functional” way using the Array.each method, pretty much like in PHP.
This makes also other functional loops a bit nicer than in python; so instead of list comprehensions you can use select (aka find_all) map.
For iterating a number of times python forces using the awkward range construct, here you can instead use the Integer.times method, leading to muhc nicer code:

sum = 0
10.times do
  sum += 1
end

Blocks

Blocks are… well… blocks of code. They are perhaps the most interesting part of ruby, because they double as a kind of anonymous functions: a method can receive a block as a parameter, and then use yield to call it. This is a very powerful concept, opening a new style of coding.

There are few ways to pass around code:

  • as a block: with do ... end or with curly brackets{ |n| n + 1 }. -> In this case, we just pass the block: some_method { |n| n + 1 }
  • as a variable, containing a lambda: a = lambda { |n| n + 1 }/a = ->(n) { n + 1 }. -> In this case, we should pass a “reference” to it: some_method(&a)
  • as a method, and here we follow essentially the same rules as with lambdas.

Note that we can also explicitly declare the block as a parameter of the method; in this case, there is no longer need to use yield:

def method_with_block
  yield 10
end

def method_with_explicit_block(&block)
  block.call(10)
end

Note the & when declaring the block variable!

Classes and objects

Although they express a very similar concept, objects in ruby are somewhat different from python. For instance:

  • Instance variables are invisible from outside; you need to use instance_variables (and its various similar methods) to access them. To partially mitigate this “annoyance”, there’s some sugar to easily create accessors, using attr_reader and attr_accessor.
  • to_s and inspect are the equivalent of __str__ and __repr__. I think I like more the dunder approach in python.
  • It’s possible to “open a class”, and extend/override its behavior:
    # Define the class
    class A
      def m1; 42 end
    end
    
    # Open the class and alter its behavior (or add new methods)
    class A
      def m1; 41 end
    end
      
    # Can do with global classes as well!
    class ::Integer
      def m1; 42 end
    end
    

    In python this is usually something requiring heavy monkeypatching, and never that easy; also, for some core classes (e.g. integers) that’s not possible at all.
    This flexibility seems definitely interesting, but also definitely opens new ways to shoot yourself in the foot. In a mature team, that should not be really a problem, though.

  • Classes can include modules, which is essentially a very, very cool way to implement traits. I like it a lot!
  • You can dynamically add methods on classes and objects. However, methods defined on the class are not visible by the object instances!
    class SomeClass
      def some_method; 42 end
    end
    
    def SomeClass.some_method; 0  end
    
    SomeClass.some_method      # => 0
    SomeClass.new.some_method  # => 42
    
  • There is the whole class << self way to define methods. I haven’t fully understood it, and it seems just an additional, more complicated way, to express the same thing as the normal method definition in a class. Most probably, it’s related to extending the class outside its definition, something like:
    class SomeClass; end
    
    class << SomeClass
      def some_method; 42 end
    end
    

    However, I am not convinced this is the reason for this approach, and I am sure something is missing in my mental model and I will understand it better later…

  • Calling a method dynamically is much easier in ruby, as you just need to use call. As well, it’s easier to create some dynamic “catchall” methods: where in python you need to play with __getattr__, here you have more convenient method_missing (called when a method is missing on the object), respond_to? (telling if a method exists), as well as respond_to_missing? (which, as described here, solves some usecase I am not familiar yet…)