No, of course I wouldn't have reached for CL first and it's disappointing to read the top-comment is this.
Ecosystem size is really the only thing I can think of.
I can use Discord (despite my distaste for it as a platform it has a huge Django and Python community), Telegram, IRC, and Matrix to ask for help and get metric tonnes of friendly advice, suggestions and criticisms from Pythonistas.
And I can take all of this at my own pace, writing only bits in Hy and branching as I go. Every step is as gradual as I want it to be.
Common Lisp is not the answer to my needs. Presenting it as so is tone deaf at the least.
If you're happy with your mix of Python and Hy, well, great. But you know what, you can do what you described with CL too! Need to do some webscraping? Beautiful Soup -> lquery. Django -> well… no CL framework gives you all these batteries included, but what if you want to extract data from a Postgres DB and display it in a web app? that's very easy [1] in CL too. Django ORM -> postmodern or Mito (or more). IDK about Nix but several people are working with CL and Guix.
https://github.com/CodyReichert/awesome-cl
What worked out for me was to write companion services in CL, instead of extending my big Django app. This new service that takes data from a FTP server, transforms it, stores it, displays it in a web app? CL. That web app that serves another kind of customers than the Django one? A CL app. It isn't for everybody and sometimes you wish for a bigger ecosystem, but anyways, many things can be done in CL.
[1]: ok honestly the learning process was not that easy. It's a bit easier with my contributions now! (Cookbook etc)
To me, the way to think about "introducing" someone to Lisp and helping them understand the choices the language makes - you really want to focus on the things that make Lisp stand out, and one way of doing that is re-using a lot of the functionality of a language that outsiders may be familiar with.
I generally agree that if you want to build something substantial you probably would choose CL over Hy, but if you want to "try a lisp" while minimizing startup costs and you have a Python background I think Hy makes way more sense.
If you choose language X, I advise learning it alongside people who know and use X. It seems like a no-brainer to me, and not all that expensive.
Basically, that "if you choose..." advice is for the wrong context, specifically because of the "if you choose" qualifier. I'm talking about what happens during the pre-choice time period.
While I won't claim nobody has been "psyched out" by a community of people for their language-learning choices, I just don't think that this is something that happens with Lisp particularly often. The subreddit r/Lisp is predominantly Common Lisp, but more frequently than not accepts viewpoints from Clojure, Scheme, Racket, and even NewLisp programmers all in the same comment section, without bickering.
As I said, if anybody wants to learn a new technology, and that somebody is a social person, finding a supportive community is important. If somebody going to be a novice at a particular language, and broadcast opinions about that language on disparate forums—some opinions which may come across as uninformed—sure, people will likely come out of the woodwork to correct them, suggest allegedly superior technology, etc.
I will give you this, though, you can have a taste macros with Hy, which of course are one of the cornerstones of different Lisps.
With that said, I do not feel like one would accurately develop a feel for what Lisp is like (be it Common Lisp, Scheme, Racket, or Clojure) by using Hy.
Very much agree! I shouldn't have framed it that way.
I should have said: each time you make a hybrid language between approaches, there are new people who will try it out and may discover a new and exciting way to program. Some people, in hindsight, will certainly wish they had jumped further - but they can always do that in the future and some will only make the bigger jump after making a smaller one.
As such, for Hy’s target audience, I think it’s not as much “ditch a new language” as it is “I have to use Python for $reason, but I want something more like CL”. A good example of this would be any machine learning related work.
Last but not least, I would expect that on HN we focus a bit more on “what could make Hy even better” rather than “what other languages are better than Hy”.
A quick search brought up this:
https://lisp-journey.gitlab.io/blog/pro-mailing-list-on-comm...
> This was primarily for the lack of good parallel, concurrent garbage collectors in Common Lisp implementations. The CL version of elPrep was actually still a tad faster than any of the C++, Go, or Java versions, but we had to work hard to avoid long GC pauses. elPrep allocates a lot of memory, and the pause time hurts a lot. We solved this by, basically, disabling the garbage collector, and reusing memory manually as much as possible, which turned the program into almost a manually memory-managed affair.
This doesn't happen in Go or (presumably) languages that run on Erlang's BEAM VM because there is no sync I/O (from the event loop's perspective, anyway).
...in which case you'll definitely not want to use Python either, and so are rather far out of place in this thread.
For a type-racket equivalent, we now have Coalton, it's like a Haskell on top of CL: https://github.com/coalton-lang/coalton/ Its author says:
--
Take this with a grain of salt, because I’m neither a user nor expert of Typed Racket, but:
Typed Racket focuses more on the gradual typing of a given program. It has lots of features to make that easier, such as occurrence typing. Coalton is a separate, embedded language.
Typed Racket achieves polymorphism through subtyping and and first-order type variables. Coalton achieves polymorphism through type variables, higher-kinded types, and type classes.
Coalton, like ML and Haskell, focuses on defining objects by their properties and supported functions. This is a proposed way of having modular, reusable code. Typed Racket, as far as I can tell, has no such features.
Coalton code can be fully inferred, so type annotations are not necessary. Typed Racket cannot.
All in all, I think the biggest and most important take-away is that Typed Racket goes through great effort to seamlessly blend with ordinary Racket. But that means Typed Racket has to compromise on type system features that can only be supported if you’re willing to change the language itself.Coalton puts the type system first, opting for something close to Haskell, at the expense of not being a system for gradually typing Common Lisp, and instead being a separate language altogether.
---
They use it for their open-source quantum compiler and other things.
> no closures in Hy last time I checked
I don't know when the last time you checked was, but as far as I can tell hy definitely has closures. It largely follows Python semantics, after all.
MGL's auhor [won](https://github.com/melisgl/higgsml) the Higgs Boson ML Challenge.
(Common Lisp is super cool, though. Perhaps one day Hy will be as cool.)
And as someone who programs in Clojure, I feel the interactivity is the same. Maybe I'm missing a detail of what he's doing though?
The part you don't have in Clojure (without using a library), is the restart options, but he didn't seem to ever use them, seems he just looked at the error message, and went back to fix the code and re-evaluate his test again, and that would be exactly the interactive flow I would follow in Clojure.
Am I missing something?
In Clojure, you can debug at the REPL, but it doesn't drop you in the debugger by default. You'd need to eval the function as debug first, then it will drop you into the debugger next time the function is called.
And there's no restart system by default, so for example you probably wouldn't retry by choosing restart test from the exception debugger, you'd just re-evaluate the test yourself to have it rerun.
There's a condition restart library, and I played a bit with it, but not enough, I was confused about how to go about choosing options to handle the condition or how to define them.
So the typical flow in Clojure would be:
1. Error thrown
2. Go to line of code that threw error
3. Fix code
4. Eval fixed code
5. Eval the thing that threw the error again (seeing everything pass)
And if you want to observe the state at the place of the error thrown you'd add between 2 and 3: 2.1 Eval function as debug
2.2 Re-eval thing that threw error
2.3 When in debugger inspect all locals and state, find the problem
And then go to 3.I never really looked into if there was a way to make eval as debug default with break on error, then I guess you'd get a very similar flow, except for missing the restart options.
> There's also the typical class change: in CL you can alter a class definition and the existing objects are (lazily) updated
Since Clojure isn't OO, this isn't a 1:1 comparison. But the closest thing to OO is polymorphic facilities, except they don't have state. Anyways, you can update the implementation of the polymorphic methods and it gets picked up for existing types or data that were polymorphic over them.
You can also add more polymorphic methods and those get picked up automatically.
There is a construct that has fields on it, but it's almost never used, so I actually don't remember if it would let you update fields on existing instances, I believe not. In CL can you add/remove fields on the class and the existing instances get updated? What happens to new fields how are they initialized in that case?
> you can't install a Clojure library from the Clojure REPL, right?
Well, you can, but let's say that it's something that easily breaks, so let's call it a "beta" feature. Some people use it effectively and don't seem to have issues, but others seem to always have it not work as expected, so a lot of people restart the REPL most likely when they want to add a lib.
Also, it's not a standard feature of Clojure, instead it's a feature of your build tool, either Leiningen or Tools.deps, and both have a different way to do it. Though tools.deps is the default dependency manager that comes with Clojure now, and its add-lib feature should one day get out of beta and become permanent, probably once they iron out the quirks.
https://practical.li/clojure-staging/alternative-tools/cloju...
yes, and you can even specialize built-in methods so that the update does exactly what you want O_o See update-instance-for-redefined-class. It takes as arguments the added slots, the discarded ones, it's supposed to do the right thing but we can customize it.
You wouldn't get that in Clojure for defining types, since they'd be Java types and then they only get what features the Java runtime allows on types and it doesn't have that one.
But in practice, since Clojure is functional, it's kind of a non-issue.
I should really try CL to be sure, but it seems you'd get 95% or more of the same interactivity in Clojure.
What I can tell you is I find Clojure superior to Emacs LISP in interactivity (which I've used both extensively). Emacs can technically redefine more things inside-out, like it can do the full runtime rebuild from the example link, but because it isn't functional and immutable first, managing the state as you redefine things tends to be more complex, and I find that makes it less interactive, since I need to do a lot of state setup and teardown to get things back to what I want to re-evaluate.
I love writing CL but there’s a reason it’s not enjoying wide usage nowadays, and I definitely wouldn’t go around recommending it to unsuspecting people without heavy caveats.
I don't think you are aware how much the Python workflows of AI / ML / datasci people resemble you "common lisp ideal" ...but with a different language and better libraries :)
And if Julia succeeded, it will also bring decent performance to such workflows.
Sure, the way backend devs or devops people use Python is very static and different, but any experienced ML practitioners are at home with "interactive development".
Evaluate arbitrary expressions mid-stack trace.
Run into a bug, get caught in an error, re-define the offensive function interactively, and re-run the computation before that point, all without stopping and restarting.
(Example real-world use-case: You re-define one of your REST API endpoints without having to restart your server or interrupt the request-flow, be it in test or—if you so desire—in prod.)
Change the class of an object at run-time.
Re-compile and re-load individual functions from your editor with your production, git-committed code. This one is huge. You use interactive and incremental development to, well, develop! Not side-by-side with your code, but in your code as well, at a fine-enough grained level.
Common Lisp was designed to be both interactive and incremental. It's not a bolt-on feature.
Jupyter notebooks and auto-reloading have a very different feel, and (dis)incentivize different qualities and processes of development.
Unfortunately I've seen it done... and God, the results are horrible, in Python. "Wait did calling this method just change the class of the object the method was on??!!!! Fuuuu..."
But simple stuff like incrementally building classes method by method in eg. jupyter notebooks can be done and it's even quite clean, it's just that nobody is teaching people the techniques for doing it... (And the jupyter autoreload magic doesn't seem to like it so you'd have to "do it by hand")
And sure, since Python got scoping and concurrency quite wrong, if you go full-interactive on things you'll shoot yourself in the foot faaast!
for the second point: https://mikelevins.github.io/posts/2020-12-18-repl-driven/
There might be more CL libraries than you think: https://github.com/CodyReichert/awesome-cl but I'm not that a zealot to say you can ignore Python's ecosystem, especially in certain areas…
...practically speaking you're kind of right, you (almost) can't.
In theory one can use `sys.excepthook` to start the python interactive debugger on error and/or %debug jupyter magic, but the "change it, recompile it, come back to the debugger" is not really possible unless you the yuuuuucky thing of changing it from the interactive debugger itself (think change def in code, paster changed def in interactive debugger).
As a python expert you can do it. Even modify classes at runtime etc. Tried at various times doing the various things mentioned above. But tbh you learn to rarely do it because you easily get in a twilight zone hell where it's impossible to reproduce anything, no idea what state of code is "active" and what your functions and variables actually are :)
...but basically what Python lacks to enable proper implementations of such feature is decent concurrency - while a thread is paused at the breakloop, the interpreter should allow another to eg. execute code that updates functions and classes definitions - all Jupyter's magic can't do this bc concurrency in the Python interpreter is not doable :)
...in theory the Julia jupyter kernel for example could actually implement all the features mentioned by people describing CL loops - just that someone would need to put the tons of work in, and unfortunately Julia folks seem focused on the automagic-reactive approach to recreating excell-but-as-a-notebook (I don't like this magic-reactive approach tbh... try doing that and have a random variable change "accidentally" triggering recomputing something with a 10gb dataframe you had in var or the retraining of a big deep learning model).
EDIT: I was made aware of the guidelines to not comment on voting, I apologize.
From the HN guidelines at https://news.ycombinator.com/newsguidelines.html
Jupyter notebook: you can't run it as normal code - that is, you can't import your notebook from another file. You also can't evaluate arbitrary regions of the notebook (it's cells, the whole thing, or nothing), or nested sub-expressions. You can't evaluate code in parallel - if you have a cell calling a function foo() in a loop, and you try to redefine foo() in another cell - impossible in Juypter, trivial in CL.
iPython + autoreload: you also can't redefine a foo() in a file when it's being called in a loop, docs state "reloads modules automatically before entering the execution of code typed at the IPython prompt". You don't get any sort of interactive debug-and-fix-up when you encounter an error. And so on.
It is probably still a good deal more limited to what can be done with CL, there are no miracles here, just saying the gap may not be that big.