This was definitely written by a pythonist! If I tried to write it, as a rubyist, I'm sure I'd get some things about python wrong. (I find it notable how few people there are that are actually familiar with both).
The standard alternative to `for` in ruby does involve `each` and blocks... but definitely doesn't involve defining a custom `each` method on your class... That is a specialty thing that most developers have probably done rarely. Let alone defining it in terms of `for`, which is just weird!
But the basic principle stated "Instead of passing data back to the for loop (Python) you pass the code to the data (Ruby)" -- is more or less accurate.
blocks -- syntactic support in the language for cleanly passing a single in-line defined lambda/closure object as an argument -- are possibly the thing that are most special to ruby.
> Python builds on for-like constructs for all kinds of processing; Ruby pushes other kinds of data processing work to methods.
OK, maybe, although not totally sure what you mean.
> Ruby keeps going with its methods-first approach, except instead of each we have a new set of methods commonly implemented on collections, as below:
Um. I'm not sure where the author is getting this. It is certainly possible, as shown, but definitely not common to implement `select` or `map` or other methods provided by Enumerable directly on your custom class. It is a bit more common to implement `each` alone and let the `Enumerable` mixin provide the rest. But even that I'm not sure how "common" I'd call it.
> Ruby, however, inverts this. Ruby puts object-orientation as the foundation of the pyramid. Ruby contains the messy procedural world in blocks, letting objects work with those procedural blocks.
OK, true. The author is getting the details wrong, but I guess their overall picture is still right?
> blocks -- syntactic support in the language for cleanly passing a single in-line defined lambda/closure object as an argument -- are possibly the thing that are most special to ruby.
Too bad ruby stopped short of doing the trivial obvious thing and just making blocks be regular values. Instead the language is complicated by special syntax and functions for sending and receiving blocks, and bizarrely limited by the inability to do anything with a block literal other than send it.
Blocks were so close to being good. They managed the triple flip with a double twist, but they couldn't stick the landing. It's not quite a faceplant at the end, but it clearly shows how much better they could have been.
What do you mean by inability to do anything with a literal? You can capture it, turn into a variable, turn it into a "lambda" (make next, break, return local), use it like callbacks are used in any other language. Blocks are a bit special in that they go in their own slot for message sends (method calls) which allows the syntax to be unambiguous and the yield keyword allows for optimized calls to passed in blocks because methods that don't reference a block as data don't have to worry about the block outliving the stack frame.
No there’s special methods which turns blocks into objects, and there’s a syntax to do that, but blocks aren’t objects unless you specifically do one of those things. The idea you’re replying to is that they’d be objects always.
> Too bad ruby stopped short of doing the trivial obvious thing and just making blocks be regular values.
Abstractly, I kind of see the point, in practice, I don't see it makes much difference, and given Ruby’s two flavors of function-like objects, it seems to work out for the best.
> the inability to do anything with a block literal other than send it.
But sending lets you do anything else you’d want to to do with it. Specifically, to get either of the flavors of callables, you pass it to “proc” or “lambda”, and then use the result.
Not having every block be a separate value makes it easier for the Ruby VM to optimize such code without escape analysis (which is something that's pretty hard to do in this language).
Don't have to allocate the container => don't need to use the heap one more time. Nor access the block's code through that indirection.
Same thing with methods: they aren't objects, but you can create an object pointing to a method any time you need one.
> Too bad ruby stopped short of doing the trivial obvious thing and just making blocks be regular values. Instead the language is complicated by special syntax and functions for sending and receiving blocks, and bizarrely limited by the inability to do anything with a block literal other than send it.
Blocks are syntactical structures. Your statement is like saying that the parens and commas that are part of the argument list should be "regular values".
I'm not sure what you want to do with a "block literal". If you want to do something with the closure represented by the block, then reify the block into an object to pass it around, wrap it, introspect etc.
I think a lot of confusion about blocks in Ruby is really inconsistent terminology. Using "block" to mean the syntactic expression of a closure, i.e. part of the method call syntax I think helps to disambiguate the syntactical nature of closures from the closure reified into an object that you can pass around, call, etc. (i.e. an instance of the Proc class)
> Blocks are syntactical structures. Your statement is like saying that the parens and commas that are part of the argument list should be "regular values".
Well, no. Know what else is a syntactic structure? Numeric and string literals. People would never have touched the language in the first place if Ruby required you to write code like
x = String("abc")
y = Integer(123)
But people bend over backwards to explain why
z = { |x| x + 1 }
Is bad and shouldn't be allowed.
What?
The whole block vs proc thing is an artificial distinction. It doesn't add value, it removes it.
(Yeah, I know about the difference in how they handle return. This strikes me as an incredibly ad-hoc way to address something they could have solved a lot more elegantly.)
> But people bend over backwards to explain why “z = { |x| x + 1 }” is bad and shouldn't be allowed.
Really? I’ve never seen anyone bend over backward to explain it as bad, mostly just that that's the way it is, there are tradeoffs either way, and its not worth changing.
> The whole block vs proc thing is an artificial distinction
Presumably, you mean lambda vs. proc. (Both of which are defined using blocks, and procs are what using & syntax in a function signature causes a passed block to be reified into.)
But, yes, all distinctions are creations of humans, especially all distinctions within human creations like, say, programming languages. So “artificial distinction” is a meaningless descriptor when we are talking about things in a programming language.
Ruby doesn't have functions. It only has methods. That blocks in MRI happen to be implemented without reifying an object is an optimisation. You can only ever obtain a reference to said block by reifying it into a Proc instance.
Letting you obtain some kind of raw reference to a block that isn't a method on an object would make blocks unlike every other value in Ruby.
Hah, as a Pythonista, I'd say it was definitely written by a Rubyist :D
That initial example would usually be, in Python:
class Stuff:
def __init__(self):
self.a_list = [1,2,3,4]
def __iter__(self):
for value in self.a_list:
yield value
Basically, iterators and generators are native constructs that for loop operates on in Python (lists, which are actual "data", are simply special-case optimized instances of those).
Instead of calling iterators/generators "data", I'd call them wrapper control-flow constructs that `for` really operates on which provide amazing syntactic power in Python.
Ruby, from the examples given, seems quite similar, except that the "syntactic sugar" is somewhat inverted. I don't think it makes for a huge difference, but I don't have any Ruby experience.
I was not trying to go for the shortest code, but to show idiomatic code where you could easily do something else instead of just returning same values (otherwise, there is no value in wrapping a list with a class at all).
With both `iter(self.a_list)` or `yield from` you'd have to add a list comprehension in there to process each element, and with no Ruby-like blocks in Python, that limits what one can do.
But they are both definitely good approaches to highlight!
The article has some misunderstandings about idiomatic ruby imo.
> Ruby keeps going with its methods-first approach, except instead of each we have a new set of methods commonly implemented on collections, as below:
And then you show map, select, etc being re-implemented.
But once you have "each" defined, you'd just include "Enumerable" and get all the others for free.
As a separate point, I almost never implement each on a custom object in ruby. There are probably "library code" cases where it's appropriate, but in day to day work it should be rare. Typically I'd put objects in an ordinary array instead.
Nit: Ruby style guide (and most experienced code I've seen) never uses parens to invoke a method with no args.
As a separate point, I almost never implement
each on a custom object in ruby. There are
probably "library code" cases where it's appropriate
Likewise. I've worked with Ruby fulltime since 2014 and never done it in actual project code, only in book exercises, and I'm not sure I've seen it in any gems I've dove into, though I've never poked around in ActiveRecord.
Like you said, I can't think of too many reasons why one would want to implement #each -- on a daily basis I'm just putting various objects into arrays, hashes, etc.
I tend to write very "boring" Ruby. Ruby gives you lots of ways to get wacky, but IMO the default best practice would be to keep it simple and avoid the cute stuff unless you really have a reason.
I didn't think the author was saying you should always implement a .each method. I thought the implementations were for demonstration purposes, that .each is a method like any other that can be overridden.
There are other details that are wrong. Ruby does have iterators — except they're called enumerators. The standard implementation is called Enumerator:
Enumerators behave similarly to generators in Python thanks to utilities like #to_enum:
class X
def each
yield 1
yield 2
yield 3
end
end
>> x = X.new.to_enum
>> x.each
#<Enumerator: #<X:0x00007fd30c93dfa0>:each>
>> x.next
1
[etc.]
Ruby's for loops actually use enumerators, just like Python. It's just not used as much, for cultural reasons; most devs these days favour Ruby's data-oriented inversion of control.
Python and Ruby are quite different when writing code, but have almost the exact same technical abilities and limitations in the grand scheme of things. I've always felt there's little reason to learn both.
Ruby is way more capable and expressive than Python. There is so much more you can do with Ruby metaprogramming and don't get me started on Python's feeble lambdas which bear the mark of a BDL imposing his distaste for functional programming. Python is the VHS of programming - widely adopted but technically inferior. Ruby appeals to devs who value elegance of design. Take Sonic Pi (https://sonic-pi.net), for example - I can't imagine sam Aaron producing anything like this in Python. The DSL is everything in this app as with Rails.
Yes exactly, you're not really going to learn anything (syntax aside) by picking up the second, unless you needed it for work reasons or whatever there's surely several other paradigmatically different languages that will be more interesting/instructive to learn.
I don't think jrochkind is correct at all, if you want to make a library that has a datastructure that you want to implement custom iteration on, then it is most definitely idiomatic ruby to implement `each` for it. Your article was spot on in my opinion.
That said, in the almost 15 years I've been doing Ruby as my preferred programming language, I think I can count the amount of times I implemented a custom `each` method on one, maybe two hands.
As an example of how `each` is idiomatic, consider the Enumerable mixin: https://ruby-doc.org/core-3.0.2/Enumerable.html if you implement `each` on your class that mixin gives you all those methods (such as select) for free.
> syntactic support in the language for cleanly passing a single in-line defined lambda/closure object as an argument
I think Perl has this too? Or, well, I should say that Perl allows you to do anything, so even if Perl doesn't let you do it, you can still do it in Perl.
That `do` is a special syntactic thing avaialble for passing a single inline-defined closure arg. (You can pass one held in a reference instead of inline-defined, but it actually takes an extra somewhat obtuse step -- the syntax is optomized for inline-defined).
This "affordance" says "Yes, we make it really easy to do this, the stdlib does it a lot, please consider doing it all the time in your own code", which goes along with some of what the OP is discussing. Why we write `collection.each {|something| something}` (the braces are an alternate way to pass a block) instead of `for something in collection do...`
I think the crucial difference is that a block can control the flow of the enclosing frame, somewhat analogously to python context managers. For example
def get_widget
with_lock do
return @widget # returns from the method call
end
end
def update_interesting_gadget
@gadgets.each do |g|
g.with_lock do
if g.is_theone?
g.update
break # breaks the enclosing each's while loop
end
end
end
The main advantages of Ruby blocks over that approach are:
- they have special control flow that interacts with the method call or surrounding function. ie. calling `break` in `something` can early return from `someMethod`, or calling `return` will return from the function containing the `someMethod` call (blocks use `next` to return from them)
- due to using separate syntax / being a separate language construct, there is far better ergonomics in the presence of vargs or default values
Take this contrived example for instance:
def some_method(a = 42)
b = yield
puts "Hey #{a} #{b}"
end
some_method do
break
end
In JS you would have to something horrible like this:
const breakSomeMethod = {}; // Could alternatively use an exception
function someMethod(one, two) {
var f, a;
if (typeof one == 'function') {
f = one;
a = 42;
} else {
a = one;
f = two;
}
var b = f();
if (b === breakSomeMethod) {
return;
}
console.log(`Hey ${a} ${b}`)
}
someMethod(() => breakSomeMethod);
Scala doesn't really have a name for this, it simply permits you to provide a block in lieu of a parenthesized argument list. Given that blocks are expressions:
val a = { val x = 2; x + 1 }
the syntax you are describing is just a function-valued block.
While people have pointed out that this is not idiomatic:
class Stuff
def initialize
@a_list = [1, 2, 3, 4]
end
def each
for item in @a_list
yield item
end
end
end
I haven't seen anyone point out the deepest reason why: unlike in many languages, “for” is not a basic, low-level construct in Ruby, it is syntax sugar. That is:
for x in y
x.stuff
more_stuff
end
is sugar for:
y.each do |x|
x.stuff
more_stuff
end
(Also, for quite a long time, for...in also had runtime overhead , so it was slower syntactic sugar.)
for...in is rarely used (it is syntactic sugar that is neither more concise bor more specific in intent, but more familiar to people with experience in non-Ruby languages), and mostly on scripts that consume but don’t implement collections. If you are getting deep enough to be implementing an #each method for a custom collection, that's an odd place to use for.
Ruby's “for … in” is syntax sugar in the sense that is just another way to spell an identical piece of code. This would be like if Python let you write “foreach” instead of “for” if you wanted to. Another example in Ruby is how you can say “unless” instead of “if not”.
Python's “for … in” is syntax sugar in the sense that is an abstraction over constantly writing out the iterator protocol.
If anything I found Ruby's breadth of syntax sugar a defining characteristic of it - you can express the same program in a million different ways based on the programming paradigm you wish to emulate, or even make a DSL of your own to express the feelings in your heart. Not that I think that's necessarily a good thing though...
I'm not sure if this was ever changed, but I remember running into something surprising regarding variable scoping with for loops (I think it was during an internship in 2014, so I think Ruby 2.x with some relatively low value for "x"). Unfortunately I can't remember exactly what it was; I think it was something about variables defined in the for loop still being in scope afterwards? That doesn't seem like it should be the case if it's just syntactic sugar for `each` though, so I might be remembering wrong
Yes, for loops (and similar statements) don't introduce scope, so its not quite simple sugar for .each, and there are a few cases where you might use them over each for that property (if you are mutating values that you want to access afterward, it can be more concise than each) but they are mostly anti-idiomatic.
For me, its the explicitness in Python that is killer. I can see a name and match that to an import and match that to a package. Grabbing a function pointer works exactly as I would expect.
Stealing from Krister Stendahl's laws for religious discourse, Ruby's composable iteration is one area I have holy envy, particularly after I got used to it in Rust. I've used Python's generators many times just to have to switch to an explicit for loop. Things are generally better with a composable iteration model but occasionally I find myself switching to explicit loops in Rust.
Unlike Ruby, Rust does pull-iteration like Python, though there are experiments with Ruby-style push-iteration [0] [1].
Drop a debugger statement in above the code in question
If you're using Pry as your REPL, you can use Pry's ls and show-source commands to get more of the info you want at once, with less typing. It's basically calling the Ruby introspection methods for you.
"pry" is absolutely essential. I often prototype my code with a simple helper script that gives me a function to reload the relevant code, and just "live" in the pry prompt to explore the code as I add it.
It's the closest thing you get in Ruby to a Smalltalk environment.
90% of the time, I'm trying to read code in a very local context and understand it, which involves being able to trace an identifier to its source. I don't want to run it. Maybe I can't run it (it's a rarely executed path, the runtime env is complex, someone emailed me a copy of a source file, etc.).
Language developers, be like Python. Be like Java. Be boring and explicit.
You don’t deserve the downvotes here. Real life Ruby is so magic, opaque, and spooky action at a distance to a degree that makes maintenance on a large codebase functionally impossible for anyone who is not actively working on that codebase.
I know literally zero Go; don’t even think I could write a hello world from memory. But I can and have submitted non-trivial patches to Go codebases to fix bugs, add features, and even a few race conditions.
Having to use a debugger to determine the types of variables at runtime is just so terrible and backwards. I've had to debug massive python projects that have used monkey patching in the framework and it was terrible. This thread is actually causing me pain and making me relive horrible bugs i've had to fix. Reading about auto loading and having to use a debugger to inspect types is just so fucking bad.
I use Rubymine and can cmd+click on anything and it shows me exactly where it came from. Been developing in rails for a decade and the auto loading has bitten me at most 5 times.
Onboarding to all but the most perfectly-written-and-maintained Rails codebase is pure hell, for exactly that reason. I've done lots of Rails in the past, but sworn it off after enough such experiences.
I have no problem with PHP autolading, or TS dependency injection (as seen in Angular, NestJS, TSed), or even in Scala (Play! framework via Guice). Because it's not magic (since you need to import the types you know what you are getting, for PHP there's the autoload.php generated by Composer, for TS everything is in the node_modules and in the lockfile, and for both Scala and TS the compiler checks things too. Of course in TS there are sometimes problems for non-native [ie. JS + .d.ts] libs, when the typing becomes outdated).
But in Python the amount of magic shit one had to do to get things loaded in a sane manner was always somehow a serious burden.
.. but in Ruby (Rails). Well, yeah, all you have is a lockfile, and nothing else. No imports, no typing. That gets crazy really fast. :)
You can use DI in Python too--it's an architectural pattern that can be used in any project regardless of language.
I've never seen or used autoloading in Python. What would the use case for that be? I guess it could be "convenient" to avoid imports? That seems like a bad idea to me and not very "Pythonic."
I'm not sure if this is relevant to your comment, but I've seen people abusing sys.path in Python projects instead of setting up a proper package and installing it.
Modern Python package/dependency managers like Poetry make this nicer/easier than the old school setup() approach and they also create lock files.
We had problems with circular imports back then in a Flask based backend. Also we wanted configuration based loading. Local dev, testing, prod, on premise. Plus multi-tenancy meant that everything was very-very dynamic and additional layers of configurability were present based on the request. (So basically request scoped service injection was needed. Of course we did not do that, we just passed around a bunch of parameters in a "very Pythonic way".)
Circular imports often indicate an architectural issue. In any case, there are easy solutions in the rare cases where it's absolutely necessary, like putting one of the imports at the bottom of one of the modules or in a function.
Regarding configuration based loading, that's something that's pretty common in Python webapps, and injecting capabilities/services per request seems like it would be pretty straightforward (and not anti-Pythonic either, depending on the implementation).
I just don't see how Python makes this kind of thing more or less difficult than other languages.
> But in Python the amount of magic shit one had to do to get things loaded in a sane manner was always somehow a serious burden.
Controlling how Python imports things, seems dead easy with importlib [0] (introduced in Python 3.1). You can control the namespaces, the loader, the paths, generate code on the fly, etc.
If you wanted to, say, duplicate lockfile imports using a requirements.txt file, then that's probably a teeny tiny ten line thing or something like that.
Python is quite flexible if you want to rewrite how the language is doing something.
Agreed. I was sloppy in my previous comment, but what I wanted to explain is that we started with good old static imports, but needed a lot more dynamic stuff (heavily customizable deployments - onprem, dev, SaaS prod, etc)
Python can do this, but we had to accept that yep, we need a lot of elbow grease to be able to use regular libraries plus have a highly environment and request dependent "stack".
Ah yes, now I remember why I stopped learning ruby and was generally disgusted with the language. Along with the standard library having numerous ways to do the same thing and the general use of monkey patching (and the ruby users actually thinking monkey patching is good)
> I think it’s important to say that when you’re debugging anything goes. I mean anything. Global variables, redefining methods, adding conditionals, manipulating the load path, monkey patching, printing the call stack, anything.
This is the thing that keeps me coming back to Ruby. The core philosophies of the people who influence Ruby just align so well with my own.
[Back in 2008ish I had a choice to learn Ruby or Python, after working with languages including C and Java. I started with Python, but switched to Ruby because Python launched slower and had no native Integer type (important for old ARM CPUs of the era with no FPU), but it's Ruby's philosophy and potential for poetic (but readable!) flow that make it stick now over a decade later]
> I can see a name and match that to an import and match that to a package.
I like this too, but... I don't find dynamic languages to be very explicit.
Given that Python is a dynamic language, I only going to know what I can do with (or what is true about) the arguments to some function if I have worked with the codebase previously (or have sufficiently good documentation, and unit test coverage).
At least with an explicit import you have something to search for.
I have often spent several minutes trying to find a definition for a ruby method/modules/class only to finally have to run it and dump the file location manually with `source_location` to find out it's really a 3rd party gem added to the project.
Let alone other 'magical' techniques Ruby affords, e.g. you can re-open a class to monkey-patch more methods in; or even define method_missing so that the methods an object has are dynamic. Some very neat stuff.
I'm not the biggest Python fan myself and would not recommend it for large projects, but I use it a lot both at work and personally for scripting/small stuff. It feels dumb, but I like dumb for those use cases.
Well.... except when the package doesn't match the import path. Then you can get stuck in a merry search for "what the heck is X and why is it not in our lockfile".
It is interesting that this kind of context switch for some programmers is a breath of fresh air, and for others its a horrible hack.
I think the fact this is not an objectively good or bad thing has led to a lot of the tension we see in other aspects of programming.
I think a connected issue is tooling: if you presume a certain floor on tooling, more 'advanced' things like multiple composed domain languages become much more feasible since the tooling can remove the need for memorization and help guide construction without fear of errors. But if you presume all tooling beyond text editing should be regarded as non-essential at best and superfluous at worst, it greatly constrains the set of things in programming one can consider sane or not. I do wish we had more projects experimenting + getting traction with "high tooling, high abstractions, high domain mixing" so we could get more literacy around these methods, but I think the vast majority of programmers today have abandoned these kinds of ideas as good or worthy of further development. On pure interia alone, I long ago capitulated to the idea we will be stuck in text editors and having heated debates over typographical syntax for programming languages for the foreseeable future.
People working on things like LINQ and more advanced domain mixing like MPL or (where I once worked) Intentional Software run up against lots of resistance from people I think probably due to some fundamental differences in assumptions, but I'm not really sure what the deepest differences are.
I'd argue when I'm writing C# in a web-development setting (ASP APIs etc.) that a large majority of the code is LINQ. Especially if using something like Entity Framework. It's really just part of the language now.
A line of LINQ looks and works nothing like a line of C#, and the difference is even worse in VB. Whether it's "part of the language" or not, it's a fundamentally different way of writing logic than the base language you're writing around it. I agree it's completely coupled with EF, but I hate EF too. (After you've used ActiveRecord for over 10 years, any other ORM just feels like a ball and chain, but that's another topic.)
> A line of LINQ looks and works nothing like a line of C#
Maybe if you're using the query form, but if you're using the method form, I'd argue that they work together very nicely.
For me, at least, I've found embracing Lambda expressions to be very helpful in a number of cases, between Linq, or writing my own code for doing things like rendering tables (long story there).
Granted, I learned C# right after LINQ was released, but I'd still argue that LINQ is well worth having around.
I came from a Rails background and it was definitely painful to learn EF since it has a lot of nuances and things you wouldn't know without experience (e.g. How things mutate throughout the object lifecycle, migration funkiness, how tracking works and 'AsNoTracking()' etc.)
Think I'd still prefer ActiveRecord really but after you get used to EF (Core) it's still a good tool.
Are you using the query syntax instead of the method syntax? Because the method syntax is completely in line with normal OO code (it's just a bunch of chained methods). I'll agree that the query syntax is weird, but I haven't seen it used much.
Are you talking about the `from x in y select x.z` syntax or the `y.Select(x => x.z)` syntax? Because the latter is the one I most commonly see, and it fits in fine with the rest of the language.
If one is having to implement an each or map or select method in ruby, then I can definitely appreciate the differences you point out. Even though I've been using ruby a long time, it would frankly still be a bit of a pain to have to remember the details of how yield works.
Because Enumerable provides so much functionality, I almost never need to implement methods like those. So what I really care about a lot more are the ergonomics of using each, map, select, reject, all?, any?, etc, which I use very frequently.
Yes though the point is the same, Enumerable gives the objects methods (internal iteration), in Python you would use built-ins max or filter or a list comprehension on an iterator (external iteration).
Enumerable is one of my favorite aspects of the Ruby standard library! I've been writing it off and on for quite some time and still discover incredibly useful and elegant functions in there. It's incredibly rare to not find what I need when working with collections.
While this is cool, you can also just do `some_enum.sum` (also works with string concacts)
One thing that is cool about the .to_proc shorthand though is, for example, you have a method
def square_it(x);
x * 2;
end;
people tend to write an enumeration that uses it like
items.map {|i| square_it i }
but you can actually write it like this
items.map(&method(:square_it))
So not only can you write shorthands to methods on the object itself, you can write shorthands to other methods and "automagically" pass the item as its argument. Nifty stuff.
Not a rubyist, but I love that collect() made it into Rust, too. I feel like it takes the role that generators have in Python, but is much easier to reason about in terms of the control flow and what data is being passed where.
Enumerable#collect is just an eager version Python map() in method rather than function form (#map is a avaulable as an alias for #collect); it is a little more concise for when the function you are mapping over is a method on the object being processed (even more than Ruby’s normal advantage with lambdas), but not fundamentally more powerful.
(Enumerable#lazy gives you an object where collect/map is, and other merhods are, lazy rather than eager.)
> I feel like it takes the role that generators have in Python
Ruby has generators, though in either Ruby or Python map/collect (the lazy versions, in Ruby) can be used for some of their uses, since it basically produces a generator from an input collection or generator and a mapping function.
It’s not, though. (If it was, we could write about its equivalence with Python’s [from functools in py3, core in Py2] reduce function, though, saying much the same thing as about the actual equivalence with map upthread, except reduce/inject is less often a substitute for generators than map/collect.)
Hmmm, I don't think this article accurately represents how people write python since ~2005. I'm biased because I use python every day, but there's a big objective mistake.
> Ruby flips the script, giving the objects deeper customizability.
Not really, because python allows you to do things the Ruby way, but I'm not sure the reverse is true.
class Stuff:
def __init__(self):
self.a = [1, 2, 3, 4]
def __iter__(self):
for item in self.a:
yield item
and you can write it more simply in this case.
def Stuff():
a = [1, 2, 3, 4]
for item in a:
yield item
What about the select and map example?
class Stuff:
def __init__(self):
self.a = [1, 2, 3, 4]
def __iter__(self):
for item in self.a:
yield item
def map(self, f):
for item in self.a:
yield f(item)
Which can be used just like the ruby example, with syntax of similar ugliness.
print(Stuff().map(lambda item: item))
I think I could come up with a python example that maps 1:1 onto pretty much any ruby example, but I don't think it's possible in general to find ruby examples that map onto more complicated python.
Author here! Very true, though I was trying to focus on idiomatic python that pushes logic into for loops, list comprehensions, etc w/ composable iterators through tools like those found in itertools (zip, etc...)
Since Python doesn't have Ruby's blocks, you have to define a non-trivial function as a free function, so it's less likely you'll do this. (Python's lambda's are far more limited than Ruby's blocks.)
You can also trick Ruby to return a new value every time a method is called for iteration, but again my focus on what I see as idiomatic in the two languages
> think I could come up with a python example that maps 1:1
My take on it:
class Stuff:
def __init__(self):
self._list = [1, 2, 3, 4]
@property
def each(self):
for el in self._list:
yield el
for item in Stuff().each:
print(item)
It's even less verbose than the Ruby equivalent in the original article, thanks to the indentation-defined blocks.
It's also worth mentioning the Enumerable module that can be mixed in to classes, which gives you `map`, `select`, and much more for free by implementing `each`.
At a deeper level, this comes down to internal vs. external iteration, and, more generally, how languages weave together concurrent threads of execution:
Ruby is the first language I learned (apart from dabbling with Visual Basic as a kid). I still reach for it to actually get things done. One thing I always appreciated about it was the consistency of everything being an object and how that allows you to just chain methods together. Kind of reminds me of Lisp in it's consistency (although in Lisp everything is data or a list, not an object). Later I dabbled in Smalltalk (Pharo) just for fun to try to understand what inspired Ruby's object model.
I've tried Python, hated the inconsistency of it, but then again I never tried any C family language before learning Ruby. Calling Each then feeding it a block feels more natural and consistent than a for loop IMO, even if for loops are the standard 'programming' way of doing it. It does make sense to use it as an analogy to describe the differences between the languages.
> I've tried Python, hated the inconsistency of it
Can you elaborate on this? I’ve written in both Python and Ruby, but I’m not sure what you mean by this critique of python. I wouldn’t characterize either language as “inconsistent.”
In Python some standard operations are global functions and some are methods. Which are which isn't exactly clear. It's also kind of irrational and random but I hate seeing __init__ everywhere.
For example, to find the length of an array in Python you use the function len(). In Ruby you call the method .length. As the article is about, loops in Python are for blah in blah. In Ruby they're blah.each and for loops are just sugar (not sure why they're there but you can avoid them). In Ruby pretty much everything is an object and you just call methods. Very few keywords, global functions, etc...
Also, as the other poster said, if you call a[0] on an empty array, in Ruby you get nil, in Python it throws an error. Not sure which is better or considered more 'proper', but Ruby's behaviour is what I'd personally expect in a dynamic language.
> In Python some standard operations are global functions and some are methods.
Typically, all of the former are also the latter because the global function just calls the corresponding method. E.g., len(x) works by calling x.__len__().
> As the article is about, loops in Python are for blah in blah. In Ruby they're
...for blah in blah, if you choose to use them at all, which has the same semantics as the python.
It relies on method calls to #each under the hood, and usually in Ruby you won't bother with for/in, but it is there still...
You could dig even deeper. The reason for these differences in how the for loop works happen because Python comes from the procedural tradition, while Ruby comes from the Smalltalk tradition.
In other Smalltalk-inspired languages (but not Ruby), you can do the same thing with an even simpler language feature: if-statements. In procedural languages, including object-procedural languages such as Java and Python, an if-statement is a core language feature with its own syntax. In Smalltalk, conditions are kicked out of the core language and into the standard library. You have methods on the True and False objects that either take an anonymous function and execute it or perform a noop, or take two anonymous functions and decide which one to execute.
I have nothing to contribute to the content of this article, but I really want to express my gratitude to the author.
I've been using Python for over a decade and installed Ruby once or twice just to touch it, and I really like how this article has managed to bring Ruby onto my radar, not as something which I should use, but should appreciate.
For me it was just a Python alternative which some companies really do like, but this article told me a bit about the beauty of the language. Nice differences.
To me, the biggest fundamental difference between Ruby and Python seems to be that:
In Python methods are implemented via callable attributes (it's dicts all the way down)
In Ruby attributes are implemented via methods (it's messages all the way down)
I've been working professionally in Ruby for 5+ years, but non-professionally using/dabbling Python for 15 before that. Personally I still find Python more intuitive though even if I haven't used it for ages - it just fits my brain more automatically and quicker. Ruby is nice overall (ie the smalltalk inspired bits), but the more Perl inspired bits irk me, and parts of the Ruby community can produce some insane library/framework code trying to make interfaces as "elegant" as possible.
As an almost exclusive Python programmer, I’ve often looked at Ruby’s block syntax with jealous eyes. It would be so nice. But I wonder if that’s maybe the exact thing that I also like about Python: the language is pretty damn simple in its essence. You want to minimize cleverness wherever possible, and that idea is firmly planted in the community. Ruby has this sense that Ruby is almost a meta language, you create cool “DSLs” in it to accomplish your goal. That makes it harder to fit into the brain also.
For what it’s worth, the language itself is extremely simple and composable, which is what makes it possible for people to use it as a kind of metalanguage.
That was really popular in Rails gems around the time that Rails itself peaked in popularity, but has since died off considerably. It was never especially common outside of Rails gems. There aren’t that many things that call for DSLs.
DSLs remain very common in Ruby, but Rails taught us to avoid littering. DSLs now tend to be intentionally very tightly scoped.
E.g. Rouge, the Ruby equivalent to the Pygments syntax highlighter, defines DSLs to define lexers and themes. The do so in terms of methods on a meta-class so that the DSLs are confined to the definition of a lexer or theme class inheriting from the right classes, so there's a single place to look at the definitions, and their use don't leak all over the place.
I think that really comes down to how your brain works or the way you think about logic. For me Ruby is a lot easier to wrap my head around and understand than Python is.
The author overlooked the most mind-bending feature of Ruby blocks, the thing that makes them so very different than lambdas in other languages: Blocks can return from the enclosing function.
def process
collection.each do |element|
return element if condition
end
end
This has the effect of popping three (or more) frames off the stack. From the perspective of #each, yield never returns.
This feels a bit ironic given how much the for loop has been villainized in numerical computing, by none other than languages like Python—and Matlab and R—where for loops are so awfully slow that you can only get performance if you avoid them like the plague and write (sometimes awkward) vectorized code that pushes all the for loops down into some C library (or C++ or Fortran). Compared with, say, Julia, where people are encouraged to "just write yourself a dang for loop"—because it's not only effective and simple, but also fast. I guess what I'm saying is that it feels like even though Python may embrace the for loop as syntax, that affinity seems superficial at best, since if you care at all about performance then Python immediately rejects the for loop.
In Python numeric computing it's common for your outer loops to be for loops and your inner loops to be vectorized PyTorch/whatever.
I personally like being able to easily comprehend and control what's being vectorized. Maybe it would be nice if my compiler could automatically replace any inefficient loops with vectorized equivalents, and I could think in whichever idiom came more naturally to the problem at hand. But I don't think there's anything too illogical about looping over epochs and batches, and then computing your loss function with matrices. Maybe I'm just used to a suboptimal way of doing things :)
> Maybe it would be nice if my compiler could automatically replace any inefficient loops with vectorized equivalents
The trouble is that a for loop is much more expressive than vectorized operations, so most for loops cannot be transformed into vectorized equivalents. The reason convincing people to write vectorized code works for performance is that you're constraining what they can express to a small set of operations that you already have fast code for (written in C). Instead of relying on compiler cleverness, this approach relies on human cleverness to express complex computations with that restricted set of vectorized primitives. Which is why it can feel like such a puzzle to write vectorized code that does what you want—because it is! So even if a compiler could spot some simple patterns and vectorize them for you, it would be incredibly brittle in the sense that as soon as you change the code just a little, it would immediately fall off a massive performance cliff.
I guess that's actually the second problem—the first problem is that there isn't any compiler in CPython to do this for you.
With hindsight, the languages turned out to be a bit "too dynamic" for their own good. Very few are changing variable types often enough for that feature to be useful. The downside, makes typing bugs possible/more likely, and slows down every access. Par for the course, would say that slow loops are a symptom not a cause.
matlab has been jit compiled for years now, the "for loops are slow" dogma is over.
numerical computing in python is kinda weird as it wasn't the original purpose and the fast math libraries were bolted on as an afterthought, but even then tools like numba do the same in python, although there's a bunch of nuance in writing simple enough python and hinting at the correct types for the variables in order to get it to compile something reasonable.
julia's let's use strict types, jit compile everything from day one and avoid locking approach is nice though.
After numba’ing several nontrivial pieces of numpy code in my life, you might as well just rewrite it in nopython mode for Cython unless it’s very trivial stuff. Numba errors and partial coverage of numpy is a huge time sink in my experience.
Except for cases with local variables, I have absolutely no idea which I should prefer. Kotlin makes a cute case for the functional style with it's lambda shorthand:
list.forEach {
...
}
I've found the functional style helpful for times I want almost add a language feature to avoid boilerplate. The functional style was added without special-purpose language changes, but the iterator version required Java to add Collections, Iterators, and eventually fancy for-each syntactic sugar.
The big advantage of the functional style in Java is that you are disallowed at the compiler level from modifying the contents of the collection you're iterating over. That sort of compiler-enforced good practice is definitely for the best.
This is a great article. I really enjoy reading articles comparing two languages and these kinds of posts are not as easy to come by as discussions about a single language.
Bottom line: external iteration is composable, internal iteration isn't. Try implementing zip or early stopping using internal iteration (hint: you can't).
I find it easier to think of as "push-pull" though. In Python and Go, you "pull" from iterators. In Ruby, JavaScript, and Rust, you're "pushed" upon. You can do both styles in any language, but there's one way that's favored.
To break the dilemma, you need 2 call stacks, which means threads / coroutines / generators.
(There was a recent blog post about writing an iterator to do the powerset calculation, which is a bit tricky, and made me think of this post)
After spending years in functional-land, I find the for loop (and while loop) construct maddening; you have to maintain the state of the entire enclosing scope system in your head, whereas enumerable constructs like map and reduce close (ideally read only) over variables in scope and if you need to keep state you have to use reduce, which explicitly tracks what changes between iterations and nothing "unexpected"... In a general sense, FP enumerations are more explicit than control loops which are implicit state machines.
> After spending years in functional-land, I find the for loop (and while loop) construct maddening; you have to maintain the state of the entire enclosing scope system in your head.
This is only true if immutability is enforced. In js you see map used to mutate variables outside the scope of the map closure all the time.
const o = {}
const d = [1, 2, 3, 4]
d.map(i => o[i] = i**2)
Which is equivalent to this python.
o = {}
d = [1, 2, 3, 4]
for i in d:
o[i] = i**2
The cognitive load is the same in both. The strength of pure FP languages come from enforced immutability, but that constraint often adds cognitive load in other ways.
> constraint often adds cognitive load in other ways
In my experience (and others) those constraint only reduces cognitive load, it can increase actual performance load, and can make certain algorithms "basically impossible", but you're also never actually writing those algorithms. When was the last time you ACTUALLY used dykstra's A*? Come on, most of us are writing SAASes, APIs/backends and basic frontends here (yes, the rest of you do exist), and even for shitty O(n^2) algos, your n is probably in the 10-20 range. Your bad algorithm will not take down the server.
I think most programmers would disagree with you on this. Perhaps after enough FP experience the cognitive load that comes from the language’s constraints fade away, but I haven’t seen this in practice. FP is less commonly understood and harder for the average dev to work with.
> Come on, most of us are writing SAASes, APIs/backends and basic frontends here (yes, the rest of you do exist), and even for shitty O(n^2) algos, your n is probably in the 10-20 range. Your bad algorithm will not take down the server.
This isn’t related to the earlier point but I’ll bite. This thought process assuming “Your bad algorithm will not take down the server” is a recipe for bad engineering.
For example, we had a bulk action (1-500 records) in our API where the first implementation pulled all the data into memory and processed the data in the request. This ended up being disastrous in prod. It took down our server many times and was tricky to track down because the process would be killed when it maxed out memory.
The solution wasn’t to switch languages or anything. It was just to move the operation to our async worker queue and stream through chunks of data to avoid running out of memory. It cause a lot of headaches for devops that should have never happened.
While you’re right that there are many cases where n is not large, engineers must consider how large n can be or explicitly restrict n before pushing a bad algo to prod.
'FP is less commonly understood and harder for the average dev to work with' citation needed - if anything I'd say the opposite is true, reducing state and mutation etc makes code easier to read and reason about.
Look up any programming language popularity survey. Pure FP languages are substantially less popular, therefore less commonly understood and harder for the average dev to work with.
If FP was the norm, then you could make the same argument for OO.
I started a poll on twitter. Now, obviously there is bias since my twitter audience is mostly FP people, but keep in mind:
- almost all experienced FP people "started in OO", or at least, imperative. So if they are picking map/reduce it is out of experience with the alternative.
- the split between bootcampers/informal and CS/formal is instructive: You can see that "bootcampers", who typically have less need for a low-level mental model of what the metal is doing, find that that for/while loops are harder on the brain than map/reduce:
Pure FP languages may be less popular/common, that doesn't mean FP ideas are harder to understand. In stackoverflow's developer survey https://insights.stackoverflow.com/survey/2021 Clojure was the second most 'popular' language after Rust, and you could argue Rust draws heavily from FP styles too. Languages that are unquestionably OO or procedural, like Java and C, scored lower.
I was never a fan of ruby until I had to use it at work.
I think my main superficial turn-off was indeed the non-traditional loops and I was a fan of python whitespace indentation.
But on the whole ruby just feels very coherent and language features mesh very well together - it often feels like a true lisp with self-style object orientation done very well [1].
Python otoh feels like an imperative language with classical OOP added on and extended via magic methods.
See e.g. functional programming in python vs ruby with map, filter, reduce and so on and how blocks and procs in ruby interact with those vs lambdas in python.
I still use python often but have to say I tend to miss ruby when I do.
> In terms of programming-in-the-large, at Google and elsewhere, I think that language choice is not as important as all the other choices: if you have the right overall architecture, the right team of programmers, the right development process that allows for rapid development with continuous improvement, then many languages will work for you; if you don't have those things you're in trouble regardless of your language choice.
"the right overall architecture, the right team of programmers, the right development process that allows for rapid development with continuous improvement" should be the working definition of "agile" development in almost every context. Would save a lot of confusion and pointless arguments about the true definition of agile, kanban, extreme programming, or what have you.
Most of the things you list are things you may or may not get right the first time; what makes something agile, at least, when the term was first floated, was the ability for the team to own the solution, say "this is not working" (be that architecture, team structure, process, etc), and change it easily. Agile was "ability to (rapidly) change", not "adherence to a defined set of rules called 'Agile' ".
Most places don't actually seek out that definition of agile, though. And so there's a lot of bikeshedding around "what process is the one true way (and what can we add to it to make it work when it fails us)" rather than "how do we empower the teams to make their own decisions on how to best be effective"
I think in Python, I saw a good deal of adoption in the Sciences early on.
I worked on a Physics experiment almost 15 years ago, there were inklings of Python dripping in to doing numerical analysis.
NumPy just using existing Fortran libraries is a good example of it just being bootstrapped by the scientific community.
Most of these guys come from a Fortran background, and some used the C++ ROOT framework.
Ruby was just too alien for them.
ROOT had made some excellent Ruby bindings, and I spent some time showing some of my colleagues how much time they could save using them, but they just found it to be too different.
It's sort of funny to think, because languages made for AI originally like LISP used functional concepts like map, collect, etc etc...
I think the AI trend using Python was boosted by numerical analysis/computing libraries already being present.
Peter Norvig is always a pleasure to read, even on HN. Imagine how precious is the history of HN Threads... i hope someday someone compiles it in a consumable fashion.
It's always been the global functions in Python that put me off. My brain just doesn't think like that. When I want the length of an array, I look for a method on the array. Same with map, and anything else.
That said... pretty small gripe in the grand scheme (no pun intended, maybe) of things. When I've written projects in Python, I've enjoyed it.
> It's always been the global functions in Python that put me off.
I'd argue it's the inconsistency. Some things are methods, some things are functions. The name choices are also a bit boneheaded (eg. "dumps"). It's a lot like PHP in these regards.
Ruby, on the other hand, is beautifully designed. But it gives you a bazillion ways to skin a cat, and people love to be "clever" with their cat skinning. In those cases, I appreciate the utilitarian approach and "zen" of Python.
My biggest gripe with Python, though, is the totally 80s feel of the version and package management. It's one of the worst experiences in my daily engineering life. I can never know if complex ML code will work on my system without thirty minutes of patching things up.
I'll gladly use Python to get work done or Ruby for small websites. But I'm looking for a new daily driver for scripting that learns from the last thirty years of mistakes. (Modern language, sensible packaging/dependencies/version switch, optional static typing, and optional static binary output.) Nim, perhaps?
Nim is nice but it's not dynamic so replacing a lot of the use cases of Python/Ruby (or R) would be a pain. Like, I'm not going to do any data munging in Nim when the others exist, exploratory programming in Nim is also sub-optimal. That being said, it's a nice language, way easier to write than any other statically typed language I've used, I never used Pascal/Delphi but Nim makes me appreciate that whole family of languages a lot more (Nim is basically a modern Pascal with Python's significant whitespace that compiles to C).
Not really. Proc vs def, let and var keywords, Pascal style ranges and arrays, etc... I found C# is closer to Nim when it comes to translating code than Python is. The semantics are pretty different, especially since it seems everyone uses numpy in Python for any sort of array maths.
That's what turns me off about Python (and f-strings), being a Perl programmer. I've had to do some proof of concept for reading smart cards and the Perl PCSC library sucks. So I did it in Ruby, which like Perl has pack/unpack methods for converting hex strings to binary and back. It's just great. Now I have not two programs to read smart cards, one using the Ruby smartcard library and another one using rubyserial and doing serial communication directly with the card reader, with a partial implementation of the T1 protocol, all in under 1k lines of code.
This doesn't have anything to do with Python but Ruby feels more of a natural language for doing rapid application development and active record than Laravel does.
The caveat is that Python is only _fine_ at functional programming, and that only because it's very, very malleable. The deeper you go into functional programming with Python, the more you wish you were programming in Clojure, or F#, or whatever.
But I'd still rather use a functional style in Python than an OO or procedural style.
> I think my main superficial turn-off was indeed the non-traditional loops and I was a fan of python whitespace indentation
I've always been perplexed that this, a very small thing that barely ever comes up since 99% of the time you are doing a for each loop anyway, is the main turn-off for people learning Ruby, when Python has those crazy weird if statements and indentation-sensitivity
Author here - True! though generators are a specialization of iterators discussed in this article. I had considered it, but didn't want to get too bogged down.
I'm a longtime Rubyist who's dabbling in Python a fair bit nowadays due to the growth of the ecosystem and the libraries available. I find Python enjoyable to work with but the thing it makes me love more about Ruby is how in most cases you think about objects and the capabilities of those objects, whereas in Python it feels like a crapshoot (I'm sure it's not, it's just how it feels as a dabbler) as to whether you pass objects into a function (e.g. `len`), use methods on objects, or whether to even think of OOP at all :-)
Now, that's not entirely a negative, I quite like Python's freewheeling "do whatever works and feels best for you" ways, but that feels somewhat contrary to the Zen of Python's "There should be one-- and preferably only one --obvious way to do it." (!)
Those last aren't mutually exclusive. The idea is that there should be one obvious way, such that you'll find that what works and feels best is that way. The idea is that you will simply want to comply, not to demand that you do.
Developer happiness, I believe, was one of the main pinnacles and design choice of Ruby according to Matz. Meaning he wanted developers to enjoy coding in Ruby.
I think there are just multiple kinds of developers. I've programmed a fair amount, professionally, in Smalltalk and Ruby, and quite a bit in Python (along with lots of other languages). If I squint I can see why some kinds of developers like Smalltalk and Ruby more, but I am not that kind of developer. Python is better (for me) for small to medium projects, and then I prefer strict typing for larger projects (currently Go, which I'm ok with).
But I know plenty of people that love, love, love Ruby and I respect them as developers, but what we enjoy in a language is just different.
> I think there are just multiple kinds of developers.
I've seen this very glaringly. I love Ruby and Rails, and want to leverage the stack as much as is reasonable. I've come to realize that there are people who really would like to write 100-200 lines of Java for every line of Ruby I write, just so that they "have control." I think they have no idea what they're talking about, but I have no influence over them. The fact that I'm writing my 3rd application in 6 years which does something that entire teams of outsourced developers could not do is, surely, just coincidence.
Yes this is mostly it. Some of us just have a different world experience and are wired differently; the thing with engineers is that they don't like subjectivity - they need absolute answers, but it's really like debating who is better Metallica or Nirvana.
The type of programmer who likes Ruby usually appreciates aesthetic code, freedom and expressiveness. The type of programmer that likes Go usually appreciates strictness, one way of doing things and performance.
In the end to the average SAAS startup it really doesn't matter which of the popular languages you choose, it's been proven countless times that any of these languages can build monster size companies
I believe it, and it shows! Coming from a longtime Perl background and switching to Python (which has its pros and cons) and then finally trying out Ruby, I felt like Ruby was what Perl should have become.
Ruby was heavily inspired by Perl. I know a few people that moved from Perl to Ruby and felt quite a home. (Personally, my path away from Perl is likely to be to TypeScript on Deno, since if I'm going to make a change I might as well pick up some major benefits along with the normal set of trade-offs).
As a guy who uses a lot of languages and having started on C-like languages I appreciate Python in that it doesn't make me work terribly hard to get what I need to get done. I'm far from a Python expert cause I don't use it exclusively.
In spite of all that, it's pretty intuitive, reads like pseudo code. Plenty of data types and packages to get the job done.
The few times that I've touched Ruby, however, even though it's succinct and beautiful language, I always have to find myself relearning its super object oriented centric paradigm.
Everything takes 2-3 times as long to achieve and, arguably, not that fast a language either.
I suppose if i worked on web app project exclusively, maybe Ruby might be my language of choice, but I don't.
I'm sort of a jack of all trades master of none position (but that's ok cause in my particular situation I get paid pretty well for it).
Why not both? I want the language to provide an iterable/enumerable interface with the usual generic workhorses (map, reduce, etc.) and allow me to implement that interface as a free function for any type. If I'm authoring a type, I'll add specialized functions as needed.
Ruby manages to do this. The default implementations of all the methods you want are on the enumerable module, so if you implement the “each” method you get everything, including the ability to get an enumerator object that you can pass round and use for anything else you want.
It is funny to add other languages to this discussion. To wit, I'm reminded of the article regarding someone learning clojure where their coworker left them a note that read "just use a hashmap!" Indeed, in so much of lisp, it is all just some form of list. Either lists that have a first and a rest, or those that are alternations of names and values, or those that are pairs of items, etc.
Python seems fine, except that I can't get comfortable in a language that uses whitespace as part of its syntax.
I know the typical arguments of "linter, etc" but I believe my ruby and JS code, which includes using a linter and formatter, is easy to reason because brackets allow for better mental encapsulation.
All personal experience, I can't speak to the technical differences.
I think once you get more used to it you'll find the complete opposite. All those brackets are extra things your brain needs to process when reading the code, strip them away and it becomes just like reading written language. Ruby to my eyes, even when well formatted, looks as ugly as PHP - a mess of unnecessary indications that destroy the readability of the code (for me).
As a Python->Ruby convert like the parent here, I'm curious, which indications do you find unnecessary? The only one I can really think of is `end`, which, sure. But the implicit return is also much cleaner after I got used to it, so I think that's a wash.
I've barely written any Ruby in my life, so I may well change my mind like you if I did more. I do find it interesting how some languages just look uglier than others, and how subjective that is. I really struggle to learn languages where my initial reaction to how it looks is bad.
I agree that languages that look bad are hard to read. For me curly brackets or significant white space for blocks/scope is a big problem, it just all turns to soup.
So Ruby using end instead of brackets or white space is one of my favorite features of the language.
Python looks horrendous to me, and it's absolutely one of the things that stops me from using it. It feels like walking with gravel in my shoes to have to look at it.
> All those brackets are extra things your brain needs to process when reading the code, strip them away and it becomes just like reading written language
No, it doesn't, because written language explicitly marks ends, and sometimes also begginings, of structural units (programming gets brackets from natural language), it doesn't just use white space (until you get to units bigger than sentences.)
> No, it doesn't, because written language explicitly marks ends, and sometimes also begginings, of structural units (programming gets brackets from natural language), it doesn't just use white space (until you get to units bigger than sentences.)
Written language does of course use white space to separate words (this was not always the case, see for example classical Latin).
And I think it's fair to say that a line of Python is roughly equivalent to a sentence, and whitespace indentation is only used for syntax at that level.
The analogy breaks down a bit at higher levels, as Python blocks can recurse, but in writing we have structures like chapters, books, etc.
As pointed out, in written language, structural units (like chapters, paragraphs, bullet-point lists...) are usually finished with... distinctive whitespace (indentation or vertical space).
Beginnings are usually more prominent (more whitespace, bigger fonts and different alignment, or introductory sentences with colons for lists), just like in Python.
I am only saying that there's no need to make up arguments against significant-indentation. You can like how it looks or not: that's pretty subjective, though.
The only two objective arguments for either side I can see are:
* Terminating "end" or "}" are superfluous if you are already indenting code.
* Re-indenting a block with no termination indicator can lead to subtle bugs (at least more of them compared to when an indicator for block end is present).
They do have it. Quotes (esp multi-paragraph ones) are usually indented another level in. So are nested lists (imagine if lists on different levels of nesting only used different bullet characters). So are mathematical formulae. Or table of contents. Or bibliographic references. Tables in general are a special case of horizontal spacing that matters. Figures and images in text.
It's not as common because writing is not all hierarchical all the time (or at least, lots of space would be wasted if entire chapters/sections were indented one level extra), whereas programming is heavily hierarchical with shorter "blocks".
Horizontal indentation is the most common way to express that hierarchy, including in programming languages that use block termination indicators — indentation is used to help humans parse the code, termination indicators are there to help computers parse it.
You are making it sound as if you are not making use of indentation when programming: I thought the only complaint was about it being significant.
I was a Ruby early adopter - early 2001 - because I needed to move from Perl to a new scripting language (OO Perl just seemed like such a hack and there really was something to the Write Once aspect of Perl - I'd come back to a Perl program 6 months after I wrote it and wonder what the heck was going on). I took one day to play with Python. I got burned by the significant whitespace thing right away. Next day I looked at Ruby and it seemed like the perfect fit for what we were doing. Over the next dozen years or so I was a huge Ruby fan. But then around 2014 I got a different job in a Python shop. I thought I was going to have a rough time with the significant whitespace, but I ended up liking it a lot. That's not to say that I don't miss features from Ruby, but I found that I could get along fine with Python's significant whitespace.
Having used both professionally I love Ruby and I think python is an ok language. However, if there is one thing I do like about Python it is the white space syntax. It forces all coders including the not-so-technical people to write scripts using proper indentation and it makes inheriting Python scripts a little bit easier
> You can have your IDE indent code in the style you prefer automatically.
If you look at significant whitespace through the lens of developer preference, then sure, it sucks. Where it shines is ensuring code is readable by people other than the guy who likes 320 character long lines, and likes aligning everything to the = sign. The only language I know that is as easy to read as Python is Go, and it does so by forcing developers to use go fmt.
Indentation instead of brackets in Python was a masterstroke in my opinion. Despite complaints from folks that don't use it much, and therefore run into an issue here and there due to lack of familiarity.
As a regular user the tradeoff pays great dividends every single day in readability for years on end. I would remove more punctuation if I could… every single new feature to Python seems to include colons lately. :-/
I’m looking at diff between two commits right now where someone decided to fix the indentation in a non-Python language. While the new code is very easy to read, the diff is very difficult to analyze.
Because there are places where indentation signifying intention breaks down.
The classic example being the arbitrary restrictions on the Python lambda syntax.
Ruby is more orthogonal and systematic. Everything is an object, and you call methods on those objects. And sending a block as a method parameter gives you something that very much resembles functional programming (minus the immutability).
There are a few other constructs (it's not quite so dogmatic about object orientation as Smalltalk), but Ruby is very regular and so you can understand how everything works with a small number of syntactic constructs.
The lambda thing is not about whitespace indentation. It is a purposeful decision that if you need more than one line to do it, it should have a name.
At first I disliked this, but it does nudge you towards readability, which winds up to be a good thing. And if you look at it, there is very little space difference between an unnamed lambda and an embedded function:
def lambda_version():
a = lambda x:
line one
line two # note: this sort-of requires the idea of implicit return, which Python does not have outside of lambda
a()
def named_version():
def lambda_replacement():
line one
return line two
lambda_replacement()
A language that uses parentheses does not preclude one from also using indentation to convey code structure. In fact, this is usually standard practice.
Python, however, does indeed preclude one from using additional signifiers of code structure beyond indentation.
And in practice you don’t type out parentheses. You put in a single opening parentheses and your editor will add the closing parentheses and add the appropriate indentation (and if desired, newlines) for you. In practice the only difference in typing is typing a single ( character vs a single <tab> character.
All that being said, this is the very definition of bike shedding, so it’s really not a factor when picking one language over another.
I don't understand what you mean. How are parentheses relevant? Also, personally I hate the parenthesis completion, it gets in the way more than it helps.
By "parentheses" do you mean "brackets"? If so, sure, you can use indentation along with brackets, but why am I now using two things? With Python, all I need to do is backspace every once in a while, to signify the end of indentation, which is very easy to do.
I can copy code marked up with parens and brackets from basically any source (no matter how it's indented or how whitespace is handled) and my linter will automatically understand and format it nicely.
In an indentation language - If I copy a chunk of code that's nested more or less deeply than the section I'm currently working on, I have to manually replace whitespace everywhere. Sometimes the linter is smart enough, sometimes it's not.
If your editor keeps the selection once you paste, you can just hit tab / shift + tab until the indentation's correct. And there's no counting “did I copy the correct number of close brackets”.
I actually find Python easier in this regard, even in editors where I have to re-select everything I just pasted.
You need to spend a couple of days debugging scripts where someone has fucked up the indentation by copying and pasting with a program messes with leading whitespace. Or where someone has inserted an extra whitespace character in a random line in a long script.
I was a TA for a physics lab that used python (3 months)
This came up nearly 100+ times in those 3 months, just in the classes I TA'd for.
Making invisible characters carry significant meaning really throws non-programmers for a loop at times: It's so easy, right until they break it and don't understand how.
If you're honestly having trouble with that, the solution is an editor that shows whitespace. Everything above notepad is capable. I use it with a subtle theme that makes tabs just visible enough to stand out. Indentation guides are helpful as well. Geany is a good choice for beginners, free and x-platform.
Not really - A missing bracket is immediately a parsing error.
Mis-indented whitespace is not.
if(err) {
console.log('something bad happened')
recover()
continue()
Immediately throws an unmatched brackets error.
No such luck for
if err
print("something bad happened")
recover()
continue()
now you can spend minutes trying to figure out why continue isn't being called as you expect it to in the success case. It's obvious enough once you open that specific file and see the indentation, but the tooling is no help at all.
I think that's an unfair comparison: in the first example, you have indentation to tell you what the intent was.
With inexperienced programmers like you bring up, if it was mistyped instead:
if(err) {
console.log('something bad happened')
recover()
continue()
I don't believe that compiler syntax error would help one iota (eg. they might just stick } after the continue() to get the compiler to let the code through).
Significant indentation can throw someone for a loop in a very narrow set of copy-paste mistakes (i.e. where pasted code is still indented in a way to pass the syntax checks, since you can't mix and match number of spaces in Python either).
Though having good editor support is crucial for programming.
> You can just as easily make a mistake in bracketing
I actually strongly disagree with this. There's a reason basically every style guide under the sun defaults to requiring brackets for things like single-line conditionals (where they're optional in a lot of languages): It removes this class of error entirely.
There are lots of other ways to fuck things up with brackets, so no arguments there.
> If you're blindly running software that has never been looked at, tested, and even run once to verify it works, the main problem is elsewhere.
I don't think this is really relevant. In this context - the goal of the student isn't to produce good python code, it's to complete the lab they're working on with their partners. The single biggest case I see where this happens is as follows:
1 - Student A writes some code
2 - Student B writes some code
3 - Student A wants to use what student B wrote
4 - Student A copies the code from student B into their file, but the indentation is wrong.
5 - Student A then starts removing indentation (line by line - which is the real mistake, really) and goes one line too far, or one line too short.
6 - Student A and B test one of the cases for the integrated code (either the success or the failure) and it works fine (in my example, imagine they test the error case and see it does what they expect)
7 - Then later they realize the other case is breaking, but they don't know why: usually because it's been half an hour since they pasted that code into their editor, and checking indentation doesn't immediately come to mind.
There is easy to learn and easy to use, they are not always equal.
They shouldn't be copy and pasting between each other if they don't understand how indentation works. Basics first.
A good editor can help. Geany is good for beginners and can show whitespace. I also recommend the tool pyflakes to everyone, beginner and expert alike.
I also love the cleanliness of Python compared to bracketed languages.
When I was younger and learning the language it seemed to be beneficial in forcing more structure and readability in the code (no ridiculous obfuscation by single line programs).
There was an article the other day showing how to take latex math notes in neovim the other day.
They showed a plugin that displays a render of text on lines that the cursor is not on.
Perhaps there is a way to set that up for languages with brackets? Just show the brackets relevant to that line (and the closing ones perhaps)
I've been thinking a lot about that too. I wonder if we could have tools where the primary artifact is some kind of AST (a concrete syntax tree maybe?) that the tools translate into the language of the programmers choice. Seems sort of like the next step beyond bytecode. But I'm just speculating and daydreaming at this point.
I worked on that a lot (though the native format was XML, don't shoot me) ages ago and ultimately put it aside as any kind of language that is visualized from some format meant to not be read directly has to deal with the issue that you still need a canonical textual representation for communication outside of the tooling. Someone e.g. will copy and paste their esotheric view of the code into Slack or an email.
It's a complex problem. There is a lot of interesting design space to explore there, but that issue needs a solution.
It's not about what the language is fine with. It's about what the developers are fine with. And they'll never agree, so why not give them a choice with tooling?
That's what tab-loving developers are saying the whole time: tab character is that special tooling that can be set by every developer for their own environment. :)
Even though I fully buy that argument, I still prefer spaces, perhaps for the same reasons I like to format my code myself (vs. auto-formatters). Even software engineers can't be all reasonable all the time :)
Do you have "show whitespace" enabled on your IDE?
I think it really helps being able to see the characters properly. (Also helps avoiding mixing tabs and spaces like some bloody animal.)
While there is some value in whitespace-insensitive syntax, I think using whitespace is a neat and human friendly way to keep things separated and to show structure. We are good at spatial thinking
For example this text is using whitespace between words to tell you where which words starts and ends. It uses paragraphs to further split up certain ideas. (Same with math notation which also uses whitespace)
Getting used to new syntax is not as hard as people pretend it is. Never understood the aversion of some people. For Python being whitespace sensitive is a great choice as it helps the pseudocode look of it and is often used as a beginner language. You want beginners to learn how to properly indent code and to suffer if they mix tabs and spaces. (Though maybe now that we have automatic code formatting in many languages, maybe less so.)
> Ruby, however, inverts this. Ruby puts object-orientation as the foundation of the pyramid.
I'm doing some work using Jupyter/Python and as a Rubyst on my day job this is definetly how I felt!
Since data science has so many data manipulation, the move from objects controlling the iterarion from language reserved words doing it was sooo weird at first.
Of course writing an Enumerator is writing the equivalent to an "each" method, hence why you can also trivially turn any object that implements each into an Enumerator.
Prescient. I found myself wanting to pass a block of code to a Python class today, like what I used to do when I wrote Ruby. I very nearly hacked it up using cursors and an iterator but I saw sense before birthing a monster.
are we still doing the whole y vs x thing with languages? why are people wasting time on this? assembler wins. we already know this. the rest is just laziness. /this is sarcasm.
In my opinion it doesn’t matter which language you use as long as it’s the right tool for the job . Usually this goes for familiarity of either who you can hire and or you.
Python has some obvious syntax weirdness and Ruby looks like it encourages more DSL and that might make it closer to lisp.
But, Python has so many more libraries and a good editor can help you manage the white space indentation . Also Python scales well too.
To be honest I rather have some kind of different framing of this. Instead of an argument it could have been a comparison but that doesn’t get people to click the link.
Crystal does look interesting, it also supports parallelism with green threads right? The thing that always bothered me about ruby is that there are multiple ways to do the same thing in the standard library.
Author here. Interesting! I personally write almost Python exclusively and find Ruby to be awkward in many ways. I'm learning Ruby, so if that comes through, maybe cause I'm learning it and more excited about new concepts? :)
"Grand Parent" to refer to a post two levels up, though sometimes it's used contextually to mean whatever-post-that-started-off-this-thread (vs. "OP" or "Original Post(er)" for the top-level post).
IMHO the author successfully remained neutral. Every point about the way one language works was neither criticism nor compliment, merely observation, and was immediately followed by the other language's differentiating take on a particular feature.
In the end it is about preferences. It’s going to be biased given the subject at hand. I do prefer ruby. It’s more object oriented. The language really focuses on this. Example, the plus sign symbol (+) can have
Methods or even be a class or be anything. That’s Ruby.
The s introduced in f is local to the function unless you use the nonlocal or global keyword with it. To do otherwise seems like a really bad idea to me...
The tradeoff here is that you don't have to explicitly declare new variables (something like `var` in js). Most people who work with python on a daily basis know you need the `global` keyword to avoid shadowing.
I think it was a good choice in the sense that it discourages mutable global state and makes the most common case more concise.
I would expect it to print "me too", because I would expect the assignment in the function to change the global variable when it is called, not create another variable.
Your expectation does not match the best practices that language designers have converged on in this day and age. No one can keep up with all the variables outside a given function, and it’s way to easy to cause confusing, untraceable side effects. In almost every language, variables introduced in methods like this will be lexically scoped when the contrary is not otherwise indicated.
(Some of the older languages do differ, and usually best practices in those languages include running linters that yell at you to use explicit scopes.)
Then the function should not use the global variable at all without explicitly indicating it. Printing 2 different variables when referring to a singular one is not great behavior, and passes the smell test for "confusing, untraceable" side effect to me. There should also be a different syntax for initializing a variable and changing a variable value, so that your intention is obvious.
I can see the point a bit of added safety of basically defaulting to a constant when declaring global variables, although since I use older languages (but not python), I have not run into that. I do find it humorous that the global variable is semi-protected in python, while the variable type is not.
> Then the function should not use the global variable at all without explicitly indicating it
Generally you use `global` to handle this.
> I do find it humorous that the global variable is semi-protected in python, while the variable type is not.
It's not, there are no differences between them. Bindings (names) are captured like most other languages, and you're free to overwrite them if you wish.
In your example you bind a new string to an existing name. I think it's pretty expected that this wouldn't suddenly change the global object to a local one, that would be madness.
But you're free to update the object _referenced_ if it's mutable:
Assigning is different than appending, and is the case with a syntax ambiguity. But I'm also not a big fan of the implicit `global` in your example if it's not always implicit.
Fine, we want a test by using `global` to make sure we are doing what we intend when we assign, especially since I see python does not even have true constants so there is a basic lack of safety there. There should also be similar tests before passing every variable with the same name through our function and treating a single name as a list of references without a keyword `all` or similar. Or changing the type of a variable without explicit casting.
To me that lack of continuity just makes a language feel idiomatic and buggy.
I believe Wolfram Mathematica has this terrible global scoping by default.
It's not exactly a language that programmers refer to often, as it's not really general purpose, but I had to do some work with MRI data in it and I hated it mostly due to the scoping.
An "I spent a decent bit of time rewriting the entire lab's codebase in python" type of hate.
I like that better (and I realize I might be alone!). But I don't like that you can declare a global variable in a local function by omitting the var. It's a similar needless ambiguity and frankly much more prone to errors than the python local variable initialization. At least there is strict mode.
I know it's not completely related to the thread - but how is Rails vs Django in terms of performance? I'm getting subjective information that Rails is much faster for APIs in terms of response times, serialization speed etc. although I have been using django for the past 7-8 years and have to say that the best response times was 200-300ms w/o caching for some heavy json responses. And something that should be blazing <100ms, still takes 200ms to respond.
There are too many factors to make a broad comparison in milliseconds. Are you running the code on a RaspPi or on the latest Zen3?
Serialization should be implemented in C so unless the JSON is megabytes in size, whatever is happening here probably has to do with the business logic implemented in Python. Python and Ruby are equally slow when they aren't wrapping frameworks primarily written in C (e.g. tensorflow), so choosing between them is IMO a stylistic or compatibility choice.
The standard alternative to `for` in ruby does involve `each` and blocks... but definitely doesn't involve defining a custom `each` method on your class... That is a specialty thing that most developers have probably done rarely. Let alone defining it in terms of `for`, which is just weird!
But the basic principle stated "Instead of passing data back to the for loop (Python) you pass the code to the data (Ruby)" -- is more or less accurate.
blocks -- syntactic support in the language for cleanly passing a single in-line defined lambda/closure object as an argument -- are possibly the thing that are most special to ruby.
> Python builds on for-like constructs for all kinds of processing; Ruby pushes other kinds of data processing work to methods.
OK, maybe, although not totally sure what you mean.
> Ruby keeps going with its methods-first approach, except instead of each we have a new set of methods commonly implemented on collections, as below:
Um. I'm not sure where the author is getting this. It is certainly possible, as shown, but definitely not common to implement `select` or `map` or other methods provided by Enumerable directly on your custom class. It is a bit more common to implement `each` alone and let the `Enumerable` mixin provide the rest. But even that I'm not sure how "common" I'd call it.
> Ruby, however, inverts this. Ruby puts object-orientation as the foundation of the pyramid. Ruby contains the messy procedural world in blocks, letting objects work with those procedural blocks.
OK, true. The author is getting the details wrong, but I guess their overall picture is still right?