💾 Archived View for dioskouroi.xyz › thread › 24940838 captured on 2020-10-31 at 01:00:29. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
________________________________________________________________________________
Having built remote monitoring and firmware updating systems for high-current LiFePO4 batteries in Rust for low-level STM32 and ESP32 integrations, Clojure for backend and ClojureScript on the frontend - I can corroborate this.
The dynamism of Clojure and the memory safety of Rust are a strong combination, especially for streaming EDN data over websockets to Clojure or ClojureScript frontend.
However, neither Rust nor Clojure is the end-game. Eventually all languages will IMO adopt memory lifecycle management (lookahead guarantees) and borrow-checking as language features to do runtime optimization.
An interesting language in this regard is Carp, a high-performance statically-typed Lisp implemented in Haskell with borrow-checking:
https://github.com/carp-lang/Carp
It's a shame they did not opt to follow Clojure syntax more closely, or it would make the transition much easier for the growing Clojure community. Clojure seems to be winning the Lisp language wars.
Rust could have be so much more if it used S-expressions or M-expressions. The macro language is an abomination compared to Lisp, but I understand they had to lure the embedded C crowd.
Love Carp, the fact that it compiles to C means it can run everywhere like embedded (GBA[1], ATSAMD51[2], esp8266[3]) but is also expressive enough that you could build web apps with it[4].
It's very pre-v1 though so expect breakage and API change.
[1]
https://github.com/TimDeve/dino/blob/master/main.carp
[2]
https://github.com/TimDeve/carpgamer/blob/master/main.carp
[3]
https://github.com/TimDeve/carp-lang-arduino/blob/master/mai...
[4]
https://github.com/TimDeve/koi/blob/master/examples/notes-js...
> Eventually all languages will IMO adopt memory lifecycle management (lookahead guarantees) and borrow-checking as language features to do runtime optimization
Personally I'd be more interested in CPUs adding dedicated GC chips such as this paper:
https://ieeexplore.ieee.org/document/8695831
> It's a shame they did not opt to follow Clojure syntax more closely, or it would make the transition much easier for the growing Clojure community
I've been keeping an eye on Carp, but never tried it, at first glance it looked like they used the Clojure syntax, what part do they deviate from?
How come, I have the same stack. STM32 with Rust. Mqtt, Clojure for backend and ClojureScript with re-frame for frontend. Couldn't be happier.
Nice article in general, but seems to miss the most important point (at least for me) of why Clojure (and similar languages) are way better than anything else today: it's REPL driven development. Definitely more important for me than any of the other "why we love Clojure" points.
As long as Rust doesn't offer something similar as the REPL driven development Clojure offers, there is zero reason of preferring Rust (for me) over Clojure for the same tasks. Which, because of the last point ("Rust is not homoiconic") won't happen any time soon.
Now, if you need to do embedded system development, Rust might make more sense than Clojure, but it won't replace Clojure for the same tasks today.
RDD is the one big thing that really blows me away in Clojure. I now feel quite frustrated having to work in other languages where I'm missing the tight REPL integration.
It's ideal for getting into "flow", iterating on ideas, there's no waiting or downtime that you can get in other languages.
I don't know why REPL driven development and Clojure type REPL integration isn't how we all want to do development (assuming as you say it's not embedded or some other niche area). It's just so powerful as a tool for getting immediate feedback and allowing a tight, painless iteration on ideas.
I've only dabbled in Clojure and REPL-driven development, but is there a way of easily converting a REPL sessions into a suite of tests?
Coming from more of test-driven development background, I found that tinkering in the REPL was similar to what I would usually do with unit tests, but without the ability to easily build up a test suite that would prevent regressions in future.
Depends on how you use your REPL. I run the REPL in the background and connect vim (via conjure plugin) to the REPL, so I still type out my code into my editor, into files and buffers, and only save them to disk when I'm happy with the code.
So usually I end up mostly living within (comment) blocks around the functions I'm interacting with, and when I'm happy with it, move the code into the correct test namespace, which is a matter of just moving the code around and changing references to namespaces.
Not that I'm aware off, so I use both.
I'll start by writing a bunch of tests and have a test runner that runs all my tests everytime I change my source code and displays the results. It's fast, I have one project that runs ~1400 asserts and on each save it shows the results less than 2 seconds later.
THen I write the code in my IDE source editor and use a keyboard shortcut to send it to the REPL as I'm working. I wouldn't ever write code directly in the REPL. By the time I've got it working in the REPL I normally then see that all my tests are passing.
I’m a beginner clojurian but i’ve noticed some devs put a repl folder in their source tree, e.g. stuart sierra’s component project has a dev sub folder
https://github.com/stuartsierra/component/tree/master
with repl examples in addition to the tests.
I missed a trick in life, i started getting into Clojure in 2020 but about 5 years ago i used to sit 1 row of desks away from Stuart Sierra in Stirling and I don’t think i ever properly spoke to him save for hello or so. It was a funny setup in that operation, i had to fight for my small team to even have office space at the time!
Good callout! Adding "dev" files for facilitating REPL development is one way, adding something called "rich comments" is another one (that I tend to gravitate towards) where you have comment blocks (actual code, not commented code) in the same ns as the actual functions, exploring the usage of the functions. Was recently on the HN frontpage as well,
https://betweentwoparens.com/rich-comment-blocks
-
https://news.ycombinator.com/item?id=24718478
From my understanding
https://github.com/cognitect-labs/transcriptor
does this
* but is there a way of easily converting a REPL sessions into a suite of tests?*
Yes, it's called discipline ;)
In Clojure(or JS for example) you have to rely more on being disciplined around tests and documentation than with a statically typed language.
I've found I can get the same tight feedback loop at least for the times where the code will not compile. I use Cargo mode and an after-save hook so that each save runs `cargo check` and, on success, also runs `cargo fmt`. A nice side effect is that I don't need to care about formatting any more.
(defun cargo-process-check-sentinel (process event) (when (string= "finished\n" event) (rust-format-buffer))) (defun my-after-save-hook () (when (eq major-mode 'rust-mode) (set-process-sentinel (cargo-process-check) 'cargo-process-check-sentinel))) (add-hook 'after-save-hook #'my-after-save-hook)
Have you ever tried playing with OCaml? It's obviously not the same thing as a LISP, but its type system is similar to Rust (Rust was largely inspired by OCaml) and it's (much) more amenable to playing in the REPL.
I have not, as last time I checked, there was no vim plugin for editor integration with the OCaml REPL, but did another search now and found something that seems to make it possible (
https://github.com/jpalardy/vim-slime
) so might test it out at one point. OCaml always looked interesting, but now I could actually try it so thanks for the reminder!
Ocaml more amenable for the REPL than Lisp? You can't be serious. Look at CL with Slime, for example - it's seamless.
No, they are saying that OCaml is more amenable to REPLs that Rust, which is hard to argue against.
They begin their sentence with "obviously not the same thing as a LISP" so I'm sure they are aware that it can't get better than it is with lisps :)
Right, that makes more sense. I guess my brain didn't parse the sentence correctly.
I don’t know. I worked in Clojure for several years, and very seldom hit the REPL. I spent most of my exploration time in tests. In TypeScript, I split most of my exploration time between types and tests. Occasionally I hit the Node REPL, or the Chrome console. But doing speculative work in an editor, with all its features, has always felt a lot more productive to me. Maybe that’s why I ended up drifting away from Clojure anyway.
> speculative work in an editor, with all its features, has always felt a lot more productive to me
Yeah, I agree with this, which is why I spend all my time in the editor, which is connected to the REPL. I never leave my editor to write stuff manually in the REPL, I simply send the forms from my editor to the REPL to be evaluated. So the best of both worlds :)
Recent example of how that workflow looks like:
https://vlaaad.github.io/reveal/
What's on the left is vlaaad's editor, and the REPL is on the right. Usually I have a similar setup, but the output of the REPL goes straight into my editor instead of in a separate window.
One question for you, if you rarely used the REPL, how is your workflow with Clojure unless with a REPL? If you're working on a service, do you restart the process after each change or how to you avoid the REPL when working with Clojure?
Well I don’t work in Clojure at all anymore. But I guess this isn’t substantially different from how I work. The biggest difference is that I spend most of my time sending whatever speculative work to the compiler (TypeScript) or the test runner. Both of which are run in various forms of watch mode. When I worked in Clojure that’s pretty much how I worked then too. My flow now is almost entirely contained in VSCode, and the biggest change since I switched from Clojure is that I’ve embraced the IDE, static typing and a debugger.
I very rarely stray far from real TDD (red -> green -> refactor), and I very very rarely run whatever service I’m working on. The big exception is when I do (web) frontend work, which used to be my bread and butter but has been fairly limited for a few years.
In Emacs, there is tight integration between the editor and the REPL. You can write code in an Emacs buffer, and you can enter that namespace from the REPL and the code from the buffer is immediately available. You can also add new code directly from the REPL, but I usually just add what I want to an Emacs buffer and eval it. It's a very productive workflow.
Skip to 15:53 and 20:30 for examples:
https://videos.confluent.io/watch/yHoHM4Gxo7Bu1MCdo8vVh6
?
> and very seldom hit the REPL
That was the problem right there. Not getting used to a REPL driven workflow in Clojure is like using TypeScript without an IDE. It'll negate all benefits.
I don't know. I quite liked working with Clojure at the time. My previous experience had been:
1. PHP
2. JavaScript (browser)
3. JavaScript (Node)
The introduction to functional programming was eye opening to me, and made me a significantly better programmer. Working with ClojureScript, on the other hand, was painful. Numerous hours-long debugging sessions where I discovered I was passing a different type to core functions than I expected, causing wildly unexpected and baffling errors. Reporting issues and having them closed with "wontfix" because "don't do that".
That's what led me to TypeScript. And then of course the IDE. But I carried what I learned from FP with me. And now, having learned a powerful type system, it's made me a much better programmer still.
I don't mean the REPL is the only upside to Clojure, but it's the trade-off for the lack of static type checker with IDE.
If you needed to run the TypeScript compiler as a CLI and it spiled errors on the command line, that also wouldn't be a very pleasant experience.
I've known a few people coming from Java or C# to Clojure, never really getting into the REPL, would mostly program the same way they used too, and they went back. Which frankly doesn't surprise me, I'd go back as well if that was the case. The dynamism of Clojure isn't there out of lazyness to implement a static checker, but to be leveraged both at run-time and development time.
Clojure is still a lovely language without it, but if you don't leverage the dynamism, then it'll seem like the language is lacking staticness, obviously, since you're using it statically except it has no features built around staticness.
That said, I don't blame anyone for not leaning into the REPL, Clojure and especially ClojureScript doesn't make it easy, the tooling is rough around the edges.
As an aside, I think everyone should explore multiple paradigms and languages, and having good experience in a statically typed language will make you a better programmer, even if you were to go back to Clojure one day. The act of thinking about the types behind your variables actually makes you a better programmer in dynamic languages, because you start to have a static checker inside your head, and over time make less and less type related mistakes or can catch them very quickly.
"Many people try to compare Rust to Go, but this is flawed. Go is an ancient board game that emphasizes strategy."
This cracked me up so hard I can no longer deny being a straight up nerd.
What I also found is that Racket is a very good match to Rust, in a similar style as writing Python and extending it with translated C code where speed is needed. The advantage is that both languages allow for a functional style and Racket is much faster than Python.
Racket is, almost like Clojure, highly interactive (though not completely so, it has a conceptual separation of compile time and run time), and on top of that, it can call easily into a C ABI, which Rust provides. Both languages (Rust and Racket) are well-suited for writing pure functions returning immutable results which allows for a nice match in style. Calling across the language boundary is a bit slower than in the Python/C case but it can achieve a decent speed-up. Racket's places allow for parallel execution with almost separate memory images, and it also has green threads. And Racket has batteries such as a GUI toolkit and image data types included which is really nice.
“Learning Rust will probably not do much to solve that problem for you. It won't assist you in making the ontological leap from a tired stereotype into something sentient and real. You will remain a replaceable silhouette with no discernible identity. It might even exacerbate the problem.“
I'd have assumed a primer on Rust for Clojure programmers would spend a lot of time on what a pain closures in Rust (necessarily!) are. :)
I was interested in Rust for the strong type system and borrow checker and such as well as praise from functional programmers I trust but my experience with trying to do FP in Rust was so dreadful that I put it down and stopped learning it. Perhaps I will need it badly enough to push through this someday.
The thing is Rust isn't functional, in that the coding patterns rely a lot on mutation actually, but the compiler can assert that your mutation will be race free, and that you won't segfault and most likely won't leak memory.
So you can say it appeals to functional programmers in the sense that they tend to be wary of mutation, so does Rust in a way. Except where a functional language will turn to immutability, Rust turns to an advanced compiler and linear types to safeguard you against the dangers of mutation.
The real benefits are performance, which has always been the achilles heel of functional programmers.
Yeah, I quickly discovered it’s not functional and has a much different perspective on mutability, despite clearly being inspired in some aspects by FP.
As for performance, I don’t think it’s nearly as large a problem for FP in general as some make it out to be. OCaml and Haskell are typically quite fast, but of course they rely on GC, so they aren’t suitable for environments that are heavily resource constrained or where pauses are unacceptable.
Remember you can only have one mutable reference whereas you can have any number of immutable references. The ownership rules incentivize immutability.
This is wonderfully written :)
Indeed, a fantastic read, even though I am not a Clojure programmer
Dumb question from a curious Clojure guy : how terse is Rust codebase for non-trivial cases?
(2015)