Not to be too nit-picky, but it doesn't actually have a for-loop. The for you linked to is not a for-loop but a for-each loop (simply called for). Well it is actually more restricting even then a for-each loop, as it maps over elements and can only return a collection of the same length as it iterates over.
That said, you're correct it does have a pseudo while-loop. Though it will still force you to use a managed mutable container along with it, which is still safer than without. Also, it's a very ackward while-loop, as it doesn't support continue or break, that's because in reality it isn't a real while-loop, it's a fake while-loop built on top of a recursive loop.
And for all other readers, it's true, Clojure doesn't disallow the use of less abstract less safe construct, it simply makes them more ackward to use and not the default and obvious choice. The reason for this is also something the article didn't bring up, but higher level safer abstractions often come with a performance cost. This is why most languages have escape hatches for whatever safety mechanism they provide, and why Rust lang loves to talk about "zero-cost" abstractions.
Clojure's "for" is really a list comprehension function, but Clojure's "doseq" is more or less what you'd expect from a for loop (like Python's anyway or any other range-based for).
Python's for isn't a for-loop either, it's a for-each loop also sometimes called a for-in loop.
In that sense, neither Python nor Clojure have real for-loops. But Python gets much closer, because Clojure's doseq is similar to its while, it is actually syntax sugar for a recursive loop, and not an imperative loop. While Python's for is an imperative for-each loop, which supports the statements break, continue and return inside it.
Oh sorry, I meant to say iterative, not imperative.
Basically all loops in Clojure are implemented in terms of recursion (loop/recur) and not jumps. Which is why there's no break, continue or return statements.
Whatever categories you assign them in, I was more trying to show the differences between Python's for and Clojure's doseq. And also show that neither are your classic for-loop.
It is iterative too, though: it iterates through an input sequence.
> Whatever categories you assign them in, I was more trying to show the differences
Fair enough. I take your overall point, I just don't think that not being a classic for-loop matters so much in practice, certainly not "most of the time" anyway.
> It is iterative too, though: it iterates through an input sequence.
I don't think these words have supper formal definitions to be honest. I'm using iterative to contrast it against recursive. So in that respect, Clojure's doseq is recursive. You can also see it as iterating over a sequence and thus say it is iterative, but the way it loops over the sequence is through recursion, which is where doseq differs with Python's for. Maybe it's clearer if I say that doseq is recursive while Python's for isn't and just not mention iterative at all haha.
> Fair enough. I take your overall point, I just don't think that not being a classic for-loop matters so much in practice, certainly not "most of the time" anyway.
I don't know what you mean by "matter", but with regards to the article I'm discussing I'd say it does. A classic for-loop is prone to easy to make bugs that are well known, such as "off by one errors", and "overflows". So the fact that the language has you use this higher level abstraction for looping over collections can help prevent a certain amount of such errors and thus provide additional program safety.
Now I don't know if the further differences between Clojure doseq and Python for would also result in safer code. I guess the question is: is the use of break and continue a common source of defect? And is forcing branching instead a safer alternative? Personally I don't know, I'd say maybe not.
That said, Clojure also favours the use of its immutable for or reduce over doseq, and that is arguably much safer, because mutable state inside a for-each loop is also a known source of common defects, like changing the sequence as you loop over it, and especially if concurrency is involved. So that's another relevant practical difference in my mind.
> So in that respect, Clojure's doseq is recursive.
The implementation is recursive, but the user facing interface isn't really.
> I don't know what you mean by "matter"
I mean that in any real world code, at least any that I've encountered in ten years of Clojure and ~20 of Python, the difference has not impacted any actual code that I've written. Sure, in Python I have seen and used "break" but in Clojure it has never been necessary.
> A classic for-loop is prone to...
Ok, so you're actually arguing in favour of non-classic for loops? I certainly agree. Even in C++, I try to avoid traditional loops in favour of range-based loops or the standard library abstractions like std::for_each, std::transform etc. I think maybe we're on the same page, I thought you were arguing that the lack of classic for-loops was a deficiency and I was just saying that in Clojure it doesn't actually matter that its loops aren't the classic ones.
> Now I don't know if the further differences between Clojure doseq and Python for would also result in safer code.
Probably not. I think Clojure does result in safer code, but for other reasons than the differences between doseq/for, as you say yourself. I don't think break is a common source of defect, personally, but I also don't find the lack of break a problem in Clojure: you only use doseq when you need side-effects, why would you need to break/continue? Typically you'll already have filtered the list before calling doseq anyway.
> Personally I don't know, I'd say maybe not.
I agree, I don't think its a big deal either way.
> Clojure also favours the use of its immutable
Yes, I think that (at least in my own experience), Clojure tends to be safer and less error prone, but the reasons are not its loops, they're mainly due to its immutable-by-default nature.
I think that over all, we are more or less in agreement. But I did overlook that doseq doesn't support break/continue in my original comment, so thanks for pointing that out!
Yup, we're in agreement. My point is actually that using higher level abstractions, such as how Clojure does it, leads to safer code, i.e., less prone to bugs and security vulnerabilities. I also personally think it often yield better readability as well, once you get used to the abstractions. And that, when needed, such as for raw performance, there are still escape hatches if need be.
> Yes, I think that (at least in my own experience), Clojure tends to be safer and less error prone, but the reasons are not its loops, they're mainly due to its immutable-by-default nature
Definitely immutability is a big one. Though I think depending what you compare it against, the loops help as well. Like languages where the only iteration is through while or for-loop (the kind that takes a condition). Or those that lack higher level declarative constructs like filter, any?, every?, partition, etc.
Also, related to immutability, it kind of implies loops must be recursive, cause if you think about what a standard for-loop is:
for(i = 0; i < 10; i++) {}
If your language doesn't have (or discourages) mutable variables, how does i work?
I suppose the loops do help in the sense that looping in Clojure tends to favour using higher level abstractions. I don't think I've ever used while, I use doseq only for side-effects, for is for list comprehensions (or for when its more readable than map), but most of my looping is actually through map, reduce, filter/remove, etc. So I suppose you're right because that is definitely less error prone than C-style for loops. So yeah, as you say, the higher level sequence abstractions definitely do help reduce errors in comparison to traditional loops.
> how does i work?
The language can handle the mutability internally, perhaps? That is, the user only sees immutability, but under the hood, the language maintains the state mutably. But I guess that's just an optimisation over recursion since the end-users code looks the same as if it were recursive. Kinda like 'reduce'.
That said, you're correct it does have a pseudo while-loop. Though it will still force you to use a managed mutable container along with it, which is still safer than without. Also, it's a very ackward while-loop, as it doesn't support continue or break, that's because in reality it isn't a real while-loop, it's a fake while-loop built on top of a recursive loop.
And for all other readers, it's true, Clojure doesn't disallow the use of less abstract less safe construct, it simply makes them more ackward to use and not the default and obvious choice. The reason for this is also something the article didn't bring up, but higher level safer abstractions often come with a performance cost. This is why most languages have escape hatches for whatever safety mechanism they provide, and why Rust lang loves to talk about "zero-cost" abstractions.