Impressive benchmark, thank you. I want to use Neanderthal at work, just didn’t have the opportunity yet.
On a slightly unrelated note, I would be interested in implementing something like FAISS (https://engineering.fb.com/data-infrastructure/faiss-a-library-for-efficient-similarity-search/) in Clojure, Neanderthal should definitely be appropriate for all the similarity calculations, correct?
the link is broken, I had it working by swapping the &
- https://aiprobook.com/deep-learning-for-programmers&release=0.8.0&src=lobste.rs
+ https://aiprobook.com/deep-learning-for-programmers?release=0.8.0&src=lobste.rs
Things like this are why I don’t trust Martin’s opinions. He didn’t say a single bad thing about Clojure, he didn’t have any nuance, he doesn’t respect the other viewpoints. He does the same thing with TDD and clean code, where it’s impossible for them to be the wrong tool. He’s been programming for five decades and still thinks in silver bullets.
For the record, his Clojure example is even shorter in J. It’s just *: i. 25
.
His example is shorter even in Clojure:
(map sqr (range 25))
but that misses the point. Both Clojure examples are easy to explain and understand, where in J it is not obvious what *: and . stand for, and how these should be changed should we wanted to compute something different. But even that is not the point.
The point is that Uncle Bob is writing about his own experience with the language that he finds fascinating. He writes about his experience and he gets to choose how he writes it. If anyone disagrees (plenty people do, I suppose) they are very well entitled to write about their experience themselves.
I don’t want to sound like an asshole, but what exactly is his experience besides teaching and writing books ? Cause we see so many people advocating for specific language/technology without any substantial real world experience.
As professional advocates go, he’s well known and (at least be me) well regarded.
A professional advocate advocating for something is a signal too… and a lot of the things he was advocating 25 years ago are still relevant today.
http://web.archive.org/web/20000310234010/http://objectmentor.com/base.asp?id=42
A professional advocate advocating for something is a signal too
Yes, it’s called Appeal to Authority.
I’m also not convinced he’s much of an authority. I’d say he’s a zealot. His tirades against types are tired. His odes to discipline are masturbatory. His analogies… well… This is the same guy who said C++ is a “man’s language” and that you need big balls to write it.
His analogies… well… This is the same guy who said C++ is a “man’s language” and that you need big balls to write it.
This is called an ad hominem. If you’re going to be a stickler about logical fallacies I’m surprised that you can’t even make it a few sentences without contradicting yourself. Are they important or not?
A professional advocate advocating for something is a signal too
This is called inductive reasoning. Given some evidence, such as a well-regarded professional advocating for some tool, we can try to generalize that evidence, and decide the tool has a good chance of being useful. You’ve surely heard of Bayesian probability; signals exist and they’re noisy and often incorrect but minding them is necessary if you want to make any sense of the world around you.
Yes, it’s called Appeal to Authority.
Logical fallacies only really apply when you’re working in the world of what’s called deductive reasoning. Starting from some premises which are assumed to be true, and moving forward using only techniques which are known to be sound, we can reach conclusions which are definitely true (again, assuming the premises). In this context, the one of deductive reasoning, appeal to authority is distinctly unsound and yet quit common, so it’s been given a nice name and we try to avoid it.
Tying it all together, the parent is saying something like “here’s some evidence”, and you’re interjecting with “evidence isn’t proof”. Great, everybody already knew that wasn’t proof, all that we’ve really learned from your comment is that you’re kind of rude.
Fallacies can apply to inductive arguments too, but you are right in that there’s an important distinction between the two types and how they differ. I would say that the comment you’re replying to is referring to the idea of informal fallacies in the more non-academic context. The Stanford encyclopedia has a good in-depth page about the term.
Also, not all fallacies are equal, appeal to authority may be seen as worse than ad hominem these days.
This thread started with, “Things like this are why I don’t trust Martin’s opinions.” Uncle Bob’s star power (or noteriety) and whether that qualifies as social proof or condemnation, is the point of the discussion, not a distraction.
The point is that Uncle Bob is writing about his own experience with the language that he finds fascinating. He writes about his experience and he gets to choose how he writes it.
I wouldn’t be complaining if he was just sharing a language he liked. The problem is he’s pushing clojure as the best language for (almost) everything. Every language has tradeoffs. We need to know those to make an informed decision. Not only is he not telling us the tradeoffs, he’s saying there aren’t any! He’s either naïve or disingenuous, so why should we trust his pitch?
The problem is he’s pushing clojure as the best language for (almost) everything.
That’s not what he said though. The closest he came to that is:
Building large systems is Clojure is just simpler and easier than in any other language I’ve used.
Note the qualification: ‘… than any other language I’ve used’. This implies there may well be languages which are easier for building large systems. He just hasn’t used them.
Not only is he not telling us the tradeoffs, he’s saying there aren’t any!
He repeated, three times for emphasis, that it doesn’t have static typing. And that it doesn’t give you C-level performance.
Note the qualification: ‘… than any other language I’ve used’. This implies there may well be languages which are easier for building large systems. He just hasn’t used them.
We need to consider the connotations and broader context here. He frames the post with
I’ve programmed systems in many different languages; from assembler to Java. I’ve written programs in binary machine language. I’ve written applications in Fortran, COBOL, PL/1, C, Pascal, C++, Java, Lua, Smalltalk, Logo, and dozens of other languages. […] Over the last 5 decades, I’ve used a LOT of different languages.
He doesn’t directly say it, but he’s really strongly implying that he’s seen enough languages to make a universal judgement. So “than anything other language I used” has to be seen in that context.
Nor does he allow special cases. Things like
But what about Javascript? ClojureScript compiles right down to Javascript and runs in the browser just fine.
Strongly connotating that “I’m writing frontend code for the web” is not a good enough reason to use Clojure, and he brushes off the lack of “C-level performance” with
But isn’t it slow? … 99.9% of the software we write nowadays has no need of nanosecond performance.
If Clojure is not the best choice for only 0.1% of software, or even 5% of software, that’s pretty darn close to “best language for (almost) everything.”
He repeated, three times for emphasis, that it doesn’t have static typing.
He repeats it as if the reader is hung up on that objection, and not listening to him in dismissing it. Note the increasing number of exclamations he uses each time. And he ends with
OK, I get it. You like static typing. Fine. You use a nice statically typed language, and I’ll use Clojure. And I’ll be in Scotland before ye.
Combined with his other posts (see “The Dark Path”), he doesn’t see static typing as a drawback. We can infer it as a drawback, but he thinks we’d be totally wrong in doing so.
You have to explain both examples for them to make sense. What does map
do? How do you change sqr
out for a different function? If you learn the purpose of the snippet, or the semantics of each of the individual elements, you can understand either the J or Clojure example just as well as the other (if your understanding of both languages is equal).
Also the meat of the article is trying to convince the reader to use Clojure (by explaining the syntax and semantics, comparing its syntax to two of the big 5 languages, and rebutting a bunch of strawman arguments - nothing particularly in-depth). I don’t see a balance of pros and cons that would be in a true account of an experience learning and using the language, including more than just a bullet point on the ecosystem, tooling, optimisation, community, etc.
I am sure that any programmer that has any experience in any language would guess that you change sqr out for a different function by typing the name of that other function. For example, you compute exp instead of sqr by, well, typing “exp” instead of “sqr”.
The same with map. Of course that someone has to know what particular function does to be able to use it effectively. The thing with Clojure (and other Lisps) is that it is enough to know that. You don’t need special case syntax rules. Any expression that has pretty much complex semantics is easy to write following a few basic rules.
I understand the benefits of the uniformity of Lisp, but my point was just that you can’t really say that (map sqr (range 25))
is any more or less understandable than *: i. 25
if you know the purpose of the expressions and the semantics of their constituent parts. And given that knowledge, you can reasonably make substitutions like exp
for sqr
or ^:
for *:
(though I would end up consulting a manual for the exact spelling).
Further experimentation would require more knowledge of either language. For instance, why if
isn’t a function in Clojure, or why lists don’t have delimiters in J. It’s all apples and oranges at this superficial level.
My version of Clojure doesn’t define sqr
—is that built in?
That aside, I don’t find either version very easy to explain to someone who isn’t already experienced with functional programming. What does “map” mean? How does it make sense that it takes a function as an argument? These seem obvious once you’ve internalized them, but aren’t easy to understand from scratch at all.
If I were reviewing this code, I would suggest they write (for [x (range 25)] (* x x))
Of course that one has to understand the semantics of what they’re doing. But, in Clojure, and Lisps it is enough to understand the semantics, while in most other languages, one has to additionally master many syntax rules for special cases.
Closure has quite a lot of special syntax compared to many Lisps. for
example, data type literals and other reader macros like literal lambdas, def
forms, let
forms, if
forms and other syntax macros like ->
are all built in. Each of these has their own special rules for syntax and semantics.
We’re on the same page I think, except that I think knowledge of semantics should be enough to understand any language. If you see a verb and a noun in close proximity, you’d be able to make a good guess as to what’s happening regardless of the glyphs representing their relationship on screen.
If you want a language that emphases semantics over syntax, then APL is the language for you! There are just a few things to understand about syntax, in order of importance.
¯
. Some dialects have special-case syntax for complex or rational numbers: 42 3.14 1J¯4
''
quotes. Doubling the quote inside an array escapes it: 'is'
or 'isn''t'
[]
braces: 'cafe'[3 2 1 4]
←→ 'face'
(Many APLers have a disdain for this form because it has some inconsistency with the rest of the language.){}
braces.⋄
(Mainly useful for jamming more code into a single line)From there, the grammatical rules are simple and natural in the form of verb noun
or noun verb noun
or verb adverb noun
etc. Probably the most difficult thing to learn and remember is that there is no operator precedence and evaluation reduces from right-to-left.
When I’m programming in APL, I rarely think about the syntax. When I’m programming in Clojure, syntax is often a concern. Should I use map
or for
? Should I nest these function calls or use ->
?
When I’m programming in Clojure, syntax is often a concern. Should I use map or for? Should I nest these function calls or use ->?
None of those are syntax. map
is a function and the rest are macros. They’re all inside the existing Clojure syntax.
Macros can be used to define syntactic constructs which would require primitives or built-in support in other languages. [my emphasis]
True enough. However, at least in Clojure, macros are pretty deliberately limited so as not to allow drastically changing the look-and-feel of the language. So I’m pretty sure every macro you’ll come across (except I guess reader macros) will have the same base syntax, (a b ...)
.
Currying makes sense in Haskell/ML, but with very few exceptions, it just doesn’t fit in to Clojure. The obvious argument is that Clojure, like many other languages, chooses a syntax that prefers variadics to the exclusion of currying by default.
More importantly, currying encourages patterns that should be avoided. For example, functional combinators and monadic interfaces thread a context argument in the rightmost position. For one thing, these patterns produce unprintable/opaque closures, which deeply hinders debugging. That’s a shame in a language that ships with a pretty printer for - and encourages use of - a common, closed, syntactic data structure (EDN). For another thing, Clojure is impure, so you can just have effects, rather than introduce the extra complexity of monads which make it much easier to accidentally produce lazy or duplicated effects (since the monadic values are reified, the implicit linearity of imperative constructs is lost). Instead of combinators or monads, just write a direct-style, effectful interpreter for some EDN-encoded value, pass a context map as the first argument, if you need to.
Never mind the fact that currying introduces lots of anonymous values, which can easily lead to the sort of confusion that you’d want a type checker to alleviate!
This version of curry supports variadic arguments. Other issues you’ve mentioned may be relevant or not, but are the same issues that Clojure’s partial has.
As for the monads and effects, monads are not only about effects, but more generally about threading context, so although they are less needed in Clojure than in Haskell, they can be useful.
This version of curry supports variadic arguments.
It only “supports” defaulting to an arity of 2 and allowing you to declare a higher fixed arity. Currying and variadic function application are fundamentally incompatible.
the same issues that Clojure’s partial has.
I think that partial
and comp
are overused in Clojure as well. For top-level declarations, the syntactic overhead of using named parameters serves as useful documentation and you get the right code-reloading behavior implicitly through var resolution. Otherwise, you have to remember to (comp #'some-named-var #'some-other-named-var)
to avoid some confusion later. For inline functions, the #(....%....)
shorthand syntax is still quite short and much clearer and more flexible than either comp or partial.
monads are not only about effects, but more generally about threading context
I think my comment covered this, but I’ll restate more concretely: You should pass “pure” context via a map as the first argument; and you should pass “impure” context as either thread-local global vars or by putting a reference type in your pure map.
The interplay of currying and context threading is that you can define methods on initial parameters to delay computation and then provide the context later to force evaluation. In a strict, impure language, it’s much preferable to just do a thing rather than to build up an opaque structure that does a thing. If you actually need delayed and context-varying execution, it’s much better to design a transparent structure to be interpreted. Then the context can be threaded through the interpreter directly.
Have you checked if it makes a difference? It’s a pretty big confounding factor here.
Put another way, if it turns out to add 500ms of latency and the “native numpy” is faster than your optimized clojure, does that change your argument at all?
Yes, I have, and no, it doesn’t make a difference. But, as I said, if I’m wrong, you could easily debunk my argument by firing up your Python interpreter and doing this yourself.
It’s less that “I’m debunking your argument” and more “I’ve seen way too many benchmarks completely ruined by problems like this, so it bothers me that you’d intentionally call out a problem and then not address it.” Like you’ve presumably used a pure python script to confirm it doesn’t make a difference, so why not include that too?
Because even if I did do that, someone would demand that I prove that I did not run 30 YT videos and a 3D shooter in the background. Why would you have to trust me at all? The point of my article is that you should not trust any blanket advertising, and should check whether what someone claims, be it me, the CuPy project, or Google, really works as advertised when /you/ use it on /your/ hardware.
But yeah, I know my tools, and I know that you’d get identical results from a pure python script. I’ll write a follow-up post with additional interesting results, and I’ll include a profiler report that clearly shows where the time is spent.
That may have been your intention, but the article reads like the point is “look how great Clojure and Neanderthal are!” Which is totally fine! We’re all proud of the tools we make. But it also means you shouldn’t be idly brushing off any issues that make the comparison look better for you than they actually are. It shows bad faith. You say
Doing your absolute best to eliminate any possible confounding factors, and showcase why there’s a difference between Numpy and Neanderthal in the end, would be a significant improvement to the article.
(And if your goal was not “Neanderthal is great” but “don’t blindly trust benchmarks”, then making that explicit in the first paragraph would be a significant improvement, too. As it stands, opening with “Clojure kicks ass on CPU” makes it pretty explicit that this is a post about how great Clojure is!)
But look, these NumPy and CuPy measurements are faster when run from Clojure REPL via libpython-clj, than when run from Python console or editor. If I included that info, would that make people leave these tangential arguments aside? Probably not. Someone would point out that of course that Python interpreter introduces lots of overhead, but that I need to go out of my way to circumvent that by using some special tool XYZ to mitigate that noise.
It’s a lot simpler. NumPy and CuPy are very useful tools. They are definitely infinitely more performant than the Python code normal users would write themselves. But compared with better programming environments, they also leave some improvements on the table. Maybe NumPy users don’t care about that. It’s up to them. But there are many programmers who would love to be able to have similar functionality without Python.
It is interesting to me to compare my favorite tools to these libraries since many (even non-python) programmers blindly think that everything you write with NumPy/CuPy/PyTorch/TensorFlow would be perfect. It is not my job to fix Python’s issues.
And, I seriously mean “send your comments”. I will write a follow up article in which I will clarify this additionally. But I can’t make NumPy/CuPy better than they are…