Why Hy?
docs.hylang.org
docs.hylang.org
So if you design a language and want to use Lisp-like syntax, I recommend to also check out io before steaming ahead.
Though I have to admit I can not imagine how a person can be so mentally inflexible as to be not able to tolerate Lisp-style syntax. I get that Common Lisp might look a bit old-school at first glance but something like Clojure is basically designed to look aesthetically pleasing.
In the case of most Lisps, code is written using the syntax of list and symbol literals. (There are other literals too, of course.)
(I (usually) like apples)
is a nested list literal with four symbols. You can think of symbols as "interned strings"; they're written without quotes for convenience. It's not unlike ["I", ["usually"], "like", "apples"]
in Python, except I had to use actual strings instead of symbols because Python doesn't ship with a standard symbol type.This is also a nested literal list:
(loop for i below n do (print i))
but this happens to be a list literal that can be "run" (or in Lisp parlance, evaluated) as code.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.
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.
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.
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 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)
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".
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).
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!
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.
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
For my part, I had similar "this doesn't feel quite like lisp" moments with both Hy and Clojure. And I ultimately realized that it's the mere presence of the base platform that gets me. With Racket, I'm used to feeling like I'm sealed off in my own lispy bubble. With Hy and Clojure, there are always bits of Python or Java (read: Not Lisp) hiding in the background. Many differences from other well-known lisps that I know are attributable to that base platform.
And that's a catch-22 situation. Objectively, they might be (and, IMO, are) good design decisions. But they're also covered in non-lisp cooties. I don't know why I should expect otherwise. Brains are weird.
I think in isolation it would be a fairly bad choice, but in practice you get a drop-in replacement for lua that fixes every problem with lua.
I wouldn't want any other lisp to work like it does but damn I consider that thing my savior at this point. I've come to really hate lua and fennel bails me out every time.
if 2 < 3:
s = "Math works"
else:
s = "Math doesn't work"
print(s)https://github.com/natrys/ghdl
I definitely had fun writing that in Emacs/hy-mode. And indeed having access to Python ecosystem is neat. However, if I may:
- I didn't get to use any intellisense, which was quite painful. Is there any Language Server for Hy now?
- I think Hy tends to break with every Python minor release, which is a bit annoying. Stable is still broken on 3.10, and alpha is a big change.
- This one might be a matter of subjectivity but I felt that Hy is trying to be more "pythonic" and less "lispy", and I am not sure what to feel about that. For example, familiar things like `&kwargs` or `&optional` seems to have got replaced with something less familiar (particularly, change to `#**` for keyword arguments spurred this though).
Its read language (Lissp) doesn't have an Emacs mode (yet), but its syntax is so close to traditional Lisps that lisp-mode pretty much works. No completions either. There is Lily https://github.com/mjobuda/damascus-tools/tree/main/lily, which proved Jedi works on Lissp, but it's never quite worked right.
Hissp leans more Lispy than Pythonic compared to Hy, but still prioritizes interop, so the object model is still Python's.
2. That's right. Hy depends much more on finicky details of Python than most Python packages do, particularly Python's AST, so each 3.x release of Python usually requires some changes to Hy to support it. Each Hy release supports (at least) all versions of Python 3.x that have been fully released and are not yet at end of life.
3. More precisely, we're trying to take more of our conventions from Python rather than Clojure. See https://news.ycombinator.com/item?id=31250992
I think metaprogramming is a bit of an antipattern in most use cases(The more power the programmer has, the less the compiler/linter can follow it, just because... there's more possibilities).
But for the stuff it that needs it, it seems to be unbeatable, and this seems to have type annotations, which fixes one of traditional lisp's ickiest things.
Plus, having access to Python's libraries should make it possible to really evaluate what LISP can do by as a language, rather than having the choice mostly made by ecosystem size for a lot of projects.
I prefer a much more predefined structure that fits in better with autocomplete, and focuses more on reuse than new ideas, but I could see some pretty amazing stuff being done in this, and it being a benefit to the Python community as well.
You don't really have any help from the system, it's all on you to ve sure you've tested every code path, and it doesn't produce any formal report like a linter's "No warnings" message.
Nor can other devs repeatably do the same tests and get a list of warnings to work on, you can't automatically test the whole existing codebase when new static checking comes by, etc.
It's super useful for experimenting when you aren't sure of something, and it's great to have a debug console in the debugger, but I've never been that much of a fan of interactive programming in most cases.
Plus, the same annotations used for static analysis also drive modern intellisense type tools, so even the most quick casual project can still have some pretty great documentation with almost no time cost.
Ctrl+F and try skipping to ";;;; The Basic Macros" section on that quick start page. Is that about the right level?
For many of the "popular" languages, a simple cheat sheet showing the equivalent control structures in a common language might be enough for a typical programmer to get a handle on it quickly. But Lisp is not an Algol-family language. It's different enough that such a document would fail to convey what makes it interesting. There's an inferential gap that must be bridged first, and it's difficult to do that quickly; a programmer mistakenly [expecting a short inferential distance][1] may not have the patience to "get it".
[1]: https://www.lesswrong.com/posts/HLqWn5LASfhhArZ7w/expecting-...
1) Simple, one way to do everything. for loops, code formatting, and project setup.
2) Good concurrency (go routines)
3) No exceptions, readable code and error handling.
This article is titled "Why Hy." It should sell me on the language with a philosophy supported by examples, not instruct me on how to write a floating point number (unless the language has a interesting take on writing floating point numbers!)
On the other side I am quite torn about the decision. I quite liked the early releases of Hy where I could write a code that was much more familiar for me as a Clojure programmer. Still need to give a try to the new releases though.
In truth, I've never understood the fuss about `let`. It's taken a huge amount of Hy development effort, but it adds very little. Python's native scoping works well in the large majority of cases, and in tricky cases, I'd rather use `nonlocal` or `global` explicitly so I'm sure what's happening at the level of Python semantics. I suspect that people who heard that previous versions of Hy were missing `let` assumed that some other feature (such as nested lexical scopes) was also missing that has actually always been there.
My use case is Jupyter notebooks. With normal Python scoping you quickly accumulate a lot of global variables and you might accidentally use the wrong one. With let-bindings you have variables that clean up after themselves.
(let [a 1]
(defn foo []
(setv a 2))
(foo)
(print a))
you probably `2` to be printed, but the seeming equivalent Python code a = 1
def foo():
a = 2
foo()
print(a)
prints `1`. Python interprets the assignment as declaring a new local variable. Frankly, Python got scoping wrong (in its quest to eliminate declarations), but here we are. So `let` needs to work around this somehow. The current implementation implicitly adds `global a` or `nonlocal a` as appropriate. (Actually, the name `a` is also mangled so that any references to `a` outside the `let` refer to a different variable.)But maybe it is because I am familiar with Clojure (where `setv` or equivalents doesn't exist) and not other Lisps.
This would be the equivalent Clojure code anyway:
user=> (let [a 1] (defn foo [] (def a 2)) (foo) (println a))
1
nil
So yeah, makes sense for me. But can understand why some other people would expect the other way though. * (let ((a 1))
(defun foo ()
(setq a 2))
(foo)
(print a))
2
2 a = 1
def foo():
x = a
foo()
so if this 'a' is a new variable, what happens when the name collides with a global variable? a = 1
def foo():
a = a
foo()
> UnboundLocalError: local variable 'a' referenced before assignment
yuck! >>> a = 1
>>> def foo():
... a = globals()['a']
... print(a)
...
>>> foo()
1
Still yuck? >>> def foo():
... import __main__
... a = __main__.a
... print(a)
...
>>> foo()
1Python is generally quite good at having a verbose option to express things, so I'm surprised there's not an explicit way to reference the enclosing scope from an object.
a = 1
def foo():
x = a
a = 2
foo()
> UnboundLocalError: local variable 'a' referenced before assignment
Basically a name cannot be both global and local inside the same function. Otherwise it would be a total mess."a = a" just means assign the value of local variable "a" to a local variable "a", so the error makes sense.
def f():
a = 1
def g():
nonlocal a
a = 2
g()
print(a)
though nonlocal can't be used when referring to global variables... it's kinda bad.If you desire a path closer to clojure
Basilisp is much less mature than Hy but tries to stay generally compatible with Clojure, which is somewhat different than the goals of the Hy project.
doom emacs make it easy to get started.
I wonder: 1/ Shall we mix multiple paradigms of programming in the same project? 2/ Should we adopt a single programming language that supports multiple programming paradigms and utilise single paradigm for each given project, for the sake of minimizing the skill set required to the development team and – not necessarily –the learning curve?
(/ (+ (- b) (sqrt (- (* b b) (* 4 (* a c)))) (* 2 a))
is a pain in the neck, and even more so when you have to read it, while
(-b + sqrt(b*b - 4*a*c))/(2*a),
can be checked on the fly.
I am kind of amazed this hasn't finally sunk into programmers' hearts.
I can write ugly code in any language. But if I was writing this code in Clojure, I would probably do something like this:
(let [r1 (- (* b b) (* 4 a c))
r2 (+ (- b) (sqrt r1)))
r3 (* 2 a)]
(/ r2 r3))
That is perfectly readable to me.Yeah, you may say that I needed to introduce other variables to make this work and the code got bigger as a result, but on the other side how many people are actually writing this kinda of code on daily basis that are not Computer Scientists?
[1]: https://github.com/gilch/hissp
[2]: https://hissp.readthedocs.io/en/v0.3.0/tutorial.html#hello-w...
(py "(-b + sqrt(b*b - 4*a*c))/(2*a)") (/ (+ (- b)
(sqrt (- (* b b)
(* 4 (* a c))))
(* 2 a))
The trick is that a trail of any number of `)`'s should also end the line. Like any language, it takes some getting used to, but I find this tree structure perfectly readable now.Sure for the four-function calculator operations, operator precedence levels are a reasonable notation, but when you can define your own functions, and your programming language now needs a dozen precedence levels, it becomes hard to read. Lisp is completely unambiguous.
One of the reason as i know is, they have no idea of using unit test.