Ruby vs. Python comes down to the for loop
softwaredoug.com
softwaredoug.com
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?
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.
def proc &p
p
end
is what the special method looks like but yeah they’re not standalone expressions but rather part of the method call syntax.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.
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.
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)
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.)
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.
z = ->(x) { x + 1 }
z.call(y)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.
Ruby doesn't have functions; things that look like bare (non-method) function calls in other languages are just method calls on self.
So procs/lambdas can't be called the same as, or differently from, functions—they are the closest thing Ruby has to functions to start with.
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.
class Stuff:
def __init__(self):
self.a_list = [1,2,3,4]
def __iter__(self):
yield from self.a_list def __iter__(self):
return iter(self.a_list)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!
E.g. why write:
yield from (a*2 for a in self.a_list)
When you can: for a in self.a_list:
yield a*2> 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.
>> [1, 2, 3].each
=> #<Enumerator: [1, 2, 3]:each>
>> e = [1, 2, 3].each
=> #<Enumerator: [1, 2, 3]:each>
>> e.next
=> 1
>> e.next
=> 2
>> e.next
=> 3
>> e.next
Traceback (most recent call last):
2: from (irb):17
1: from (irb):17:in `next'
StopIteration (iteration reached an end)
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. d1 >> play(P["x( x) "].palindrome().zip("---[--]").zip(P[" o "].amen()))Author here! Thanks for the feedback.
I suspected as much, and was more or less writing it to be illustrative. Though I agree I am bad at Ruby :)
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.
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.
And it's a bit overdone in (older) JS (libraries).
In JS:
someObj.someMethod(function(something) {
something
});
Compare to ruby with the block arg: someObj.someMethod do |something|
something
end
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...`
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 someObj.someMethod(something => something);- 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); myList.foreach { x => doTheThingToTheThingEvery(x, 100 millis) }
Of course it's sometimes a bit too much. It starts out cute [1] and handy [2][3][4], then [5] ... [6] :)[0] https://docs.scala-lang.org/tour/basics.html#blocks
[1] https://scastie.scala-lang.org/tpfgE80WTc6QQaHslraJag
[2] https://www.playframework.com/documentation/2.8.x/ScalaActio...
[3] https://stackoverflow.com/q/50370202/44166
[5] https://www.playframework.com/documentation/2.8.x/ScalaActio...
[6] https://miro.medium.com/max/784/1*EMiSTuRIxUPCzWOgLkiXoA.jpe...
val a = { val x = 2; x + 1 }
the syntax you are describing is just a function-valued block. 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.
In Python, `for value in iterator: x(value)` is syntactic sugar for (roughly)
while True:
try:
value = next(iterator)
except IterationError:
break
x(value)
Basically, it seems that `while conditional` are both low-level control flow constructs in both: how do they differ between languages?Python's “for … in” is syntax sugar in the sense that is an abstraction over constantly writing out the iterator protocol.
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].
[0] https://github.com/AndWass/pushgen
[1] https://epage.github.io/blog/2021/07/pushgen-experiment/
I miss this so much. I'm a python dev now working on a big Rails monolith. Where does all this stuff come from?
Drop a debugger statement in above the code in question, then at the prompt turn the method into a Method object using something like:
bar_method = foo.method(:bar)
and then bar_method.source_location
Once you have a Method object you can pass it around as a stand alone function, rebind it or call too.#source_location only works for methods defined in ruby itself i.e. not C or Java but it's still a handy tool on big ruby projects.
Most of the time I just use ctags to navigate new codebases but we also have things like Solargraph via LSP too.
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.
ls foo
show-source -d foo.barIt'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.
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.
[EDIT] Which sucks, because I really like Ruby.
So if you want to always be aware where things come from choose Sinatra, Padrino or Hanami.
Autoloading is one of the worst features ever introduced into a language/framework.
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. :)
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.
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.
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.
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".
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 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).
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.
Admittedly, Rails makes this much worse.
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.
https://docs.microsoft.com/en-us/dotnet/api/system.linq.enum...
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.
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.
Think I'd still prefer ActiveRecord really but after you get used to EF (Core) it's still a good tool.
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.
Stuff.new().each do |item|
puts item
endIn idiomatic ruby, we drop this parenthesis, so the code will look like Stuff.new.each ...
The real magic is when you start doing things like:
some_enum.collect(&:+)
This simple line will do everything from sum numbers to concatenate strings, etc.
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.
(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.)
#map is an alias for #collect. [0]
#reduce has the alias #inject. [1]
[0] https://ruby-doc.org/core-3.0.2/Enumerable.html#method-i-col...
[1] https://ruby-doc.org/core-3.0.2/Enumerable.html#method-i-inj...
I haven't been a professional Rubyist in some years but my habit here was to use inject.
> 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.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
class Stuff:
def __init__(self):
self.a_list = [1,2,3,4]
def __iter__(self):
return iter(self.a_list)
--- or do what the other person did, and make `__iter__` a generator, which isn't necessary in this case but is much more flexible.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.Why not:
each = self._list
Or, if you need to be able to re-assign self._list to a new object: @property
def each(self):
return self._list
Or, if you for some reason need it to return an iterator: @property
def each(self):
return iter(self._list)
Or, if you really want it to be a generator function: @property
def each(self):
yield from self._list class Stuff
attr_reader :list
def initialize
@list = [1, 2, 3, 4]
end
end
Then: Stuff.new.list.each { |item| ... }
Stuff.new.list.map { |item| ... }
Stuff.new.list.select { |item| ... }
And if you want top-level each/map/select methods, you could do delegate :each, :map, :select, to: :list class Stuff
include Enumerable
def initialize
@list = [1, 2, 3, 4]
end
def each
@list.each{ yield _1 }
end
end class Stuff(list):
pass
kinda makes the article pointless. List obviously only serves as an easy to understand example, not something you are actually trying to implement.I’m genuinely confused as to where you picked this up. Enumerator and Enumerable are endemic.
I think for loops are mostly used in Ruby by people that picked it up in other languages first and simply never unlearned it.
https://journal.stuffwithstuff.com/2013/01/13/iteration-insi...
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.
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.”
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.
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...
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'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.
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.
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.
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.
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.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 :)
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.
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.
but it can be done and goes to show that it's not going to be putting optimization experts out of work yet.
reading about the state of autograd libraries in julia seems to indicate the same.
for (var i : list) {...}
or list.forEach(i -> ...);
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.This fails at runtime, not compile time for me:
x.forEach(i -> x.add("a"));Bottom line: external iteration is composable, internal iteration isn't. Try implementing zip or early stopping using internal iteration (hint: you can't).
https://journal.stuffwithstuff.com/2013/01/13/iteration-insi...
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)
Then you can turn any internal iterator into an external one.
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.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.
> 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.
If FP was the norm, then you could make the same argument for OO.
- 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:
https://twitter.com/DNAutics/status/1459348334007787526?s=20
https://twitter.com/shidonichan/status/1457795846750097408?t...
d.map(i => o[i] = i\*2)
Will produce a new array on top of mutated data, o, in the parent scope. d.forEach(i => o[i] = i**2)
would be the one-to-one equivlentI 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.
"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 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.
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.
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?
Yet Python is a dynamically typed language and translating it into many statically typed languages requires and almost complete rewrite.
Translating it to Nim is surprisingly easy in comparison.
> When I want the length of an array
len(array)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.
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'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
if condition: return expression if condition: foo = value
or
if condition: body statements elif condition: body statements else: body statements
two spaces
to get your line breaks to show up.• I was talking about Hacker News markup.
• Python officially recommends spaces: https://www.python.org/dev/peps/pep-0008/#tabs-or-spaces
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." (!)
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'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.
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).
So that's why I prefer Python at the moment.
https://nixpulvis.com/ramblings/2020-03-16-set-builder-notat...
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.
So Ruby using end instead of brackets or white space is one of my favorite features of the 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.)
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.
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).
In programming things are frequently nested several levels deep.
This is the key difference that makes a termination character/word important I think.
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.
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.
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. :-/
No, I don't miss brackets everywhere one bit.
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.
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()That's actually because Guido thinks multi-line expressions are messy to parse, rather than because they can't be done. Consider:
f = lambda x:
return x + 5
Or, perhaps: print(sum(map(lambda n, b:
while n:
b.append(n)
n -= 1
return b
, range(3), [[], [], []]), []))
You can see why this was never added to the language, though.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.
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.
I actually find Python easier in this regard, even in editors where I have to re-select everything I just pasted.
I choose to believe that this means S-expressions.
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.
Black can also diagnose/fix some issues.
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.Let's have it handle tabs or spaces while we're at it. The answer could always be "whatever you prefer".
My kingdom for better tooling!
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)
It's a complex problem. There is a lot of interesting design space to explore there, but that issue needs a solution.
As long as you don't mix tabs and spaces in the same file python is fine with either.
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 :)
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)
{[Do; you; think; that; this; is; easier; to; read; ?][Does; it; feel; more; comfortable;?]}
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.)
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.
$ txr stuff.tl
1
2
3
4
$ txr stuff-cons-like.tl
1
2
3
4
$ cat stuff.tl
(defstruct stuff ()
(vec (vec 1 2 3 4))
(:method length (me) (len me.vec))
(:method lambda (me index) [me.vec index])
(:method set-lambda (me index val) (set [me.vec index] val)))
(each ((x (new stuff)))
(prinl x))
$ cat stuff-cons-like.tl
(defstruct stuff ()
(list (list 1 2 3 4))
(:method nullify (me) me.list)
(:method car (me) (car me.list))
(:method cdr (me) (cdr me.list)))
(each ((x (new stuff)))
(prinl x))
Quit with :quit or Ctrl-D on an empty line. Ctrl-X ? for cheatsheet.
1> (disassemble (compile-toplevel '(each ((x (new stuff))) (prinl x))))
** expr-1:1: warning: new: stuff does not name a struct type
data:
0: stuff
syms:
0: struct-from-plist
1: iter-begin
2: iter-more
3: iter-step
4: iter-item
5: prinl
code: ;; manually annotated!
0: 2001000A gcall t10 0 d0 ;; t10 <- (struct-from-plist 'stuff)
1: 04000000
2: 20010009 gcall t9 1 t10 ;; t9 <- (iter-begin t10) ; t9 is iterator
3: 000A0001
4: 20010006 gcall t6 2 t9 ;; t6 <- (iter-more t9) ; more items?
5: 00090002
6: 3800000F if t6 15 ;; if t6 not nil continue, else goto 15.
7: 00000006
8: 2001000A gcall t10 4 t9 ;; t10 <- (iter-item t9) ; get item
9: 00090004
10: 20010006 gcall t6 5 t10 ;; t6 <- (prinl t10)
11: 000A0005
12: 20010009 gcall t9 3 t9 ;; t9 <- (iter-step t9) ; step iter
13: 00090003
14: 34000004 jmp 4 ;; repeat
15: 10000000 end nil ;; terminate, yielding nil
instruction count:
9
#<sys:vm-desc: 14fcb80>A fairly concise example: https://garycordero1690.medium.com/ruby-enumerator-74b84615f...
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.
And "the right tool for the job" is of course language one knows and loves ;)
Crystal don't have the same tradition of having aliases of common methods as Ruby does, no.
You've highlighted the idiomatic differences in the language design and how each lends itself to a different style of thinking. Neat.
s = "Me too."
print(s)
# Global scopes = "I love Geeksforgeeks"
f()
print(s)
This one returns `I love Geeksforgeeks`
Holy mogy, God bless someone doing codereview or inherit python code from some bad developers.
Me too
I love Geeksforgeeks
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... def f_global():
global s
s = "Me too."
print(s)
print(s)
f_global()
print(s)
Will print: I love Geeksforgeeks
Me too.
Me too.
Of course... please don't do global mutable variables.I think it was a good choice in the sense that it discourages mutable global state and makes the most common case more concise.
(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.)
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.
I think the example here is misleading and causing confusion.
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.
Neither are considered pinnacles of language design.
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.