I’m currently going through some job interviews and as a way to revisit and learn core fundamentals of the language, I’ve been trying to answer the questions from common ruby interview questions topic.
I thought in share my own answer to some of those questions and open them up for discussion to see what others have to say, hoping we can learn interesting concepts of the language.
so, as the title suggest, the question is:
what are some differences between ruby and other programming languages?
Funny, the lines have blurred over the years as the language has evolved, but I think there is a big categorical difference, essentially the dynamic/static, which used to mean “interpreted at runtime” vs “compiled to native code ahead of time” but… that’s not exactly right anymore.
Ruby now runs on a VM so it essentially does get compiled down to YARV bytecode, and then that representation runs. But, unlike Java or Python that persist their bytecode, ruby does not, so it is essentially auto-recompiled to bytecode each execution. And then you add on the JIT-compiled bit, and it gets even muddier because the VM will compile parts of the code to actual native code.
But I think there is still something important here related to portability and compilation, specifically getting down to native code (machine code, assembly).
just-in-time - Ruby code is portable across architectures and is only compiled at runtime. (Python similarly, though the pyc files are cached. Java is compiled to its own bytecode ahead of time, but then that is portable, and also gets JIT to native code in the VM, as ruby now does).
ahead-of-time - Some other languages that compile to native code ahead of time require compilation for a specific architecture. (C, Rust, Go)
Other differences I’d note:
Concurrency came late and is still developing.
No meaningful concept of ‘interface’.
It is so dynamic that the privacy keywords (protected, private) are more of a guide than a rule.
Those are great points, I would challenge a little the productivity/performance tradeoff because sometimes you can be more productive if the code is easier to understand and/or maintain for example. So if I was making the question I would use it to discuss that topic in more depth and also understand what’s your definition of productivity or what other tradeoffs you’ve seen with the language.
I would also add the following:
Follows “More than one way of doing something” philosophy because it’s optimized for programmer happiness and readability.
Multiple implementations available: Ruby MRI, Truffle Ruby, JRuby, mruby, etc.
Unit testing, TDD and other verification techniques are more widely adopted than in other more languages.
Maybe add more concrete example, like comparing each language’s key traits. For example, Node.js is non-blocking, while Elixir focuses on functional programming and immutability. It’d also help to show how each language solves the same problem.
My 2c: it is one of the very easiest languages to develop DSLs (Domain-Specific Languages) in. Some folks may be turned off by that, and the meta-programming stuff in general where the execution context of a block could be “anything” (thanks to instance_exec)…but I absolutely love it. I’m also a fan of modern JavaScript, ngl, but man do DSLs there look ugly, and there seems to be a bit of a cultural avoidance of utilizing strong OOP paradigms in general of which I am a huge fan.
One negative difference: I don’t like Ruby’s templating story these days. Heredocs feels weird and limited compared to JS’ tagged template literals, and ERB as a concept just feels old. I’ve tried to build out a superset of ERB called Serbea which introduces some additional syntactic sugar (for instance, Liquid-like filters), and I’ve also worked on Streamlined which uses procs in heredocs to control escaping and helpers better. But I think there’s lots of room for improvement here.
Interfaces in Java say what a thing should do.
In go they are even more interesting, because they are automatically applied if you match the method signature patterns.
In ruby we still need to say what you need to implement if you want to benefit from certain mixins.
For example. You reap so much benefit just by implementing <=> and pairing it with an include of Enumerable.
But for other more complex concepts, where say you’d need to implement three methods to really benefit from a mixin. Or to be ducktype compatible with some object, what do we do?
Usually the mixin or superclass defines stubs like
```
def <=>
raise NotImplemented, “You need to implement this to get all the benefits of enumerable”
end
```
I just think other languages have build clearer ways to express: You need to implement X Y and Z in order to be compatible with this use case.
it’s funny to see the different answers from you guys. I think that wasn’t really about finding the right answer, but more about showing off the interesting things we love about the language.
just out of curiosity, and why do you think that? immutability concerns maybe?
I’m also newbie in meta-programming matter… that’s also because of the of block characteristics that captures its surrounding scope?
I think some programmers abhor any sort of “spooky action at a distance”…any changes to any state anywhere needs to be as “local” as possible. In fact, let’s get rid of “state” altogether! All programming should be to create new copies of data and let go of old ones. I think it’s a very myopic view, but I can understand some of the arguments up to a point.
As for the issues with instance_exec, consider the following code:
class SpookyStuff
def do_stuff(&)
puts @a
instance_exec(&)
puts @a
end
end
class ScareMe
def jumpscare
@a = 1
SpookyStuff.new.do_stuff do
@a = 2
end
puts @a
end
end
ScareMe.new.jumpscare
If you run that code, and you don’t understand what instance_exec is, you might assume that the @a = 2 code is changing the @a instance variable above it from 1 to 2, and so down under the do_stuff call, the puts @a call would return 2, but instead it returns 1!
That’s because inside the block passed to do_stuff, the code is executing as if were written inside of SpookyStuff rather than ScareMe. So @a = 2 is setting the instance variable for an instance of SpookyStuff, NOT an instance of ScareMe.
There are plenty of reasons why Ruby provides this sort of mechanism — many DSLs are powered in this fashion — but for folks not familiar with the flexibility of block execution contexts, or if using a library/framework which doesn’t document this well, it can be a real gotcha! moment.