Why no pattern matching?...I'd much rather use (defrecord Color [r g b a]) and end up with maps with named parts than do color of real*real*real*real, and have to rename the parts in patterns matches everywhere (was it rgba or argb?)
Not sure why he is comparing the best practice in Clojure against the worst practice in ML; let's compare with Scala to be fair: case class Color(r: Double, g: Double, b: Double, a: Double). This also avoids the type ambiguity of Clojure's [r g b a], what are r, g, b, a? What can I do with them? The Scala case class tells you immediately.
Yeah this is by far the worst part of the whole document. These arguments make no sense; none of the problems he's describing actually ever happen when pattern matching in a lisp.
Agreed, but Scala was already discussed earlier in the document, and it's the other big FP language on the JVM, so I wanted to be as apples-to-apples as possible.
Right, I think Rich's point is more like... people tend to not do named field matching when pattern matching in a lot of code, right? Like there's the ability to avoid the issue, but if people are still writing code in the lightweight non-named style, they're still running up against this I think
Looking at report_error,you can seen a bunch of destructuring directly pulling out values. You can in particular see a lot of extracting of len and pos.
Now in the unvierse where these are very different types, unintentionally swapping them out leads to a mess. In a world where the types are the same (or, well, if you're using something like Clojure) you could just flub the positioning on a destructure of one of these elements right?
I am not really a Clojure expert, but I think Rich's argument here is that if you're instead writing something like len as an accessor function, then you can do len(item) and be sure that you have extracted the right value.
Now I do agree with a sentiment that in a more typed system you're less likely to flip stuff around. And perhaps you can set up your linter/autoformatter to automatically fill in the names to avoid the tedium and repitition of what you describe. I do understand Rich's point about ADT destructuring being kinda flimsy when used in a lightweight fashion!
There is definitely a "Rich's view on these languages are colored by him not touching them enough". An argument I'm amenable to at times. But his world is also a different world from other people's worlds, and his concerns are clearly different.
He also says he doesn't mind destructuring, and you can easily do:
=> (let [[r g b a] [:a :r :g :b]] r)
:a
In any case this sounds more like an argument against positional patterns than pattern matching in general, which also fits with his general recommendation of a la carte polymorphism with hmaps. Seems to me his main gripe is with the switch like nature of pattern match expressions, as he prefers dynamic dispatch. But IMO this is missing the point: sometimes you don't want things open to extension at any time. As a contrived example, if you had an Optional you don't need that to be open to a 3rd or 4th state. And sometimes you want compilation to fail if a new variant is added without being explicitly handled everywhere, although for a dynamic language like Clojure that doesn't make as much sense.
I get what you're saying, and do agree that I think his argument is against positional matching more than anything (and he wouldn't have much to stand on if we lived in a universe of "verbose" pattern matching on names only)
I do think his argument broadly is less about "we should have extensionality" and more "we shouldn't spend time on structural restrictions when we can design programs that don't require those restrictions". An argument for terseness (based on the sort of axiom of "less code -> less bugs", IMO, which ... I don't fully agree with but I get!)
To be clear, I spend much more of my time in the other camp, but every once in a while I'll write up some code in the "less structural restriction" space and it is nice sometimes. But hey, there's a reason we talk about Clojure being a write-only language
I have a better argument against pattern matching: it locks you into a particular representation of your values and makes you optimize for use-site ergonomics rather than performance, which is not always the right tradeoff.
For example, let's say I'm implementing a type for JSON values. If I want to use pattern matching I have to do something like:
type JsonValue:
Array(Vec[JsonValue])
Map(Map[Str, JsonValue])
String(Str)
Bool(Bool)
Number(F64)
Null
Which makes my values have a certain memory layout which I can't change.
If I want to use some kind of allocator/pool for objects, or have a single large memory allocation for all variants (and reuse the payload as different values of different types based on the tag), I can't do this with this representation.
Instead of I keep JsonValue abstract and have test and accessor functions, I have more flexibility.
For this problem, in my language I'm planning to allow "overloading" the pattern matching syntax so that you can keep your representation abstract but still allow pattern matching. It requires value types/unboxed values so that generation of the pattern-matched values can be done without allocation, you wouldn't want match foo: ... to allocate.
Overloaded pattern matching sounds like F# banana clips or Haskell view patterns if you needed further inspiration. I agree with the approach - it feels less clunky than regular ol' ADTs, especially for things exactly like reading json, as you say.
Which makes my values have a certain memory layout which I can’t change.
You can change it. Just keep the representation internal and don't expose it to the outside world. You can use pattern matching internally in the module and expose combinators to users.
I've dealt with this when designing the Value type for my scheme implementation. Practically, you want the Value to be easy to access the various different types of, but you also want to keep it opaque so that you can optimize the representation.
My solution was to split out Value into Value which is opaque and can only be converted to specific types via TryFrom, and the UnpackedValue type which is an enum as you describe. Value can be converted to an UnpackedValue at a cost, but this is worth it if you're dealing with multiple different types. You don't pay anymore cost than if you'd match on the tag and convert them individually, but you get the nice ergonomics of pattern matchinng.l
Scala has unapply which similarly allows for defining up a pattern match overload I believe.
It's not really documented but I believe you can do some similar tricks with Python objects and match. Given how controversial match was I'm sure nobody wanted to bring up any potential dark magic you could do to pattern match your custom class on a list
But I do know this - it is seriously unimportant. We all have much better things to do, I hope, than argue over things like this.
This is a bad argument. If Rich wasn't so precious about Clojure, there would be many people willing to write and test an efficient polymorphic version of last, as seen by the numerous ignored patches on forgotten tickets in Clojure's jira. This would benefit everyone who uses last for ~no cost. Until then, last will continue to be another footgun, something that is "fine" until suddenly it's not and you're left wondering why the hell it doesn't work as expected. (Especially in light of his comments about not providing a lookup function because of performance worries.)
Clojure is great, truly my favorite language. But it could be so much better if the core team was willing to let go of the reins a little.
That's a bit odd. You can do (cons x some-vector) and it'll be O(n), which is probably what you don't want. In fact, it's a common pitfall for beginners.
You can't just swap out sequences or lists for maps or sets in a program. So, you need to use the right data structure for the job, and then the right functions will be available. When it makes sense, some functions are maximally polymorphic (e.g. seq, or into). But lookup, under any name, shouldn't be, IMO, so it isn't in Clojure. Similarly there is no insert at the beginning of vectors, append to end of lists, lookup for values of maps, insertion in the middle of sequences etc.
And he's right, I have fixed the following N^2 bug MANY times in other people's Python code:
for x in mylist:
if x in otherlist:
pass
It's quadratic, but there's no double loop. On the other hand, this algorithm is linear:
for x in myset:
if x in otherset:
pass
I'd go as far as to say this is the most common Python performance bug I've encountered.
The key is the broken polymorphism -- you should not use the same syntax in for an operation that is either O(1) or O(n).
O(1) and O(n) operations should have a different spelling, to make it clear they have different performance characteristics
I might go even further and say that if you've used Python for say 10 years, and you haven't fixed this bug, there's a good chance you have it and don't realize it :-)
At least for web and data science stuff, where it tends to come up
Actually this is pretty much the same point I made the other day about SQL - some joins are inherently O(M x N), but they don't have a sufficiently different syntax than fast joins IMO
It's also common in practice for SQL queries to "blow up" in running time. And then the authors of those queries have to spend a lot of time figuring out why, which is not obvious from looking at the code
If the syntax were more suggestive, that could help
And I should add that this same philosophy made its way into the YSH language design:
ysh-0.35$ = 'foo' in {'foo': 42}
(Bool) true
ysh-0.35$ = 'foo' in ['foo', 'bar']
= 'foo' in ['foo', 'bar']
^
[ interactive ]:4: fatal: RHS of 'in' should be Dict, got List
ysh-0.35$ = ['foo', 'bar'].indexOf('foo')
(Int) 0
That is, we don't have "broken polymorphism" for in. Instead, you use in for the O(1) operation, and mylist.indexOf() for the O(n) operation. (the indexOf() name matches JavaScript)
In context it's clear he's talking about defaults, in the sense of providing clojure.core functions that would naturally lead developers to poor algorithmic complexity categories. The sentence prior:
there is no insert at the beginning of vectors, append to end of lists, lookup for values of maps, insertion in the middle of sequences etc.
Cool, thanks! I just discovered that the GitHub app does not support gists, strange.
I've played around with Clojure to do some Advent of Code problems, I found that getting a good 'feel' for what was performant code was actually quite difficult! I'm not sure what kind of things I should try out to really benefit from Clojure? Kinda hard to not be excited after listening to (or reading in this case) one of Rich's talks.
Definitely recommend joining the clojure slack (or zulip) if you're curious about the language/ecosystem- everyone's there more or less (once in a blue moon even Rich will show up, but some of the core team are also regulars).
Getting an integrated REPL setup is the path to the best experience with Clojure (and any other lisp), how you go about that is gonna be dependent on your editor of choice: CIDER for emacs, Conjure for neovim, Calva for vscode, (forget what the intellij one is, but there's something).
A combination of those two things will give you the opportunity to absorb some good overall idioms and 'theory' of the language :^)
Why does clojure not allow specifying new syntax like common lisp?
The argument seems to be reader macros don't compose... but doesn't Racket demonstrate they can? You can even mix #lang in the same file with an escape hatch IIRC
I've complained about it before, but I don't think Rich has a great understanding of what made the existing Lisps so great, and that answer seems to be another case of that.
At the risk of over-simplifying, a function (defun) is a function that runs at runtime, a macro (defmacro) is a function that runs at compile time, and a reader macro is a function that runs at read time. It's nothing to do with creating multiple incompatible readers.
For example, #(1 2 3) is a standard (meaning it's in the CL standard) reader macro that transforms into something like, (make-array 3 :initial-contents '(1 2 3)) There are JSON syntax reader macros, so things like (defparameter *foo* #J({"foo": 42, "bar": 42})) will expand into (defparameter *foo* (make-json :entries '(("foo" 42) ("bar" 42)))
Maybe somebody else chooses #J() to mean something else, but that's a risk with normal functions, too, and there are solutions that work around it.
The way I phrased that makes it sound like an ad-hominem, and it really shouldn't have been. It's not really "Rich is wrong and doesn't understand" in most cases. And I don't even know if Rich is the source of them, or the Clojure community in general. But there are things about Clojure that make me dislike it after using Common Lisp for a while.
One example is the standard libary functions are overly simple. find in Clojure vs find in Common Lisp. The Clojure API isn't wrong, it just doesn't cover as many bases as the CL version. find is just one of many here.
Another example is CLOS. Generic functions, CLOS, and MOP in CL are the coolest, possibly "best" OOP implentation I've used, and it's disappointing that Clojure punted on it and often suggest maps instead. It's not that it's wrong or doesn't work to use maps as objects, but there was a better way in the Lisp world, and they chose to avoid it based on some contrived edge cases (as mentioned in the linked page).
In this case, I'm actually not sure Rich understands CLOS well, because part of what makes it so cool is that it wasn't "built-in" or hard-wired into the language as he says, but implemented with macros on top of it and later standardized. People can (and sometimes do) make their own OOP systems independent of CLOS. I'm even pretty sure the broken CLOS example could be fixed by using MOP to implement a custom inheritance order. To be fair, though, the the full details of CLOS and MOP are pretty unique and also complicated, and out of the box his example does fail, as contrived as it is.
Ironically, though, I also see Rich's point on not including something like CLOS. In CL, it really was added as a library and then included in the standard. There were even competing OOP implementations, like Flavors and Common Loops. So if it can be a library, why bog down the standard with it? Later on, the fact that majore features like OOP could be added on with macros and clever programming has been a justification for not updating the CL standard with new stuff, and an argument could be made that CLOS shouldn't have been standardized. I'm genuinely curious if CLOS could be implemented as macros on top of Clojure like it is with CL.
I also have minor nitpicks like adding [ and ] to the syntax. I'm not necessarily a "s-expressions for everything" type of person, but it really simplifies the language to make it all lists.
Another nitpick is making everything immutable. CL is more practical, and sometimes mutable state is just faster or easier to reason about. It's also a little inconsistent that things like CLOS are left out because the developer should implement it themselves if they want it, while things like immutability are forced on everybody.
At the end of the day, maybe the biggest reason I don't like Clojure is that it feels a lot of the design decisions are down to "Because Rich thinks it should be this way," instead of any objective reason or consensus. The answer to the question "Why cannot "last" be fast on vector?" in the article kind of shows that, IMO.
So if [CLOS] can be a library, why bog down the standard with it?
What struck me about the design of CLOS when I read “the art of the metaobject protocol” was the way it supports dynamic reconfiguration without requiring a clever JIT to get good performance. There are clear points in the protocol where you can expect it to go off and generate and compile code, to support a new class or generic function or slot layout or whatever. (Contrast with Python that delays all the metaobject work so its hot paths are long house-of-horrors rides past a series of dynamic surprises.)
I expect that a good CLOS-on-Clojure (“Cloojure”?) would need to hook into Java’s invokedynamic machinery so that the metaobject machinery is legible to the JVM’s JIT. Dunno if that’s feasible for a library written in Clojure.
Not sure why he is comparing the best practice in Clojure against the worst practice in ML; let's compare with Scala to be fair:
case class Color(r: Double, g: Double, b: Double, a: Double). This also avoids the type ambiguity of Clojure's[r g b a], what arer,g,b,a? What can I do with them? The Scala case class tells you immediately.Yeah this is by far the worst part of the whole document. These arguments make no sense; none of the problems he's describing actually ever happen when pattern matching in a lisp.
This, and restoring the place of the arg list relative to the docstring as God intended are things Fennel did right.
No need to compare with Scala. SML handles this just fine:
Agreed, but Scala was already discussed earlier in the document, and it's the other big FP language on the JVM, so I wanted to be as apples-to-apples as possible.
Isn't the comment about how you use the value at a usage site rather than the declaration?
Like when pattern matching you can easily write
Color(a, r, g, b)instead ofColor(r, g, b, a)and end up with tears later on, right?That was true until recently but now you can name the fields in the pattern match: https://dotty.epfl.ch/docs/reference/other-new-features/named-tuples.html#pattern-matching-with-named-fields-in-general
Also OCaml has supported named fields in pattern matches for a long time.
Right, I think Rich's point is more like... people tend to not do named field matching when pattern matching in a lot of code, right? Like there's the ability to avoid the issue, but if people are still writing code in the lightweight non-named style, they're still running up against this I think
Like here's a random file in OCaml's debugger.
Looking at
report_error,you can seen a bunch of destructuring directly pulling out values. You can in particular see a lot of extracting oflenandpos.Now in the unvierse where these are very different types, unintentionally swapping them out leads to a mess. In a world where the types are the same (or, well, if you're using something like Clojure) you could just flub the positioning on a destructure of one of these elements right?
I am not really a Clojure expert, but I think Rich's argument here is that if you're instead writing something like
lenas an accessor function, then you can dolen(item)and be sure that you have extracted the right value.Now I do agree with a sentiment that in a more typed system you're less likely to flip stuff around. And perhaps you can set up your linter/autoformatter to automatically fill in the names to avoid the tedium and repitition of what you describe. I do understand Rich's point about ADT destructuring being kinda flimsy when used in a lightweight fashion!
There is definitely a "Rich's view on these languages are colored by him not touching them enough". An argument I'm amenable to at times. But his world is also a different world from other people's worlds, and his concerns are clearly different.
He also says he doesn't mind destructuring, and you can easily do:
In any case this sounds more like an argument against positional patterns than pattern matching in general, which also fits with his general recommendation of a la carte polymorphism with hmaps. Seems to me his main gripe is with the switch like nature of pattern match expressions, as he prefers dynamic dispatch. But IMO this is missing the point: sometimes you don't want things open to extension at any time. As a contrived example, if you had an
Optionalyou don't need that to be open to a 3rd or 4th state. And sometimes you want compilation to fail if a new variant is added without being explicitly handled everywhere, although for a dynamic language like Clojure that doesn't make as much sense.I get what you're saying, and do agree that I think his argument is against positional matching more than anything (and he wouldn't have much to stand on if we lived in a universe of "verbose" pattern matching on names only)
I do think his argument broadly is less about "we should have extensionality" and more "we shouldn't spend time on structural restrictions when we can design programs that don't require those restrictions". An argument for terseness (based on the sort of axiom of "less code -> less bugs", IMO, which ... I don't fully agree with but I get!)
To be clear, I spend much more of my time in the other camp, but every once in a while I'll write up some code in the "less structural restriction" space and it is nice sometimes. But hey, there's a reason we talk about Clojure being a write-only language
I have a better argument against pattern matching: it locks you into a particular representation of your values and makes you optimize for use-site ergonomics rather than performance, which is not always the right tradeoff.
For example, let's say I'm implementing a type for JSON values. If I want to use pattern matching I have to do something like:
Which makes my values have a certain memory layout which I can't change.
If I want to use some kind of allocator/pool for objects, or have a single large memory allocation for all variants (and reuse the payload as different values of different types based on the tag), I can't do this with this representation.
Instead of I keep
JsonValueabstract and have test and accessor functions, I have more flexibility.For this problem, in my language I'm planning to allow "overloading" the pattern matching syntax so that you can keep your representation abstract but still allow pattern matching. It requires value types/unboxed values so that generation of the pattern-matched values can be done without allocation, you wouldn't want
match foo: ...to allocate.Overloaded pattern matching sounds like F# banana clips or Haskell view patterns if you needed further inspiration. I agree with the approach - it feels less clunky than regular ol' ADTs, especially for things exactly like reading json, as you say.
You can change it. Just keep the representation internal and don't expose it to the outside world. You can use pattern matching internally in the module and expose combinators to users.
I've dealt with this when designing the Value type for my scheme implementation. Practically, you want the Value to be easy to access the various different types of, but you also want to keep it opaque so that you can optimize the representation.
My solution was to split out
ValueintoValuewhich is opaque and can only be converted to specific types via TryFrom, and theUnpackedValuetype which is an enum as you describe.Valuecan be converted to anUnpackedValueat a cost, but this is worth it if you're dealing with multiple different types. You don't pay anymore cost than if you'd match on the tag and convert them individually, but you get the nice ergonomics of pattern matchinng.lScala has
unapplywhich similarly allows for defining up a pattern match overload I believe.It's not really documented but I believe you can do some similar tricks with Python objects and
match. Given how controversialmatchwas I'm sure nobody wanted to bring up any potential dark magic you could do to pattern match your custom class on a listpreviously
This is a bad argument. If Rich wasn't so precious about Clojure, there would be many people willing to write and test an efficient polymorphic version of
last, as seen by the numerous ignored patches on forgotten tickets in Clojure's jira. This would benefit everyone who useslastfor ~no cost. Until then,lastwill continue to be another footgun, something that is "fine" until suddenly it's not and you're left wondering why the hell it doesn't work as expected. (Especially in light of his comments about not providing alookupfunction because of performance worries.)Clojure is great, truly my favorite language. But it could be so much better if the core team was willing to let go of the reins a little.
That's a bit odd. You can do
(cons x some-vector)and it'll be O(n), which is probably what you don't want. In fact, it's a common pitfall for beginners.Doesn't
consreturn aseq, which is O(1)?It does, but converting the vector to a seq is O(n)
This is inaccurate; creating a seq of a vector is basically free (on account of being lazy), but indexing that seq by offset is obviously O(n).
I stand corrected. Thanks!
That's interesting, because he would have to forbid nested loops to do that.
The full quote has the context:
And he's right, I have fixed the following N^2 bug MANY times in other people's Python code:
It's quadratic, but there's no double loop. On the other hand, this algorithm is linear:
I'd go as far as to say this is the most common Python performance bug I've encountered.
The key is the broken polymorphism -- you should not use the same syntax
infor an operation that is either O(1) or O(n).O(1) and O(n) operations should have a different spelling, to make it clear they have different performance characteristics
I might go even further and say that if you've used Python for say 10 years, and you haven't fixed this bug, there's a good chance you have it and don't realize it :-)
At least for web and data science stuff, where it tends to come up
Actually this is pretty much the same point I made the other day about SQL - some joins are inherently O(M x N), but they don't have a sufficiently different syntax than fast joins IMO
https://lobste.rs/s/t8fc8a/unexplanations_relational_algebra_is#c_gsdg75
It's also common in practice for SQL queries to "blow up" in running time. And then the authors of those queries have to spend a lot of time figuring out why, which is not obvious from looking at the code
If the syntax were more suggestive, that could help
And I should add that this same philosophy made its way into the YSH language design:
That is, we don't have "broken polymorphism" for
in. Instead, you useinfor the O(1) operation, andmylist.indexOf()for the O(n) operation. (the indexOf() name matches JavaScript)In context it's clear he's talking about defaults, in the sense of providing clojure.core functions that would naturally lead developers to poor algorithmic complexity categories. The sentence prior:
Cool, thanks! I just discovered that the GitHub app does not support gists, strange. I've played around with Clojure to do some Advent of Code problems, I found that getting a good 'feel' for what was performant code was actually quite difficult! I'm not sure what kind of things I should try out to really benefit from Clojure? Kinda hard to not be excited after listening to (or reading in this case) one of Rich's talks.
Definitely recommend joining the clojure slack (or zulip) if you're curious about the language/ecosystem- everyone's there more or less (once in a blue moon even Rich will show up, but some of the core team are also regulars).
Getting an integrated REPL setup is the path to the best experience with Clojure (and any other lisp), how you go about that is gonna be dependent on your editor of choice: CIDER for emacs, Conjure for neovim, Calva for vscode, (forget what the intellij one is, but there's something).
A combination of those two things will give you the opportunity to absorb some good overall idioms and 'theory' of the language :^)
Cursive for IntelliJ https://cursive-ide.com/
The argument seems to be reader macros don't compose... but doesn't Racket demonstrate they can? You can even mix #lang in the same file with an escape hatch IIRC
I've complained about it before, but I don't think Rich has a great understanding of what made the existing Lisps so great, and that answer seems to be another case of that.
At the risk of over-simplifying, a function (defun) is a function that runs at runtime, a macro (defmacro) is a function that runs at compile time, and a reader macro is a function that runs at read time. It's nothing to do with creating multiple incompatible readers.
For example,
#(1 2 3)is a standard (meaning it's in the CL standard) reader macro that transforms into something like,(make-array 3 :initial-contents '(1 2 3))There are JSON syntax reader macros, so things like(defparameter *foo* #J({"foo": 42, "bar": 42}))will expand into(defparameter *foo* (make-json :entries '(("foo" 42) ("bar" 42)))Maybe somebody else chooses #J() to mean something else, but that's a risk with normal functions, too, and there are solutions that work around it.
I would be interested to hear more things he's wrong about, as somebody considering making a lisp :)
That explanation was very helpful thanks.
The way I phrased that makes it sound like an ad-hominem, and it really shouldn't have been. It's not really "Rich is wrong and doesn't understand" in most cases. And I don't even know if Rich is the source of them, or the Clojure community in general. But there are things about Clojure that make me dislike it after using Common Lisp for a while.
One example is the standard libary functions are overly simple. find in Clojure vs find in Common Lisp. The Clojure API isn't wrong, it just doesn't cover as many bases as the CL version.
findis just one of many here.Another example is CLOS. Generic functions, CLOS, and MOP in CL are the coolest, possibly "best" OOP implentation I've used, and it's disappointing that Clojure punted on it and often suggest maps instead. It's not that it's wrong or doesn't work to use maps as objects, but there was a better way in the Lisp world, and they chose to avoid it based on some contrived edge cases (as mentioned in the linked page).
In this case, I'm actually not sure Rich understands CLOS well, because part of what makes it so cool is that it wasn't "built-in" or hard-wired into the language as he says, but implemented with macros on top of it and later standardized. People can (and sometimes do) make their own OOP systems independent of CLOS. I'm even pretty sure the broken CLOS example could be fixed by using MOP to implement a custom inheritance order. To be fair, though, the the full details of CLOS and MOP are pretty unique and also complicated, and out of the box his example does fail, as contrived as it is.
Ironically, though, I also see Rich's point on not including something like CLOS. In CL, it really was added as a library and then included in the standard. There were even competing OOP implementations, like Flavors and Common Loops. So if it can be a library, why bog down the standard with it? Later on, the fact that majore features like OOP could be added on with macros and clever programming has been a justification for not updating the CL standard with new stuff, and an argument could be made that CLOS shouldn't have been standardized. I'm genuinely curious if CLOS could be implemented as macros on top of Clojure like it is with CL.
I also have minor nitpicks like adding [ and ] to the syntax. I'm not necessarily a "s-expressions for everything" type of person, but it really simplifies the language to make it all lists.
Another nitpick is making everything immutable. CL is more practical, and sometimes mutable state is just faster or easier to reason about. It's also a little inconsistent that things like CLOS are left out because the developer should implement it themselves if they want it, while things like immutability are forced on everybody.
At the end of the day, maybe the biggest reason I don't like Clojure is that it feels a lot of the design decisions are down to "Because Rich thinks it should be this way," instead of any objective reason or consensus. The answer to the question "Why cannot "last" be fast on vector?" in the article kind of shows that, IMO.
What struck me about the design of CLOS when I read “the art of the metaobject protocol” was the way it supports dynamic reconfiguration without requiring a clever JIT to get good performance. There are clear points in the protocol where you can expect it to go off and generate and compile code, to support a new class or generic function or slot layout or whatever. (Contrast with Python that delays all the metaobject work so its hot paths are long house-of-horrors rides past a series of dynamic surprises.)
I expect that a good CLOS-on-Clojure (“Cloojure”?) would need to hook into Java’s invokedynamic machinery so that the metaobject machinery is legible to the JVM’s JIT. Dunno if that’s feasible for a library written in Clojure.
[]and{}along with map accessing like{my-map :cat}are the best lisp innovation since I was born. And my definition of lisp is using s-expr!