This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
I love chatgpt to help come up with better names for products I will eventually procrastinate from building. Just came up with "pocket pain cave" for the simple app that will hoover up all the folks who don't care enough about the social/virtual world aspect of zwift to cough up for their massive price increase and just want to use it to do their structured workouts.
I don't know what most of those words mean but you might owe Frank Herbert royalties for pocket pain cave 😿
What do we want?! > Static structural types in Clojure! When we do we want it!? > java.lang.ClassCastException: class clojure.lang.Symbol cannot be cast to class clojure.lang.Keyword (clojure.lang.Symbol and clojure.lang.Keyword are in unnamed module of loader 'app')
Do you get that error in logs post development? Because I normally get it at the REPL as I code the function/macro. Which is pretty instant in terms of how quickly I learn about the mistake from writing the code to the error. Would having it show as a squiggly line speed things up that much more ?
> Clojure: present in the back half of our top 20 from the years 2014 to 2017, Clojure has now slipped down to 27 – which admittedly is outperforming fellow one time high fliers Visual Basic at 30 and CoffeeScript at 31. A Lisp dialect that was one of the Groovy/JRuby/etc JVM-based Java alternatives, Clojure has steadily lost ground in these rankings, as have most of its counterparts with the exception of Scala. It’s fair to wonder how much the rapid growth of Kotlin has impacted these, because in addition to competing with other languages, the JVM-based cohort is competing for space on top of that specific platform.
> Do you get that error in logs post development? Because I normally get it at the REPL as I code the function/macro. Which is pretty instant in terms of how quickly I learn about the mistake from writing the code to the error. Would having it show as a squiggly line speed things up that much more ? Try for yourself:
clojure -Sdeps '{:deps {:org.clojure/core.async {:mvn/version "1.5.648"}}}'
Now imagine you're a new user and how you'd feel as your first time running clojure! Yay fun timesHum, ok, but this doesn't even have anything to do with static types. That's a command line, it wouldn't typ check that, you'd still get a runtime type error.
I would like a really good and easy to use static typing solution for Clojure, not sure this is a good example of why though. The issue here is Clojure's bad error messages.
This is a runtime error. Static typing would force the implementer of this CLI tool and tools.deps to consider the types of the data up front and deal with improper types properly - and likely make them think about reporting inappropriate errors on incorrect types. If they didn't consider all the types, this program wouldn't compile. Bad error messages and type errors only being caught at runtime are related. The broader point is that there is a macro trend in programming languages (the broad adoption of static types - see the above talk) that clojure is on the wrong side of. It is clear after many years of desire for a typing story from (some large minority, perhaps majority of) the https://download.clojure.org/stateofclojure/2023/Data_Q7_230630.pdf and no response from the maintainers of the language that it isn't going to happen. It's sad to see the 2014-2017 era be clojure's peak, but that's what looks like is happening.
I mean, not everybody wants them. But anyone is free to create a TypeScript-like for Clojure
I don't think you're correct about that example though. There's no improper type here. The code would compile even with static type checking. This is something I see often, it's like people who assume types exist at the boundary. A rest API returns untyped JSON, a CLI takes a string as input. In both cases you need to work out the types at runtime.
The other issue as well is that, A typed variant of Clojure would no longer be Clojure, but it would start to resemble any other language. Look at all static language, they're all the same. You have classes, or some similar ADT construct. You have methods defined over them. There's a way to define subtyping, if not actual subtypes, it's something similar, union types, structural types, etc. You stop having functions that return different type of things, or can take very varied types of input. So instead you have more rigid functions. Etc. It makes them all approach Java. Typescript is a great example, it's basically taking JavaScript and making it a class-based Java-like.
For example, there are functions on Clojure that people don't know how to type them. You'd need some very powerful mechanism, and then nobody knows how you'd implement a type checker for that. So you'd need to get rid of those, and make clojure.core full of functions that are easy to type check instead. The whole feel of Clojure might change.
I'm not saying all that to say it shouldn't have static typing. More-so, I want to bring a realistic take. A statically typed Clojure is non-trivial. It would have trade-off needed to be made. Elixir is researching some experimental stuff, and it be great of things come out of that which can be brought back. TypedClojure also does some experimental stuff, and honestly it's getting better and better. But ya, it's not just a matter of wanting to do it, but how do you even do it?
Also, you don't get this in your IDE? I know you were calling the CLI, but if you actually do that in a deps.edn file, clj-kondo does show a static error under the keyword.
I think that a gradual typing system in clojure that could play nice with the rest of it would probably be universally welcomed. I feel like spec an malli fill that role in some capacities. I'm guessing that racket has far more direct to the point error messages, but then you don't get the jvm or javascript ecosystem. tradeoffs within tradeoffs.
Emphasis on that could play nice with the rest of it I don't know any static typed language written over a dynamic one that "plays nice" with the rest. Typescript is a great example - I don't feel it "plays nice" with the rest of JS world, because it kinda enforces a way to make code that is usually not that common in the JS world (class-based code, with some interfaces and implementations over interfaces, injecting dependencies with some annotations - never saw this kind of code in JS)
Ya, to my knowledge, all static type systems force you into defining the fields and methods of your "entities". What is called an abstract data type. The existence of abstract data types makes a language feel very OO-like. You define the field of structs, and their type, and you create instances of that struct. Then you specify that a variable is meant to hold an instance of that particular struct, and a function is meant to take as input an instance of a particular struct, or return one, etc. And that also forces you to have some kind of subtyping. So that functions that work on subset can also work on superset. Which creates a form of inheritance. And so on.
The only one I know of that doesn't force you into this is MyPy for Python. But I've never used it, only read about it. But it can actually track key existence on maps automatically, in most cases, without forcing explicit creation of an abstract data type alias for the map.
I think something like that might be what would work with Clojure. For example:
(defn inspect-user
[user]
[user]
(println (:email user)))
(defn login
[...]
[...]
{:user username, :session session})
(inspect-user (login ...))
This would error in MyPy as a compile error. Because it never saw anything add the key :email to the map being passed to inspect-user.
But if you did:
(defn inspect-user
[user]
[user]
(when-let [email (:email user)]
(println email)))
(println email)))
It would no longer error.
So it infers, in most cases, what keys exist on a map, and accessing a key that it doesn't think exists would type error at compile time, unless you first checked if it did exist.
You're free to assoc more keys, it won't complain.A bitter truth about note-taking apps and "systems:" > The ecosystem of note taking software and influencers that has emerged since 2020 offer a great example of what I am talking about. Writing is a task which forces one to confront ambiguity, uncertainty, and one’s own limits directly, so it’s not surprising that a coterie of avoidance entrepreneurs sprung up around it. https://artificialbureaucracy.substack.com/p/avoidance-machines
A key selling point of REPL-driven development is its fast feedback (or reduced friction to tie it back to the article). In contrast to the note-taking apps, I'd say it's a clarity machine (instead of an avoidance machine). Often, there's fairly little ceremony and one's left sitting with the domain problem at hand. I've found it quickly lays bare where my mental model of the problem at hand needs work. Which is nice. 🙂
unfortunately I have used “REPL prototyping” as its own kind of avoidant labor instead of thinking about the problem I’m trying to solve. I doubt any tool of sufficiently general purpose is immune to it.
The discussion of expressiveness in "Why Lisp is Diverse" (part 5 of https://redirect.cs.umbc.edu/courses/331/resources/papers/Evolution-of-Lisp.pdf) helped me see Rich's answer to https://gist.github.com/reborg/dc8b0c96c397a56668905e2767fd697f#why-cannot-last-be-fast-on-vector as a contribution to an old, ongoing dialog in the lisp world.
Maybe I'm missing the point, but this makes little sense to me. Isn't the whole value of having an expressive language that I can write clear and expressive code as close as possible to my problem domain, and then leave it up to the compiler, core libraries, implementation, runtime, whatever to make it efficient?
"it's slow because it's expressive" feels like a non sequitur
And keeping something slow in cases where it could be fast just because it's slow in the worst case feels like unnecessary handcuffs. Why bother having non-list data structures at all if you're not going to leverage the difference in performance characteristics?
if APIs guarantee correctness and performance is correctness, then it makes sense to have different APIs for different time complexities
I can certainly understand that from the perspective of performing worse, but if you say that last
will find the last item in at most O(n) time, what makes it bad to polymorphically permit O(1)? It's not advertised, and it doesn't break the contract by screwing your application performance unexpectedly
> it doesn't break the contract by screwing your application performance unexpectedly True in a strict sense, but in practice though, https://www.hyrumslaw.com comes to mind.
IIRC, the alts!
op in core.async
intentionally randomizes which is picked when multiple of the argument-ops are ready (unless caller explicitly requests for priority).
APIs communicate intent: if performance matters, use the API that denotes that you want performance,`peek` (which comes with the necessary caveat that it will have different semantics based on the data structure). "another category of problem was not being able to see what calls mattered for performance, when lists would still be ok etc". I think it's fine to expose those details and force the user to be aware of the data structure their code is working on, however polymorphic.
Another way to think about that is last
isn't intended to work on vectors at all, it works on seq
s which are linked lists by nature
Another such example: Tail-calls to the same function consume stack-frames despite Clojure being able to detect and eliminate them. Instead of doing TCO on a best-effort basis, Clojure provides recur
which guarantees it (compilation-error if it can't).
FWIW I don't think Jason isn't against the advice to programmers of the form "use peek
if you care about it being constant-time", but rather is wondering why Clojure doesn't do better than promised when it can (`last` on vectors being one example).
Why is different polymorphic complexity not okay but different polymorphic behavior / order are okay?
(conj '(1 2 3) 4)) ;; (4 1 2 3) O(1)
(conj [1 2 3 4]) ;; [1 2 3 4] O(log32N)
Right. I guess my question (argument?) is that having a performance ceiling rather than a performance floor seems unnecessarily restrictive. You can certainly promise last
won't ever be worse than linear, but how does it do harm being better in certain circumstances? Hyrum's law wouldn't really apply in such a case because it's not any sort of unintended thing that people are now relying on: I've promised you linear at worst, and that's what you're getting.
Consider sorting algorithms: they have best case, worst case, and average case performance characteristics. It would be quite strange if a sorting algorithm just decided, you know what? Let's always just do the worst job, regardless of the input for predictability.
That last
is a sequence operation is an interesting point: sequences are traversed in order, and so that's inherently linear. But that made me think of other functions, like count
. That seems like it should be a sequence thing too, but it treats it's argument polymorphically. What's the litmus test for deciding to fix the performance versus allow implementations to do better than a particular ceiling (or, in the case of count
, making no performance promise at all)?
count
is interesting because there's also a bounded-count
for more guarantees on time complexity. comes down to pragmatism I guess
I doubt there's some grand intention behind all this, but continuing to play devil's advocate, count
being o(1) is consistent with the seq abstraction because lists in clojure are counted
> Why is different polymorphic complexity not okay but different polymorphic behavior / order are okay?
Still not convinced a linear traversal guarantee (the worst you can do, really) is helpful, but I understand the design thinking. conj
is probably a clearer example of this: if you fix the behaviour as an append, you get wildly different performance between lists and vectors. Performance matters to Rich, so he chose to fix the performance and define the contract in terms of that: efficient add to a given collection.
conj
feels more sensible to me than last
because you're getting an O(1) (or thereabouts) promise, but I suppose the design thinking is consistent, even if I find it bizarre.
While it's true that, if I wanted a more efficient version of last
(or butlast
) I could use peek
(or pop
), this feels a little weird because it carries stack semantics with it. But I guess that's the point? If you want to be efficiently getting the last item added to a collection, you probably want to be using a stack. Hmmm
Something that just occurred to me: if you did pick a stack because you need efficient access to the last item, you have no deterministic way of traversing that structure; you're at the mercy of whether the stack is backed by a list or a vector. That feels like a bit of a foot gun
@U04RG9F8UJZ I don't think I see what you mean by "pick a stack" in terms of this alleged footgun.
I mean that if you recognize that it's important to have O(1) performance for finding that last item, then you would peek
what you've just conj
ed, right? So far, so good. But if you want to iterate that structure, list vs vector makes a huge difference in the result: it's not a transparent choice because there's no "stack" abstraction that preserves iteration order of stack elements regardless of the underlying choice of structure.
This is pronounced by the very easy to make mistake of turning a vector into a list because you happened to run a seq operation on it and forgot to pour the results back into a vector
I don't think clojure.core's polymorphism is meant to encourage working on something blindly, without knowing if it's a seq or a vector. (I'm reminded of https://www.reddit.com/r/Clojure/comments/4ve288/conj_i_just_dont_get_it_can_someone_help_me/d5xq2k4/ from Alex.)
I think this brings us back to some of Clojure's formative design decisions, demonstrated by conj
. CL had subtly different APIs for operating on different structures. Part of Clojure was solving that pain point. I think (citation needed) some other lisps solved it with less polymorphism and thus more functions with longer names.
From OP in the linked thread: > I think that's the takeaway here -- abstractions are for providing utility to multiple types, not for type agnostic code. > That's an interesting shift from what I'm used to from OOP