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, usingattr_reader
andattr_accessor
. to_s
andinspect
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 convenientmethod_missing
(called when a method is missing on the object),respond_to?
(telling if a method exists), as well asrespond_to_missing?
(which, as described here, solves some usecase I am not familiar yet…)