Ruby deviates from most conventional programming languages (esp. C family) in very interesting ways that are very non-intuitive to beginners.
While these choices appear to be outright weird to newcomers, they soon discover that these "gotcha"s are actually interesting innovations in many regards.
Lot of programming languages (including javascript, C# etc.) have features like properties with getters and setters which allow you
to intercept member assignment (someObject.property = "hello") and member access (const y = someObject.property) as method invocations.
In ruby, method invocation does not require parenthesis. Even when you have no arguments. So some_object.some_property is always an invocation of method some_property on object some_object. In addition some_object.some_property = 10 is always an invocation of method some_property= (yes = is allowed as a part of method name) on object some_object.
While this is quite confusing for people coming from javascript having first class functions where you would expect some_object.some_fn to return you an (unbound) function some_fn rather than executing that function, this has an interesting advanage: Ruby simply does not need a separate concept of properties.
Languages like python and javascript have special syntax for decorators. Languages like Java and C# have special syntax for annotations which allow you to associate some metadata with a method, class or property.
People coming to Ruby from these langauges are often surprised to find that no analogous features exist in Ruby. This is simply because Ruby's approach of dealing with classes makes them entirely unnecessary.
Ruby classes can contain arbitrary code that gets executed when the class is defined.
class SomeClass
puts "hello world"
endThe above will print "hello world" when the class is defined (not when instantiated).
The code inside a class can also access the members which have been defined so far. So you can do things like:
class SomeClass
extend Memoist # Include a third party module which will inject some static member functions
def some_method
#...
end
memoize :some_method # Use memoize function injected by Memoist to modify the behavior of some_method defined above
endThis is one of the most interesting (and pervasively used) aspect of Ruby. You can pass a block of code to a function.
def some_fn
some_object.some_other_fn {
puts "hello world"
}
endLanguages like Javascript allow you to pass anonymous functions to other functions, and newcomers to ruby from these languages mistakenly assume that blocks are an identical feature. They differ in a significant way - how returns are handled:
def some_fn
some_object.some_other_fn {
return 10
}
end
If some_other_fn chooses to execute the provided block then the return statement returns not only from the block but also from the outer function some_fn. Many people find this totally non-intutive, but this has a powerful consequence - it enables quick refactoring without re-structuring code.
So you can easily refactor out some extraneous "wrapping-logic" like:
def some_fn
connection = ConnectionPool.borrow_connection
connection.execute "some statement"
# ... some other code
if some_case
CollectionPool.return_connection connection
return
end
# ... some other code
if some_other_case
CollectionPool.return_connection connection
return
end
CollectionPool.return_connection connection
# ... some final logic
end
to something like:
def some_fn
ConnectionPool.borrow_connection do |connection|
connection.execute "some statement"
# ... some other code
if some_case
return
end
# ... some other code
if some_other_case
return
end
end
# ... some final logic
end
In the second implementation, all the return_connection invocations can be abstracted out inside the borrow_connection implementation itself and the consumer does not have to worry about the repetitive return_connection invocations (which can be easily missed out in branches).
Note that apart from that, the outer function (some_fn) does not have to be restructured and even though the returns have gone inside block, the last section ... some final logic will not be executed if a return is encountered.
This is arguably a much cleaner alternative than things like C# IDisposable because disposal of expensive resources can be done independent of garbage collection without requiring any extraneous effort from the developer.
More importantly, someone reviewing the code does not have to wade through all the return_connection invocations, in all edge cases, and focus on the primary logic of the function.
tl;dr Ruby is weird for a reason. Don't let familiarity bias hold you back.