• troad a day ago

    I'm very excited for Jank, I wish the creator well.

    If I could make one tiny plea, it would be to focus on tooling too, and ensuring the experience for someone trying Jank out is as smooth as possible. Don't assume everyone is already set up with paredit and can fire off emacs chords without a thought. I suspect that Jank will be of particular interest to C++ programmers, many of whom are used to a very different dev paradigm.

    The Clojure community has done a great job at trying to smooth out the rough edges of Lisp tooling, and ensure there are on-ramps for newcomers (e.g. things like Calva for VS Code). I hope Jank keeps this up, because those first impressions really do matter. I'd hate to watch people bounce off Jank because they get stuck on trying to figure emacs out, or because they get frustrated trying to keep parentheses matched in Notepad.

    • Jeaye 21 hours ago

      You're absolutely right. To start with, I'm focusing on capturing and converting existing Clojure devs. However, there's a very large pool of C++ devs who could benefit from jank. As you said, jank not only needs to be ready for them, they need to be educated on lisps, functional programming, data-oriented programming, REPL-based interactive development, etc. It's not an easy learning curve right now, into Clojure.

      I aim to focus a good deal of time, post jank's launch, to create materials which enable this. Materials specifically targeted at OOP devs coming from the native space.

      • vlovich123 8 hours ago

        As someone who might be in your target demographic in theory but not familiar with Clojure, what capabilities does Clojure itself offer as a language that a different mainstream scripting language doesn’t?

        For example, I’ve always thought that the interesting part of Erlang is Beam & the uninteresting part was a niche language. Beam with JS as the language is much more approachable & leverages a rich existing ecosystem of tooling & libraries. Indeed, if you look at it the right way Cloudflare Workers is exactly that; isolated virtual processes within a single OS process that communicate with each other using a message based IPC & if they crash just the microservice is taken down & transparently relaunched.

        So from that perspective, why Clojure instead of the same features on a different more popular language?

        • ndr 8 hours ago

          It's hard to describe what immutable/persistent data structures do to your thinking.

          A lot of responsibilities move to the edges and you're left being able to think and tests pieces of code in isolation. The repl becomes usable, and you code from inside your running program. It's a steep step to get started but it's going to transform how you think about some problems.

        • butterisgood 17 hours ago

          I can imagine Jank being incredible “glue” to start until it takes over by osmosis. (For c++ of course)

          Maybe I should play some advent of code games with it to get comfy!

        • roenxi a day ago

          This seems like an opportunity to plug the good work https://www.clojuriststogether.org/ do. They do good work. Plug concludes.

          Clojure - like many languages - seems to be benefiting a lot from the work the IDE people have been putting in to good infrastructure. A lot of the magic is being moved out of Emacs towards tools like Treesitter and the LSP server which are more platform independent. Not a huge amount to do with Clojure per say, but the Clojure ecosystem is benefiting a lot from it. The Emacs specific stuff is becoming very Emacs-specific (like paraedit, bless its socks).

          • waffletower 5 hours ago

            I am wary of LSP. I should try LSP again using eastwood, which probably has its trade-offs. While I absolutely admire Borkdude's many gifts to the Clojure community, the lack of static analysis in clj-kondo is a non-starter for me -- macro support is painfully manual. Because of its speed, clj-kondo likely is more performant in an editor LSP context, but too opinionated for me.

            • diggan 9 hours ago

              I think Clojure has benefit way more from nRepl and everything in that ecosystem, than LSP and that newfangled stuff, at least my personal setup relies on nRepl and doesn't even use LSP afaik.

            • ab5tract 20 hours ago

              The “use emacs” prerequisite that shows up frequently when exploring Lisps has been such a dealbreaker for me in the past. I have no patience for learning “chords” or even using a non-modal-but-still-not-just-normal-input editor simply to explore a language. My impression being that these chords morph depending on “modes” doesn’t help at all. At least with vim you can learn a piece at a time (sorry, not trying to revive last century’s favorite flame war, but it’s true).

              Also: no one has managed to explain to me what makes emacs so uniquely capable of interacting with a running interpreter, despite this usually being the USP I hear for “needing” emacs when attempting to Lisp.

              • pritambaral 14 hours ago

                > Also: no one has managed to explain to me what makes emacs so uniquely capable of interacting with a running interpreter, despite this usually being the USP I hear for “needing” emacs when attempting to Lisp.

                As an Emacs user that has to work in teams of non-Emacs users, the answer to this boils down to culture and ecosystem maturity.

                Purely technically, there's nothing that Emacs allows me to do with REPL Driven Development that _couldn't _ be done in another editor. Practically? I still haven't been able to even get (sufficiently) started with any editor.

                I often have to work on a Rails codebase that takes tens of seconds just to start. Dev cycles in the traditional Edit-Restart flow were so painful that I wished badly to burn this codebase down and rewrite it all in a language and style that supports a Lisp-style REPL. Then I discovered inf-ruby.el in Emacs. It allows me to just edit the code and reload only what's changed. It automatically inserts the correct `module Xyz; ...; end` etc. No more restarts.

                I showed it to my coworkers. They shared the pain with restarts. And yet, to this day, none of them have an equivalent to inf-ruby in their editors.

                inf-ruby.el is less than 1400 lines of code. It's easy enough to implement in Vim, IntelliJ, VS Code, anything really. But it exists only in Emacs, so far. Why? I'm sure because some Emacs user, once upon a time, wished similarly to have a more Lispy REPL for their Ruby dev work, and just built it. Because they were used to it, from having used Emacs or other Lisps with Emacs.

                Compare that to when I first tried to support Common Lisp development on VS Code. A language that already has full support for REPL Driven Development with an interactive debugger built-in. Nope, we aren't gonna use any of that, because in VS Code land, we use LSP. A model that really only works for languages that can be statically analyzed. You want Go To Definition on a method that's included from a module and called on a receiver that's a dynamic variable? Well, sucks to be you, I guess.

                So, while people make do with the severe limitations of Solargraph or Ruby-LSP, I use robe.el — which adds lisp style code intelligence by running inside your Ruby process — and get code intelligence that actually works with a language as dynamic as Ruby. Again, robe.el was just there for me to use. Again, there's nothing about Emacs that makes robe.el possible in Emacs but not in other editors. Again, there's no equivalent to it (that I've found) in the ecosystems of other editors.

                • ab5tract 13 hours ago

                  This is honestly fascinating stuff, so thank you for sharing!

                  I wonder if it is an exposure issue: only those who use emacs know it is possible and those who know emacs have little incentive to leave their editor-OS.

                  I guess I’ll have to dig into some of these examples you have shared and see if I can comprehend and/or relocate their magic.

                • Jeaye 20 hours ago

                  As the guy building jank, I agree with you about emacs, chords, etc. I'm a vimmer and I can tell you that Clojure is superb in Vim land (Conjure), VS Code land (Calva), Emacs land (??? some major mode), and IntelliJ land (Cursive).

                  • chii 16 hours ago

                    realistically, it's the REPL, not the editor, that makes lisp what it is - not just having a repl running, but to have your app be part of the repl, and you develop it bit by bit.

                    The javascript/UI people have found live reloading/editing to be a game changer, but this has been the case for lisp development since...well, the beginning!

                    • fmbb 15 hours ago

                      Live reloading (recompiling per method, editing values in your live runtime image) has been with UI development since since the beginning of GUIs. Smalltalk is great.

                      The 90s with C++ and Java broke with history. Thankfully the rise of web apps has given us iteration speed back!

                      • pjmlp 4 hours ago

                        Kind of.

                        Visual Age for C++ inherited the Smalltalk experience, alongside Energize C++,provided a similar experience, but were too expensive for early 1990's hardware and too resource hungry.

                        Live++ brought the experience back to game developers.

                        Java has supported partial live reloading since early days, and for those willing to pay for it, JRebel takes the experience further, back to Lisp/Smalltalk.

                        • funny_falcon 13 hours ago

                          First SmallTalk versions were built with Lisp. “Live environment” and “world image” were just inherited.

                    • ab5tract 20 hours ago

                      Awesome to read this!

                      Since I’ve got you here, can I ask you for your thoughts on LispE and how it contrasts with jank’s approach? It seems clear that they both arrive from different lineages of the Lisp kingdom. LispE is also found dangling a few toes at depth within the icy waters of array programming.

                      Perhaps the only connection between the two is that the one makes me think of the other. But if there’s more, though, I’d love to read about it.

                      • Jeaye 18 hours ago

                        This is the first I've seen of LispE, so not a very informed opinion.

                        Different lineages, for sure. Clojure really stands on its own in the lisp world and some die-hard lispers claim that it's not a lisp at all. However, for Clojure devs, I think we generally aren't interested in using the other lisps in practice, for building practical software. We just appreciate them for their lispiness. So the main difference will be that LispE is more like CL than like Clojure.

                        Aside from that, LispE is interpreted, whereas jank is JIT compiled with full AOT compilation support, using LLVM. By using Clang/LLVM, jank also has full access to C++ interop, whereas most interpreted lisps are sandboxes on their own.

                        I'm not familiar with this side of traditional lisps very much. Someone may be able to jump in to embellish or correct.

                        • ab5tract 13 hours ago

                          Got it! Thanks for chiming in off the cuff like this. Ill be keeping an eye on jank for sure.

                    • pjmlp 15 hours ago

                      The proper answer for Lisps should be use Allegro or LispWorks, which actually provide a full blown environment as Lisp environments of by gone eras.

                      Does vim display inline graphics in a repl, with editing capabilities?

                      This is one of the basic features of classical graphical Lisp environments from the 1980's, IPython and Jupyter notebooks, or better, Mathematica are only building upon this.

                      Emacs and vim are not quite at the same experience level.

                      • disgruntledphd2 14 hours ago

                        Org mode in emacs does this. It's a little clunky but it definitely works (better than notebooks IMO).

                        • pjmlp 10 hours ago

                          Being a bit clunky is not really working, and I bet it doesn't do the full extent the other Lisp environments allow, including code reloading, jumping into the debugger from the REPL, redrawing everything being shown in the REPL after returning from the debugger with the new code changes applied.

                      • veqq 20 hours ago

                        > what makes emacs so uniquely capable

                        Emacs is easily programmable (and in lisp), more unixy than modern Linux, like a whole operating system. Lispers make tools for it because it's very easy for them. Lisp tools have been far advanced of others; LSP wasn't initially adopted in Lisp communities because better tools (swank) were already around.

                        • pjmlp 4 hours ago

                          Emacs linage predates UNIX. :)

                    • dang a day ago

                      Related. Others?

                      I quit my job to work on my programming language - https://news.ycombinator.com/item?id=42658898 - Jan 2025 (32 comments)

                      Jank: Programming Language - https://news.ycombinator.com/item?id=42477992 - Dec 2024 (3 comments)

                      Jank is now running on LLVM IR - https://news.ycombinator.com/item?id=42276672 - Nov 2024 (16 comments)

                      Jank development update – Moving to LLVM IR - https://news.ycombinator.com/item?id=41845669 - Oct 2024 (49 comments)

                      The Jank Language: LLVM Hosted Clojure - https://news.ycombinator.com/item?id=32493217 - Aug 2022 (79 comments)

                      • tombert a day ago

                        I'm not super familiar with this project, so forgive a bit of ignorance on this; what does this buy you over vanilla Clojure and native-image compiling with GraalVM.

                        This is a genuine question, not meant to dismiss the project!

                        ETA:

                        Sorry, further down the article answered my question:

                        > “jank is also a good fit for any Clojure devs who want a lighter runtime without sacrificing JIT compilation, as they would if they used a Graal native image, or if they want easy access to native libraries for whatever reason.”

                        • Jeaye 21 hours ago

                          The two biggest things are:

                          1. You get a native binary without sacrificing interactive programming. With a Graal native image, all of that goes away. With jank, you can still REPL in, change things, JIT compile code, etc.

                          2. You get Clojure -> JVM level of seamless interop from jank -> C++. I am pretty darn sure that this will be unprecedented, given the challenges of the native space (no standard reflection, differing ABIs, C++ templates, etc).

                          Aside from that, jank is making some huge quality of life improvements over Clojure JVM. I've shared some of this already, but I'll have a post coming out EOQ which will demonstrate the difference in compiler errors between the two. It's night and day.

                          • tombert 21 hours ago

                            I'm looking forward to it; being able to utilize C and C++ libraries directly from Clojure is something that I could see being pretty valuable.

                            I've been debating trying my hand at making a simple game engine...it would be great if I could use a Lisp to do it.

                            • 2mlWQbCK 8 hours ago

                              Did you look at Janet? Of course there are many other embeddable LISPs, but I think in particular Janet is a good, light, (subset of) Clojure-like language.

                              It is built into TIC-80 by default, making it already useful for (small) games.

                              https://janet-lang.org/ https://tic80.com/

                              • lelanthran 10 hours ago

                                You can use lisp with almost native C support, if you use ecl or gcl.

                                • guenthert 10 hours ago

                                  ecl and gcl compile to C. That's fine, but not necessary to merely use C libraries; for that you need a C foreign function interface (FFI), which most surviving CL compilers offer. C++ is more of a challenge, hence the Clasp project.

                                  • lelanthran 4 hours ago

                                    > ecl and gcl compile to C. That's fine, but not necessary to merely use C libraries; for that you need a C foreign function interface (FFI)

                                    Last time I checked, they also allowed literal C code inline in Lisp programs. That's a bit more than the other Lisps which offer FFI.

                                    • beepbooptheory 7 hours ago

                                      I feel like Clasp has huge game development potential but has gravitated towards science just because that's where it came from. Like surely one could use it to get good godot bindings for example, but I haven't seen stuff like that around.

                              • joeevans1000 a day ago

                                You can compile a native program. Not one that requires the JVM. And it also interops with a number of more native graphical tools (as opposed to having other use things that require/work-from java). To those coming to this thread who might not be familiar with Clojure: Clojure already can sit on top of javascript (as Clojurescript) and there was work making it work on top of, if I recall right, some of the C languages. But those latter ones don't seem to be anywhere near the capabilities of this project.

                              • vmsp a day ago

                                Looks similar to Clasp but implements Clojure instead of Common Lisp.

                                https://clasp-developers.github.io/

                                • binary132 17 hours ago

                                  Clasp is really cool but it’s a shame that it does not support Windows properly. A lot of C++ is used on Windows. I’ve seen this pattern repeated elsewhere — I think it has to do with Windows exception handling being different and complicated. Hopefully jank does not suffer the same fate, since author is a gamedev!

                                  • mark_l_watson 9 hours ago

                                    Cool. I tried to build Clasp from source a long while ago, and failed. Now a brew install….

                                    • elcritch 17 hours ago

                                      The Jank creator talking with folks at CERN about integrating with their C++ codebase makes sense too. Clasp was originally used to do some molecular simulation if memory recalls.

                                      • hatmatrix 20 hours ago

                                        Good point. When Clojure first came out it was questioned whether it's a lisp at all, but now I commonly hear many say it is the best Lisp.

                                      • barrell 12 hours ago

                                        Always happy to see clojure on the front page, doubly happy to see jank! I just scoured the internet last week trying to find all the information I could on it.

                                        As a clojure(script) developer of 10 years, I still try and avoid anything JVM related. Not that I have anything against the JVM — but I never did any Java programming, and to really learn about the JVM you have to learn quite a bit about Java… which is just very low on my priority list.

                                        I have been dreaming about jank a little bit, so maybe this is a good place to ask the question, since I see the jank developer is reading the comments: would it be possible to write a module in jank that can be used in a Swift/iOS application (currently or in the future)? I assume so, but I’m not sure how accessible the outputs of jank are to other c libraries.

                                        The reason I ask is I have an offline clojurescript front end. If I want native mobile apps, that means I either have to duplicate the logic, figure out some JavaScript bridge, use react native, or use dart — none of which seem ideal. Ideally I could just extract the critical business logic into a few modules and generate the header files for swift, import directly into cljs for web, and use as regular clojure for android.

                                        Totally understandable if this isn’t a valid use case for jank, but it’s what’s captured my imagination :)

                                        • Jeaye 4 hours ago

                                          Yes, definitely. You'll be able to use it to REPL into your phone, for development, and it'll AOT compile alongside the rest of your app for release (Apple is a stickler about JIT compilation in released apps). This, we get largely just by using LLVM.

                                        • Tcepsa a day ago

                                          As a long-time fan of Clojure (I've been using it to varying degrees since 2008 and it is my favorite programming language) I'm really excited about the interpretability it sounds like this will open up!!

                                          • tmtvl 5 hours ago

                                            Somewhat related-ish: Cakelisp <https://news.ycombinator.com/item?id=36154069>

                                            • fuhsnn 20 hours ago

                                              >Seamless C++ interop

                                              More detail would be appreciated, I'm not aware of any non-transpiling language that actually support full C++ RTTI/exceptions interop.

                                              • pritambaral 14 hours ago

                                                Clasp, and implementation of Common Lisp in C++ on LLVM, has exceptions interop. The creator of Clasp even hooked up the Lisp GC to be able to automatically manage C++ objects. A compacting GC, mind, that automatically updates pointers to the managed C++ objects that it moves.

                                              • Jeaye 15 hours ago

                                                More detail to come. You're right, it's unprecedented.

                                              • rcarmo 14 hours ago

                                                Somehow I completely missed this. I mostly stopped using Clojure solely because of the JVM madness (yes, I know about babaskha, won’t use it due to the GraalVM dependency and possible Oracle tentacles lurking there) and have fallen back to things like Hy (which can be clunky but at least runs everywhere I have a Python interpreter). Something that has better Clojure-like syntax and uses LLVM aid very appealing, provided it can do (and serve) HTTP requests sanely and has enough batteries included.

                                                Any good pointers to existing libraries to get a feel for the ecosystem?

                                                • bobnamob 14 hours ago

                                                  As far as I'm aware, jank isn't at the point where libraries exist. It's still under initial development and doesn't implement all of Clojure just yet.

                                                  • sorry_i_lisp 10 hours ago

                                                    No value judgment, what do you mean by "JVM madness"?

                                                    • rcarmo 5 hours ago

                                                      Slowness and bloat, plus the sheer dependency footprint.

                                                      • pjmlp 4 hours ago

                                                        Slower than C, C++, Rust, yeah still faster than most other alternatives, with exception of .NET.

                                                        What some call bloat, others see saved effort not reinventing wheels.

                                                  • upghost a day ago

                                                    Curious what the story is for slotting Jank into gaming. Seems like a fit for Unreal, but I really love the Component-Based-Architecture of Unity. Would you get it into your Unity project the same was as getting normal C++ into your project?

                                                    • Jeaye a day ago

                                                      jank can embed into any native program like lua would. But it the offers seamless interop back into C++, including the ability to instantiate new templates. So, anywhere lua is being embedded, jank could be used while providing a tighter integration to C++, interactive dev with a REPL, and proper lisp macros.

                                                      • 2mlWQbCK 7 hours ago

                                                        A good alternative for game engines where Lua has literally already been embedded is to use Fennel, a LISP that borrows some Clojure-like syntax and that compiles to Lua-code. In some cases it should be possible, like for Löve 2D, to include a few lines of boilerplate Lua code to load Fennel-files on the fly. Elsewhere you probably have to pre-compile to Lua.

                                                        https://fennel-lang.org/

                                                    • danbolt 15 hours ago

                                                      I really appreciate the seamless rawdogging into C++ that this language provides. It’s the sort of ambition I’m here for.

                                                      Or, I find a lot of script runtimes in games have a strict boundary separating them from the native code (often for good reasons), but I wonder if we could integrate more tightly given static analysis has come a long way.

                                                      • Jeaye 4 hours ago

                                                        "Seamless rawdogging into C++" is the best thing I've read in a while. ^_^

                                                        Anything less than seamless rawdogging would be neutered C++.

                                                      • avbanks 12 hours ago

                                                        Wishing you all the best! Thank you for being ambitious enough to try this out. I'm going to test this out and write about it on my blog.

                                                        • jwr 19 hours ago

                                                          This is very cool and I intend to check on it from time to time. I use Clojure (and ClojureScript) all the time, and while performance on the JVM has never been an issue for me (the JVM is an impressive piece of engineering), I'm always on the lookout for new things!

                                                          • systems 20 hours ago

                                                            My only comment, there are must nicer system programming languages that could have been host, I am mainly thinking OCaml, as for me zig would have also been nicer than C++

                                                            My main objection to C++ is that in 2025 its not a language I would want to learn or use, nothing about its inherit qualities

                                                            • Jeaye 20 hours ago

                                                              I hear you! There are millions upon millions of lines of C++ in production, though. C++ is still the de facto language in game dev and many other industries. I'd choose Rust over C++ for any greenfield project, but C++ isn't going anywhere.

                                                              I'm starting with C++ interop but will aim to provide interop with other native langs going forward, thanks to LLVM.

                                                            • aaronbrethorst 5 hours ago

                                                              How is it pronounced? Dzank or Yonk?

                                                              • Jeaye 4 hours ago

                                                                j like gentleman, ank like tank

                                                              • runevault 21 hours ago

                                                                As someone who wants more in the space of Lisp-style languages with more interest in native, I'm going to be keeping an eye on Jank. Actually had this article recommended on my phone earlier.

                                                                • VyseofArcadia a day ago

                                                                  Very excited for jank. I've been hacking together my weekend projects in Common Lisp for roughly a year. I've been wanting to look at Clojure as a modern lisp, but I want no truck with the JVM.

                                                                  • cutler 18 hours ago

                                                                    JVM isn't the only option. There's nbb (Clojurescript for Node.js) and Babashka (Bash in Clojure). You can also use GraalVM for native compilation.

                                                                  • slifin 7 hours ago

                                                                    I hope this works with flow storm one day

                                                                    • guenthert 10 hours ago

                                                                      clojure gets robust, efficient garbage collection thanks to the JVM. What is jank's approach to memory management?

                                                                      • Jeaye 4 hours ago

                                                                        jank is also garbage collected, since Clojure practically demands it. Currently using Boehm, a conservative GC, but we'll need to switch to something more competitive when that becomes a priority. I have my eyes on MMTK + Immix.

                                                                      • zem a day ago

                                                                        super excited about the potential for native gui apps

                                                                        • joeevans1000 a day ago

                                                                          This. Is. Awesome. Please keep moving forward with it. If it works well it will be a total game changer.

                                                                          • j-pb 15 hours ago

                                                                            As an ex-clojure dev that picked up Rust, I have no interest in this as long as it doesn't have ownership.

                                                                            Once you used lexical scoping, you're not going back to dynamic scope either, the same goes for the ability to reason about "where an object currently is" lexicographically.

                                                                            I think Carp (https://github.com/carp-lang/Carp) is a much more interesting contender for a native clojure, simply because it has lexical ownership.

                                                                            • widdershins 12 hours ago

                                                                              I'm not an expert in Clojure or Rust, but an interested outsider in both, so please don't take my question as a challenge but a genuine curiosity:

                                                                              Doesn't the focus on immutable data make most of the ownership stuff redundant? My understanding was that ownership lets you be sure that you're the only mutable owner of a resource, or otherwise that you share the resource immutably. If data is (almost) always immutable, which seems to be a founding principle of Clojure, then what does ownership/borrowing buy you?

                                                                              • mrkeen 10 hours ago

                                                                                > If data is (almost) always immutable, which seems to be a founding principle of Clojure

                                                                                Being immutable isn't some "best-practice" like eating your veggies, where the more you eat, the healthier you are, and occasional sugar is OK.

                                                                                I either can reuse the result of getFoo() or I can't. If getFoo() is 99% immutable - as the caller, it's my job to consider taking locks and/or making defensive copies.

                                                                                If I want to publish library code that calls getFoo(), now my callers will need to consider taking locks and/or making defensive copies.

                                                                                Your question was about Rust ownership, and I answered about Haskell immutability - but they're both the same in that they're pretty strict compiler rules, and you have to stray into unsafe territory to violate them.

                                                                                • 2mlWQbCK 7 hours ago

                                                                                  I am not sure where "almost" came from. Data in Clojure is 100% immutable? You can pass around data between functions or between threads and never worry about anyone modifying it. I believe Clojure is less strict about I/O than what Haskell is(?), but once some data has been created it can not be modified. Any modification will return a new thing, never modify the original thing.

                                                                                  Java interop breaks things of course, much like unsafe Rust breaks guarantees of that language.

                                                                                  • mrkeen 7 hours ago

                                                                                    Given some Clojure function getFoo() which only calls into the Clojure std lib, and doesn't call any function with 'unsafe' (or similar) in the name, is it always true that:

                                                                                      getFoo() = getFoo()
                                                                                    • 2mlWQbCK 6 hours ago

                                                                                      Functions are not always pure, no. Clojure is not so strict about I/O, meaning that function could for instance call out to make a database request. But I think that is unrelated to immutability or ownership of data?

                                                                                      Immutability guarantees if I call getFoo(x) I can trust that nothing happens to x (assuming x is proper Clojure data and not some unsafe other thing like a Java Object). And getFoo can likewise return anything without there being any risk that anyone else modifies that thing. It does not matter who owns x or who owns the return value from getFoo, since no one is able to modify those things anyway.

                                                                                      * Database request was a bad example as, as far as I know, there is nothing built-in for supporting that, so that would already be in unsafe Java territory. But there are non-pure functions built into the Clojure standard library that getFoo could call to return different results on the same input, like slurp or deref, and probably many other ones (I have not used Clojure for a long time).

                                                                                • j-pb 12 hours ago

                                                                                  I gave a long-ish answer to a sibling comment, that maybe sheds some light on that question, but I'll try to also give a concrete example where ownership complements immutability. (You best read that response first before continuing here.)

                                                                                  You are right in that immutability or more precisely the persistence that copy-on-write brings somewhat solves many of the issues that ownership also solves.

                                                                                  Persistence allows you to not care as much about who owns what, because whenever something is modified it is simply copied (with structural sharing), so you know that whatever you have is local anyways, not because you are the only one who has exclusive access to the thing, but because you make a copy of it at the point where answering that question would matter.

                                                                                  But you miss out on two things from that:

                                                                                  - Performance: Those copies are cheap but not free. Borrowing complements _persistent_ immutability in that regard extremely well, because it allows you to check if you're currently the only one using something and modify it mutably in that case. Clojure has a concept called transients, where you create an intermediately mutable version of an immutable thing to perform batch updates, but you still have to manage that yourself, with ownership it happens magically behind the scenes.

                                                                                  - Exclusive types: The other comment I wrote contains an example of this. I have an `ExclusiveID` type in my Database that is similar to a `row_id`. I can recover a lot of the capabilities of transactions and software transactional memory from these, simply because a user can rely on `ExclusiveID`s existing only at a single point at a time in their code. And `ExclusiveID` itself is a completely immutable thing, but the "contract" on how it can be used is what makes it powerful.

                                                                                  Immutability on a domain logic level also does not help you when it comes to dealing with inherently immutable things like devices and networks, whereas ownership does.

                                                                                  So I would argue that those two concepts are complementing each other extremely well, both implementation wise, but also in terms of a the mental model of how your code works.

                                                                                • Jeaye 15 hours ago

                                                                                  Cool, dude! I also dig Rust.

                                                                                  Carp is a very neat project, though it's not a Clojure. It's just Clojure-like. jank is Clojure.

                                                                                  • j-pb 11 hours ago

                                                                                    This wasn't meant as a jab at Clojure (or Jank for that matter). That language and its ecosystem has heavily shaped and influenced me early on in my career to the point where I'm essentially writing Datascript in Rust (https://trible.space).

                                                                                    My point was that ownership has become a language feature that I expect, like lexical scope has become a language feature that most people expect.

                                                                                    It complements persistent immutable data-structures extremely well, especially in terms of performance via automatic transient-ication, and if you're operating in the native space I can't imagine that performance isn't on top of the list.

                                                                                    So in that sense, great language, please consider evolving beyond Clojure (archaic?) ownership model, because I'm pretty tired of Rusts bloated syntax and lovecraftian macro system.

                                                                                  • Guthur 15 hours ago

                                                                                    I think if you're overly using dynamic scoping in clojure you are probably doing it very wrong.

                                                                                    I used clojure professionally for a number of years and I can't remember ever really using it.

                                                                                    • mrkeen 14 hours ago

                                                                                      Parent's not talking about dynamic scoping in clojure. It's a comparison to designed to elicit a reaction, e.g. "probably doing it very wrong".

                                                                                      I.e. if you're not tracking ownership in new-lang, you're probably doing it very wrong.

                                                                                      • j-pb 12 hours ago

                                                                                        It's not a comparison to bash non-ownership as "very wrong", but arguing that lexical scope and ownership are literally the equivalent things.

                                                                                        Lexical scope allows you to reason about scope on a syntactical level.

                                                                                        Take this code:

                                                                                          let x = 5;
                                                                                        
                                                                                          fn foo():
                                                                                          return x \* 2
                                                                                        
                                                                                          fn bar():
                                                                                            let x = 10
                                                                                            return foo()
                                                                                        
                                                                                          fn baz():
                                                                                            let x = "hello world!"
                                                                                            return foo()
                                                                                        
                                                                                          bar()
                                                                                        
                                                                                        With lexical scope, you can look at `foo` and you know immediately what it returns, the behaviour of the function is completely local to it and is a syntactical property of the program.

                                                                                        With lexical scope you just don't know what's returned, for all you know x might not even be a number, but could be any type that just happens to be brought into scope.

                                                                                        Ownership is similar, in that you make the existence of a value a local property. If a 'thing' is inside a variable then you can move it somewhere else, but it also means that it is no longer in that variable. And that tracking of where what is, is a syntactical property, just like with lexical scope.

                                                                                        Clojure has software transactional memory. With ownership it wouldn't need half of the machinery (only that for rollback):

                                                                                          let blocked_accounts = Set()
                                                                                        
                                                                                          let account_bob = new ExclusiveBankAccount(200)
                                                                                          let account_alice = new ExclusiveBankAccount(600)
                                                                                        
                                                                                          // ExclusiveBankAccount cannot be copied or cloned
                                                                                        
                                                                                          fn transfer(source_acc, target_acc, ammount):
                                                                                            if source_acc.deduce(amount.copy()):
                                                                                              target_acc.deposit(amount.copy())
                                                                                        
                                                                                          fn block(acc):
                                                                                            let acc_identifier = acc.identifier.copy() // identifiers can be copied and are not exclusive
                                                                                            blocked_accounts.put(acc)
                                                                                            return acc_identifier
                                                                                        
                                                                                          fn unblock(acc):
                                                                                            return blocked_accounts.take(acc)
                                                                                        
                                                                                          fn do_invalid_stuff():
                                                                                            let good_account = new ExclusiveBankAccount(200)
                                                                                            let bad_account = new ExclusiveBankAccount(200)
                                                                                        
                                                                                            block(bad_account)
                                                                                          
                                                                                            transfer(bad_account, good_account) // <- this will fail because bad_account was moved by the block function and no longer exists here, it's invalid syntactically
                                                                                        
                                                                                          fn do_valid_stuff():
                                                                                            let good_account = new ExclusiveBankAccount(200)
                                                                                            let bad_account = new ExclusiveBankAccount(200)
                                                                                        
                                                                                            let ident = block(bad_account)
                                                                                            let restored_account = unblock(ident)
                                                                                          
                                                                                            transfer(restored_account, good_account, 100)
                                                                                        
                                                                                        The above code makes sure that:

                                                                                        - you can only transfer funds between two accounts in a thread safe way

                                                                                        - you can only transfer funds between accounts that are not blocked

                                                                                        Simply by not allowing something like this on a syntactical level you get a much cleaner understanding of what your code does. After a while you're wondering why we allowed anything else in the first place.

                                                                                          let x = thing();
                                                                                          let y = x; // the thing is moved from x to y here
                                                                                          print(x)
                                                                                        
                                                                                        The fact that automatic memory management falls out of this is almost accidental, if you can track where a value is at any given time, you can also track the references to it, and the resources it uses. But that is not what truly makes it amazing, the fact that you can mentally think about digital objects as if they were physical objects is.
                                                                                        • mrkeen 10 hours ago

                                                                                          Oh right. Enlightening!

                                                                                          It's interesting you mention STM. That's one of the reasons I only dabble in Rust instead of switching over to it completely.

                                                                                          In do_invalid_stuff(),

                                                                                            block(bad_account)
                                                                                            transfer(bad_account, good_account) // <- this will fail
                                                                                          
                                                                                          This can only be determined syntactically if you and the compiler agree to stay on a single thread, right? I would expect STM to be the thing which safely bridges across threads - since it will realistically a customer will call transfer() but a bank manager will call block().
                                                                                          • j-pb 7 hours ago

                                                                                            My sibling comment is explaining this correctly but maybe using a bit too much Rust jargon for the uninitiated.

                                                                                            Put simply: How would two thread get access to the same account at the same time? There would be a point in time where the object would have to be split to end up in both of them, which would be invalid.

                                                                                            This is why you see things like this in rust

                                                                                              let x = 5;
                                                                                              let thread = thread::spawn(move || { // <- important bit
                                                                                                println!("{}", x);
                                                                                              });
                                                                                            
                                                                                              println!("{}", x); // <- this would fail
                                                                                            
                                                                                            The thread uses a 'move closure', which takes ownership of all values from the outside scope that are used inside its body.

                                                                                            Note that a value needs to be marked as `Send` so that it can pass across threads, but most types are.

                                                                                            That's what makes it so powerful. You know that any value you have in any variable, is only in that variable and nowhere else in the "world". Sure that value can be `Copy` and automatically copied, but that's still a new value. Your value can be `Sync` which means that a reference of it is `Send`, but you're not splitting up the value, you're creating and tracking a new reference value that is then send across the thread boundary.

                                                                                            • mrkeen 6 hours ago

                                                                                              Right, I was thrown by the real world terms. The ownership system (and syntax) isn't doing anything about a blocked account, it's just preventing a use-after-move, which has nothing to do with this-thread-blocks, that-thread-transfers.

                                                                                              (This is also why I walk past Erlang. Such emphasis on share-nothing prevents an account from being shared by a customer and a bank manager.)

                                                                                              So, what is the actual solution for a thread asking if any other thread has blocked(acct1), so that transfer() can proceed or be rolled back?

                                                                                              • vlovich123 6 hours ago

                                                                                                Pinning objects to single threads and message passing and/or transactions systems. Transactions typically aren’t seen at the language level and are more common within database systems. Typically a thread-per-core design with objects pinned to cores randomly would scale the best even for transactions; you can develop hotspots but in practice you win a lot more by not having to synchronize unless you’re doing cross-object transactions.

                                                                                                I’m not aware of any language that provides these kinds of guarantees statically or what that would even look like. You’re asking for a lot vs where the cutting state of language design is.

                                                                                                • j-pb 5 hours ago

                                                                                                  Yeah, the idea in the above code is that you can't do anything with an account so long as it's placed in the "blocked" set, because that means that it's effectively taken out of circulation for other operations.

                                                                                                  The way I solve situations like this is, by making sure that all accounts are moved to the same thread for a short period of time, which then handles the transaction. Which I find a lot easier to think about. It's essentially sharding by account and flexibly moving the shards into the same single writer context.

                                                                                                  But if you want to do things the "old fashioned way" Rust provides plenty of classical synchronisation primitives like `Mutex` or `RwLock`.

                                                                                                  You can also combine both approaches, e.g. have a single Mutex<AccountPool> that you can check out accounts from.

                                                                                              • vlovich123 8 hours ago

                                                                                                > This can only be determined syntactically if you and the compiler agree to stay on a single thread, right?

                                                                                                No. This will fail because in block the ownership of bad_account is lost. You’d need block(&bad_account) or block(&mut bad_account) to pass a reference while letting the transfer succeed. The only way the transfer would succeed silently like that is if Account implemented Copy (the compiler would automatically inject block(bad_account.clone())). This has nothing to do with threading (which Rust would still have protection mechanisms for). It would work identically if the block and transfer were member methods (i.e. bad_account.block(); good_account.transfer_from(bad_account)) since even member methods can be declared to be requiring ownership.

                                                                                                • mrkeen 6 hours ago

                                                                                                  > This has nothing to do with threading

                                                                                                  Right, but all the promises of the type system have to lead to a first-class multi-threading experience at some point.

                                                                                                  The safest concurrent program isn't a single-threaded program.

                                                                                                  • vlovich123 6 hours ago

                                                                                                    It doesn’t automatically solve every possible problem for you just like something like Lean doesn’t prevent you from writing buggy proofs. I’d argue it does lead to a first-class multi-threading experience within the systems programming domain it’s targeting. Even outside systems programming it can be quite surprisingly good and most incorrect code fails to compile to begin with. Is there a language that in your opinion offers a better multi-threading experience?

                                                                                              • Guthur 10 hours ago

                                                                                                Sorry it's probably just me but there is nothing syntactically obvious about ownership to me in the example you gave other than the names you gave things.

                                                                                                • j-pb 8 hours ago

                                                                                                  It all boils down to forbidding this:

                                                                                                    let x = 5
                                                                                                    do_something_with(x)
                                                                                                    do_something_with(x) // <- boom compiler error
                                                                                                  
                                                                                                  you simply cannot use the same variable twice, because you lost ownership of the value it stores on the first call.

                                                                                                  But that's a bit cumbersome, so people add an operator that allows you to derive a new value from a variable without taking the old one. Let's call that one `&`. It takes a variable that would normally only have been allowed to be used once (because using it removes the value from it), and returns a "thing" that borrows the contents of the variable out, and behaves a lot like the original thing, but not quite.

                                                                                                    let x = 5
                                                                                                    do_something_with(&x)
                                                                                                    do_something_with(&x)
                                                                                                  
                                                                                                  And because those derived things can be tracked and only used once just like the original object we can make sure that no two things can derive such borrowed things at the same time in a way that is mutating the original thing. Let's call the operator that only allows one mutable borrow to exist at a time `&mut`.

                                                                                                  The ability to track that only one of these can exist at any given moment for any value is based not on some special properties of "borrows" or "references", but because of the ability to uniquely track ANY value.