Using parsing and abstractions for safety is the basis of why Clojure, even though dynamically typed, is a relatively safe language[1]. The abstractions are safer in Clojure: no for loops, no while loops, only higher level iteration and transformation constructs, data interfaces are all immutable and functional. Shared state can only be used through higher level managed APIs, numbers auto-promote themselves to higher precision, there's no linear search APIs to force the use of indexed data-structures instead, etc.
And for parsing: Spec is a way to define contracts, but it's actually a parser DSL. User defined macros which specify a Spec will have their input parsed by the Spec at read-time, and if the Parser fails, a read error will show up. Unfortunately, this only happens for macro, but the article made me curious about possible value of having this done on functions as well.
An example of this is say you have some macro which would let you do:
(:from coll :take 5)
Your Spec can parse this, and error if the first element isn't :from, the second element isn't a reference or a collection literal, the third element isn't :take, the fourth element isn't a reference or an int, as well as if it isn't called with exactly 4 elements. And all this happens at read time, when the code is parsed. You could go further, and if a collection literal was used, you could assert that the take int isn't bigger then the number of elements in the collection literal.
Now, what I've observed is that reference types kind of limit what the parser can do. I'm not sure the article addresses that. Like in my last example, I can validated the range of take over the size of the collection, but not if the collection is a variable. How do I track those references? Maybe somewhere else in the program you can find the collection literal, but not here.
P.S.: It's hard to know why Clojure demonstrates relative safety, and I assume here it has a little bit to do with the abstractions it uses and this parser Specifying mechanism. But it's also likely it's all due to the REPL or something else.
Clojure, for those unfamiliar with the language, unsurprisingly, does in fact allow for both 'for' and 'while' loops, and are part of the core language library.
What clojure does do is also provide a number of higher level control constructs, and, by the nature of the syntax of the language, puts them on the same level as 'while' and 'for', which themselves are part of a library, rather than baked into the language itself.
This can, of course, be done in other languages, using fluent interfaces.
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'.
Sibling thread went into the nature of things in Clojure that are not "true" low level loop constructs, so just to point out, Clojure does also have loop/recur where you have more control on looping.
And for parsing: Spec is a way to define contracts, but it's actually a parser DSL. User defined macros which specify a Spec will have their input parsed by the Spec at read-time, and if the Parser fails, a read error will show up. Unfortunately, this only happens for macro, but the article made me curious about possible value of having this done on functions as well.
An example of this is say you have some macro which would let you do:
(:from coll :take 5)
Your Spec can parse this, and error if the first element isn't :from, the second element isn't a reference or a collection literal, the third element isn't :take, the fourth element isn't a reference or an int, as well as if it isn't called with exactly 4 elements. And all this happens at read time, when the code is parsed. You could go further, and if a collection literal was used, you could assert that the take int isn't bigger then the number of elements in the collection literal.
Now, what I've observed is that reference types kind of limit what the parser can do. I'm not sure the article addresses that. Like in my last example, I can validated the range of take over the size of the collection, but not if the collection is a variable. How do I track those references? Maybe somewhere else in the program you can find the collection literal, but not here.
P.S.: It's hard to know why Clojure demonstrates relative safety, and I assume here it has a little bit to do with the abstractions it uses and this parser Specifying mechanism. But it's also likely it's all due to the REPL or something else.
[1] https://www.i-programmer.info/news/98-languages/11184-which-... - Shows Clojure as having some of the least number of defects per commits