[go: up one dir, main page]

Threads for robinheghan

    1. 5

      This is quite puzzling to me, as i also come from Kagi and recently switched to Uruky. I don't really have the same experience of the search lacking. I usually find what i am looking for even if it took some getting used to. It isn't the "old google" that Kagi is so i had to adapt a bit how i search

        1. ~

          As an example i had a habit of putting a specifying keyword at the end of the search if the original query did not return what i wanted. I find that doing this basically produces the same original search. So i had to switch to adding the keyword to the beginning of the query. Doing this produces at least new results. I would not say i am a power user though, and cannot really explain what i ended up changing too much as it was not really a conscious decision rather than having in the back of my head: "this is not Kagi / old google, don't treat it as such". To me, the idea of my search being even more private than Kagi and in Europe is worth the change in search habits

          1. ~

            I'm running into the same thing. The importance of a word seems to degrade with position in Kagi much more than I've seen anywhere else

            1. ~

              This is true of Google too now. My friend and I used to (back when I still used Google) send each other screenshots of Google search results totally disregarding search terms. You could force things with a plus, then you had to quote words to mean “no really,” then nothing was enough.

      1. 1

        I'm a bit of a database noob — took a course on SQL and worked with simple databases but nothing fancy. I don't understand the point of this article; it seems to carefully explain everything except the main point, which it just glances over: where does the performance benefit of the structured primary key come from?

        The only thing I can think of, here, is that the select filters on customer_id, and since this is now the first part of the primary key of order_lines, no table scan of order_lines is necessary any more as there is an index we can use. This significantly reduces the number of redundant rows accessed and is thus good for performance. But then, the same performance benefit could have been got by adding an index on order_lines (or, indeed, on order) on (customer_id) or possibly more columns. So, it seems that the article assumes no additional indexes, which sounds odd to me.

        But then the article goes:

        With a structured primary key, the orders table does not need the id column at all. This often spares an index too.

        So apparently we do implicitly assume some relevant indexes were created! But then that invalidates my understanding of the performance benefit.

        Please help me: why is the query faster in the updated schema?

        (Side note: because of the fetch first 1 row only, timings depend hugely on precisely where the first eligible row resides in the table, so the benchmark is suspect in my mind just for that!)

        1. 2

          So apparently we do implicitly assume some relevant indexes were created! But then that invalidates my understanding of the performance benefit.

          If you create a surrogate key, say an autogenerated numeric id column, to use as the primary key, then in many DB engines there is an implicit index on the id column. But you also need to create an explicit index on any column(s) you'll commonly use in a WHERE clause. Such as (customer_id, placed).

          If the primary key is (customer_id, placed), as in the example, then you get an implicit index on that and don't have to create a separate explicit index for it.

          There's one more important concept, which is when the index includes all the columns (from that table) that you're SELECTing, so the query can be served entirely out of the index.

          To see how this works, suppose we take the orders table from the article, and we want to run a query that returns a list of all orders placed by a particular customer. And for ease of explanation, let's say this is Postgres, because I know it does things this way (several other major DB engines do too).

          For our first version suppose we have the surrogate numeric id primary key, which has an implicit index, but no other indexes. We run the query:

          SELECT id, customer_id, placed
          FROM orders
          WHERE customer_id = 123;
          

          This is going to be slow, because there's no relevant index the database can use. So let's add an index on customer_id, because we probably use that a lot for filtering, and re-run the query. Now it'll be faster because the set of matching rows can be determined by scanning the index. And it can get the value for the customer_id for those rows from the index. But it still has to go to the table's storage (the table's "heap", which is not the same as "the heap" in programming languages which make you think about stack versus heap) to get the id and placed columns' values. And that can still be an expensive operation: it's likely to be random-access I/O since the set of columns we want are probably not stored adjacent to each other in the table's heap.

          But now switch to the (customer_id, placed) primary key, with an index on those two columns, and the query becomes:

          SELECT customer_id, placed
          FROM orders
          WHERE customer_id = 123;
          

          We still are able to get all the filtering from an index scan because, once again, all the columns in the WHERE clause are in the index. But we also can get the full results of the query from the index, too, without having to go do random-access I/O to the table heap, because all the columns in the SELECT clause are also in the index. This is the fastest option of all.

          Some sources will call this "index-only scan" (which is how I learned it) because the query only ever has to scan the index, and nothing else. Other sources call it a "covering index" because the index "covers" all the columns needed by both the WHERE and the SELECT.

          1. 1

            But then, the same performance benefit could have been got by adding an index on order_lines (or, indeed, on order) on (customer_id) or possibly more columns.

            Just because you have indices on two columns, that doesn't mean you can combine the indices in a single query.

            The details depends a bit on the database. In some databases, the primary key decides the way rows are laid out on disk, while other indices are specialized tables that map the indexed column to a primary key, which is then used for lookup.

            Since indices are tables off their own, you don't necessarily get any benefits from looking up an extra index.

            Now, if you have a structured primary key, and you have a query that matches both part of the key, then finding the first part of the key will reduce the amount of rows required to scan through for the second part.

            I believe PostgreSQL will, in most cases, try to pick one single index for it's queries.

            I hope I was able to explain that in a good way 😅

            1. 1

              Thank you! Your reply makes perfect sense by itself, but unfortunately I don't see how it applies to the post. :P

              I get that indexes cannot necessarily be combined; I think of a query plan as a little program that often looks like a couple of nested "for loops". For the original query, assuming order_lines has an index on customer_id, we get: lookup customer_id in index on order_lines, scan in result set to find product_id, read order_id field, then primary key lookup in orders by ol.order_id. (If that PK lookup fails, continue scanning.) This doesn't feel bad to me. If you had a (customer_id, product_id) index on order_lines you could even shortcut the scan, but that doesn't feel worthwhile assuming orders are not gigantic.

              For the revised (schema and) query, we get: lookup customer_id in prefix of PK index on order_lines, scan in result set to find product_id, read fields, then do PK lookup in orders. (Again, if PK lookup in orders fails, continue scanning.) This is identical to the previous case, just the orders PK is a little larger. Alternatively you could start in orders and lookup customer_id in its PK index, scan over results, lookup the corresponding order_lines entry each time, and check product_id; but this should be slower (more index lookups).

              I don't see any index incompatibilities or indeed any relevant differences at all between the two cases. I'm surely missing something though!

              The only thing the article gives as an explanation is that a structured PK maintains "cohesion among all rows that belong to the same customer." If you bring in the customers table too, I can see how this might increase parallelism in query execution, but not a 10-fold speedup; and the query here doesn't even use 'customers' at all!

              1. 1

                It’s faster because everything you need is now in the index, so you don’t need to look it up in orders. Also order_lines will now be sorted by order_placed per customer, so it saves the sort step too.

                The query planner is probably smart enough to realize that it doesn’t have to look up anything in orders, even when the query specifies a join.

          2. 3

            Radicle does still have some issues with regards to performance.

            they aren't kidding, https://radicle.network/nodes/rad.hardenedbsd.org just had the spinner for me. That makes me suspect that this system has the same problem as bittorrent/ipfs in that my browser needs to contact the DHT, wait for some volunteer to ack the DHT request to find actual peers, and only then would any content show up

            1. 4

              Our rad.hardenedbsd.org is on a pretty slow server. Your timeouts are likely due to our less-than-shoestring budget servers.

              1. 2

                Our rad.hardenedbsd.org is on a pretty slow server.

                Why not use a more lightweight forge than Radicle in that case?

                (You can even host your repos on Codeberg, which is a completely free and open forge that is maintained by a non-profit, assuming you don't care about self-hosting.)

                1. 6

                  We self-host everything in support of our human rights infrastructure work. We provide Tor Onion Service endpoints for all public services. We tried Gitea years ago and it performed absolutely horribly to the point of being 100% useless.

                  One week in, Radicle seems to be performing very well with regards to our infrastructure. It's just that radicle-httpd acts a bit slow. radicle-httpd isn't absolutely required for a complete Radicle experience--it's mainly only there to enable us to download ports distfiles from Radicle.

                  1. 2

                    Thanks for sharing! Good to know. If anything, I'd have expected Gitea to be much faster than Radicle but I guess that's why you always measure. :)

              2. 1

                You’re wrong.

                Your browser speaks with a http server which should have the git repos available.

              3. 1

                I wondered when they will be closing the free tier. It seems strange to do so with the new release.

                1. 1

                  They still have a free tier. Devstral-2 is no longer free now that it’s out of preview, but you can use vibe with any model you want.

                2. 3

                  I’m currently using opencode with devstral-2, and claude code side-by-side to compare them.

                  Claude code is consistently better, and opencode seems to eat tokens for breakfast, but I still find opencode good enough that I try it out first.

                  I’ve also had more luck in trying to use the LLMs to search for certain things in the code.

                  Just today I needed to trigger a exception on our server to check if stacktraces was properly sent to our observability platform, and opencode+devstral gave me a curl instruction to do just that.

                  Otherwise my experience aligns with the author. There is good stuff here, but it’s not… like…life changing or anything.

                  1. 3

                    It's important not to conflate the harness (claude vs opencode) with the model being used. The latter is where the differentiation is.

                    1. 1

                      Is that the only differantiation, though?

                      I hear Claude Sonnet did worse in opencode than in the official CLI.

                      On reddit I see people mention that the same model performs differently across roo, cline, opencode etc.

                      1. 1

                        In my experience, the difference in models dwarfs any other.

                  2. 12

                    WASM sounds like what you're talking about? The JVM and CLR are not compiling Java or C#, they're compiling an already mostly compiled bytecode. Only fully dynamic, raw source based languages have JITs that are going from source to execution - the nature of these languages means that the initial source->bytecode conversion is much much faster that is possible for languages like rust or c++.

                    WASM really is what you're looking for: it is a low level bytecode format that languages like C, C++, rust, etc can target - it's literally part of the llvm toolchain.

                    JIT compiling a language like rust or C++ from source means spending as much time - and memory - as a debug build of that code, in the best case, but given you're trying to use a JIT for performance you're look at at least as much time as a release build, and the even greater amounts of memory that requires.

                    e.g you don't JIT Rust, C++, ... you would JIT a lower level bytecode you have compiled the higher level language to.

                    In addition to WASM I believe there are a few other JITs that try to do similar things, but they lack the engineering backing of the big JS engines, which are where the fastest WASM JITs are.

                    [edit: I forgot the bigger thing - JITs create a very large attack surface, the reason JITs are safe in non-C,C++,rust languages is that they are enforcing runtime safety semantics which prevent the JITs themselves from being attacked - the languages you're referencing have no memory safety in the sense that matters here, even rust allows unsafe operations, those operations undermine the security of the JIT - WASM supports these higher level languages by ensuring that they are still fundamentally running in a VM, just without a builtin object model]

                    1. 5

                      JVM bytecode isn't "mostly compiled", my understanding is that it's more or less just a binary representation of Java. It's why Java decompilers are so exceptionally good. So by going JVM bytecode -> native instead of source -> native, the JVM is really just saving on some parsing work.

                      I believe C# is the same.

                      1. 16

                        This is actually a change as Java systems have evolved. Earlier versions of javac did quite a lot of optimisation on the bytecode. Then newer versions of the JRE ended up spending a lot of time undoing them to get the code back into an easier shape to optimise. Now, javac does only a handful of trivial optimisations (e.g. constant folding). This was a problem for earlier versions of Android, which did a fairly straightforward translation from Java (stack-based) bytecode to Dalvik (register-based) bytecode and then interpreted them. Android went through a bunch of AoT (install-time) and JIT compilers and now I think uses a caching JIT, so now prefers the not-very-optimised javac output.

                        The early .NET marketing played up a lot the idea of doing install-time compilation. This is quite a nice idea because you can have binaries that are tailored to your specific CPU model (not just a single distribution format for Alpha and x86 Windows, but also one where a Pentium II and a K6 could have different tuning in the binary). Unfortunately, it turned out to be much too slow. It's not uncommon for a complex desktop app to need at least tens of minute of CPU time to compile, and hours are not unusual. Spending this time on every client is not great, and starting with an interpreter then moving hot paths to the JIT gives a more immediate experience.

                        For iOS, Apple moved to doing this in the App Store. You upload LLVM IR and the first time anyone downloads it for a specific model of iOS device their system will produce an optimised version for that specific device class. It then caches it, so you don't do the duplicated work that .NET originally proposed. I don't know if this uses profiling information. We explored the possibility of building something for FreeBSD where we'd do some very lightweight statistical profiling in the kernel and uploading it to the package build machines so that they could aggregate it and do profile-guided rebuilds. We never shipped it because we couldn't come up with a solution to avoid people uploading malicious profiling traces to make everyone slow. Apple has a solution to that because their secure boot chain is signed by them and gives remote attestation of devices.

                        For a while, about 20 years ago, I played with a variant I called 'just too late compilation', where an interactive system (one where you modified the code as the program ran) would do interpreting and some mild JITing, but would then do AoT-style compilation when the program exited so the next time you ran it you'd get a fast and optimised version of the end state of your interactive development model (you could then keep modifying it, and the bits you changed would be a bit less fast).

                        1. 11

                          The early .NET marketing played up a lot the idea of doing install-time compilation. This is quite a nice idea because you can have binaries that are tailored to your specific CPU model (not just a single distribution format for Alpha and x86 Windows, but also one where a Pentium II and a K6 could have different tuning in the binary). Unfortunately, it turned out to be much too slow. It's not uncommon for a complex desktop app to need at least tens of minute of CPU time to compile, and hours are not unusual. Spending this time on every client is not great, and starting with an interpreter then moving hot paths to the JIT gives a more immediate experience.

                          This is not at all true. In fact, it's closer to the opposite of what you said.

                          First, the product you're referring to here is the .NET desktop framework, i.e. the version of .NET that ships in Windows. Since the beginning of .NET it was widely recognized that startup was a big problem for JITed languages -- the bytecode is not directly executable so you have to spend time to JIT it. There are two main ways to address this problem:

                          1. Start with an interpreter, then switch to JIT when a certain condition is met.
                          2. Precompilation

                          .NET has always relied heavily on (2). In .NET Framework this process is called NGEN and it has been in Windows since .NET 1.0. It runs on install of the .NET Framework and it precompiles the entire framework. As to why it runs on install -- it's mostly not about producing highly optimized code for different architectures. .NET interoperates with a large number of different native APIs and platform features in Windows and this interaction can have subtle code changes for the target platform. Combined with the huge number of Windows machines and huge number of Windows configurations, that means that the best strategy to compile code for every possible client configuration was to compile the code on the machine at install time.

                          This process can take a while -- hours or maybe a day -- but this is mostly because the NGEN process runs in the background at low priority, it has to compile the entire .NET Framework, and the whole thing isn't well optimized for finishing as quickly as possible. However, this wasn't about apps. You certainly could queue your app for NGEN but this is a relatively rare thing and would produce significant results only for very large apps, since the startup path of most applications is heavily dominated by code in the framework, not the application. For various reasons it also requires admin permissions, so there are also plenty of reasons not to do it.

                          Now, to switch over to modern cross-platform .NET (.NET 5+), we have a replacement technology that we call "ReadyToRun" or "crossgen". In contrast to NGEN, it's specifically designed to remove those platform dependencies and intermodule dependencies that required code to be compiled on the target machine. Nowadays we ship with the framework precompiled for all the targets, so the benefit is provided for free to every .NET application. It's still an essential part of the startup story -- .NET is much slower to start without precompilation.

                          Finally, you mention an interpretation. .NET has never had an interpreter. Interpretation and precompilation serve many of the same purposes and we went the precompilation route, so there wasn't a lot of reason to build it. This is in contrast to Java, which has a smaller precompilation story and a much larger interpreter story. The other reason for this, aside from path dependence, is that it's much harder to build a good interpreter for .NET IL vs Java bytecode. Reified generics, value types, and specialization mean that the .NET IL is much more complicated, so building a good interpreter requires a lot more work. Moreover, offering generic specialization as a feature causes .NET users to write code that depend on it for performance and interpreting that code can sometimes mean slowdowns so large that the entire performance class of the application changes.

                          .NET Framework was in fact very simple. It had no tiering (only one JIT optimization mode) and precompilation. There were no optimizations for "on-stack replacement" where a particular method could be recompiled on the fly into a more optimized form.

                          This is no longer true for modern .NET -- we have multiple JIT tiers and regularly recompile and replace code -- but precompilation is still a critical part of our startup performance story.

                          1. 2

                            Thanks, that's very interesting!

                          2. 3

                            For iOS, Apple moved to doing this in the App Store. You upload LLVM IR and the first time anyone downloads it for a specific model of iOS device their system will produce an optimised version for that specific device class. It then caches it

                            LLVM bitcode is deprecated on Apple platforms and is not used anymore since a few years now.

                            https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes

                            Starting with Xcode 14, bitcode is no longer required for watchOS and tvOS applications, and the App Store no longer accepts bitcode submissions from Xcode 14. Xcode no longer builds bitcode by default and generates a warning message if a project explicitly enables bitcode: “Building with bitcode is deprecated. Please update your project and/or target settings to disable bitcode.” The capability to build with bitcode will be removed in a future Xcode release. IPAs that contain bitcode will have the bitcode stripped before being submitted to the App Store. Debug symbols can only be downloaded from App Store Connect / TestFlight for existing bitcode submissions and are no longer available for submissions made with Xcode 14. (86118779)

                            1. 1

                              Re install-time compilation, isn't this exactly what Android currently does to Dalvik bytecode? IIUC when you install a program it interprets it or maybe does a quick-and-dirty compilation so you can use it immediately, while in the background it compiles it to target-specific native code. Do you know anything about why this seems to work for Android and not for 2000's-era Windows?

                              (After doing some wikipedia-crawling it appears that the Android Runtime is the component responsible for this now, replacing the Dalvik VM, and the pipeline is more complicated than I thought. It doesn't necessarily AoT the entire program to native code, just portions of it, and it will also try to JIT some of the code as the program is running. I suspect I could happily spend years digging into the details of how it works.)

                              1. 3

                                Re install-time compilation, isn't this exactly what Android currently does to Dalvik bytecode?

                                I am not completely up to date here, butI believe they moved away from this. This is what the 'optimising system performance' thing was. I believe they now do JIT compilation and cache bits of JIT state so that the second time you run a program the hot bits are already compiled (and the cold bits can be interpreted or JIT'd if desirable).

                                Do you know anything about why this seems to work for Android and not for 2000's-era Windows?

                                The ratio between program size and CPU power has changed. A typical Android phone has at least a quad-core CPU that runs at around 2 GHz and it at least as fast as a Pentium III, clock-for-clock, probably faster. My computer in 2000 was a single-core 550 MHz Pentium III. Desktop apps remain a lot more complex than most mobile ones. And Android apps tend to offload performance-critical parts (and parts that need to be shared between Android and iOS or Android and desktop versions) to native code. Again, it's been ages since I looked, but back then all of the top 100 most-popular Android apps included NDK code. So if your performance-critical parts are shipped as native binaries, the install-time compiler isn't stressed as much. The main reason for doing it on Android was not performance relative to a JIT, it was doing all of the JIT-like work at a time when you were on battery (they'd redo it overnight periodically while the phone was charging too).

                            2. 8

                              my understanding is that it's more or less just a binary representation of Java.

                              This isn't true. The JVM has a stack-based instruction set, just like WASM. Unlike WASM, though, JVM bytecode doesn't require managing memory (as you have a GC) and it understands the concept of objects and inheritance, which simplifies that aspect of compilation.

                              Here's an example of Java Bytecode:

                              Compiled from "SimpleAlgorithm.java"
                              public final class SimpleAlgorithm {
                                public SimpleAlgorithm();
                                  Code:
                                     0: aload_0
                                     1: invokespecial #1                  // Method java/lang/Object."<init>":()V
                                     4: return
                              
                                public static void main(java.lang.String[]);
                                  Code:
                                     0: aload_0
                                     1: arraylength
                                     2: ifle          12
                                     5: aload_0
                                     6: iconst_0
                                     7: aaload
                                     8: astore_1
                                     9: goto          17
                                    12: iconst_1
                                    13: invokestatic  #2                  // Method java/lang/System.exit:(I)V
                                    16: return
                                    17: aload_1
                                    18: invokestatic  #3                  // Method java/lang/Integer.parseInt:(Ljava/lang/String;)I
                                    21: istore_2
                                    22: new           #4                  // class DivisorPrinter
                                    25: dup
                                    26: iload_2
                                    27: invokespecial #5                  // Method DivisorPrinter."<init>":(I)V
                                    30: astore_3
                                    31: aload_3
                                    32: invokevirtual #6                  // Method DivisorPrinter.print:()V
                                    35: return
                              
                              1. 3

                                A simple compiler generating a stack-based bytecode at the level of objects and method calls means the output for most statements is pretty close to a postorder traversal of the syntax tree, which is easily rendered into concrete syntax. Then recognize some consistent patterns like the gotos generated by if statements, and hey! — a (primitive) decompiler!

                            3. 1

                              I haven't explored WASM much, but my understanding was that the JIT compilation is pretty rudimentary, right? That is, the WASM runtime assumes that the bytecode was already optimized by a full AOT compiler and it "just" needs to translate it to native code with local optimizations, without inlining, vectorization, devirtualization and similar larger-scale transformations.

                              1. 4

                                I guess the real question is what optimizations you think the JITs are doing that would meaningfully benefit lower level languages, enough to compensate for the extremely large costs of runtime codegen.

                                The entire model of WASM is designed around ahead of time compilation because its express purpose is safely running languages like C and C++ which have no significant runtime object model and the operations of these lower level languages are inherently unsafe. The major JITs do perform a lot of optimizations during lowering - bounds check reduction/removal, register allocation, load propagation, etc but the whole point is that it exists to support those lower level languages, while being safe, so the assumption is the AOT compiler has managed the performance aspects.

                                The way JITs for compiled languages are able to perform reasonably is by precompiling to a bytecode, the requirements for those bytecode languages is that they are verifiably safe, and that's no different than wasm. The difference is that wasm is designed for languages like c++ and rust, and so allows "arbitrary" pointers, but those pointers are entirely in the confines of the wasm vm.

                                If you wanted to make a jit for rust or c++, the first step would be making your ahead of time compiler compile those down to a compact bytecode, presumably cross platform, that includes enough information about the object model to allow you to execute the code safely - if you don't care about safety you could skimp on some of that, but to benefit from the kinds of optimizations things like the JVM do you'll need to carry in runtime information that rust and c++ generally don't include.

                                Realistically for languages like C, or C++, or to an even greater extreme rust, the optimizations a JIT can do that meaningfully help performance, that an AOT cannot do are fairly minimal - devirtualization is the biggest one, and that's only possible by generating code that is slower than it might otherwise need to be in order to re-virtualise later if needed. If you don't need to worry about dynamically loading libraries they may require revirtualisation the profile guided optimization gets you more or less everything the JVM or CLR can do, with higher performance everywhere else as well.

                                The optimizations in JS engines are simply irrelevant to C, C++, etc, and the optimizations performed by the JVM or CLR are mostly able to be performed ahead of time by compilers for those lower level languages because the optimizations being done in the JVM and CLR are the lowering from a safe IR to the inherently unsafe instructions in the ISA. For instance converting the array index ir instructions into pointer arithmetic.

                            4. 15

                              My summary by virtue of skimming the talk:

                              1. Anything can throw an exception. (Yes, it can but this can be solved through cultural norms such as in Go, without needing pure functional programming)
                              2. Trite example of URL comparison doing DNS lookup in Java. (It's not like Haskell and OCaml stdlibs are free of badly designed APIs)
                              3. Optionals (Zig has this too, doesn't have anything to do with pure functional programming.)
                              4. Equational reasoning
                              5. Bunch of vague graphs about cost of change/progress without any concrete examples/case studies
                              6. Unsubstantiated claim that AI tools benefit more from local reasoning.

                              I would love to see actual case studies grounded in people's real world experience.

                              Foundation DB and TigerBeetle are poster boy projects for Deterministic Simulation Testing. The CEO of TigerBeetle states that the static memory allocation strategy would've been much more time-consuming or impossible to use in another language (due to lack of pervasive support for custom allocators)

                              What is an equivalent project what would've been impossible without being written in a purely functional language? (Note: the question is about language not about style for a particular piece of code.)

                              1. 16

                                What is an equivalent project what would've been impossible without being written in a purely functional language?

                                Not a project as such, but Haskell’s support for software transactional memory can’t be translated faithfully to an impure language. Haskell’s STM only allows side-effects that can be committed or rolled back transactionally. It takes over the control flow around retries, which it can do because transactional code is pure apart from its effect on the STM.

                                1. 2

                                  Depends on the effect, no? Eg imagine that you're implementing an infrastructure-as-code tool like Terraform. And it has to execute a 'plan' to delete a cloud database and then some networking infrastructure for the database. But it has to do them in a single transaction. So if the second delete fails it has to roll back the database deletion. Is that doable? I don't think so...

                                  1. 2

                                    A program can’t use an STM TVar to run terraform or delete a database or launch missiles. STM is just a fancy multi-address CAS.

                                    1. 2

                                      Yeah, so ultimately STM is just a fancy way of controlling in-memory mutation. In other languages I can just use an in-memory SQLite database to get the same atomic effects.

                                      1. 5

                                        You’re missing the point that STM prevents the programmer from mixing non-transactional effects into a transaction, and that STM takes care of conflicts and retries in a manner that depends on the code being pure. There was a lot of STM research in other languages contemporaneous with Haskell’s STM, but none of them were so elegant in the way they handle conflicts and retries. Which is what I meant by “can’t be translated faithfully”.

                                  2. 2

                                    That's a good example of a feature hard to retrofit elsewhere.

                                    Do you know of any non-trivial production codebases relying on STM in a load-bearing way?

                                    1. 5

                                      Most production Haskell code will be relying on STM for shared mutability across threads, it's standard default practice. It's also taught and re-emphasized by the most important educator in Haskell.

                                      To say that it's load-bearing is probably an understatement. I don't think it's a stretch to say that only toy code in Haskell would not be using STM in some form at least for a long-running, networked service. With that said there are a lot of uneducated Haskell users out there, so the floor here is very low.

                                      With all of this said I think STM is just about the only thing that justifies pure functional programming and I think functional programming as a whole (beyond the very, very basics of "make functions pure if you can and it doesn't suck") is a massively overrated paradigm that people waste time on for basically no net gain. I've done FP for 10+ years and in hindsight it's been one of the most useless aspects of programming that I can think of.

                                  3. 16

                                    Trite example of URL comparison doing DNS lookup in Java. (It's not like Haskell and OCaml stdlibs are free of badly designed APIs)

                                    My first bug coming out of university was doing a URL comparison in Java.

                                    Haskell's base library is similarly ancient and has lots of mistakes BUT it definitely does not (because it can not) make unexpected network calls for things like equality comparison. It's totally missing the point to say this is just about badly designed standard libraries.

                                    1. 8

                                      Trite example of URL comparison doing DNS lookup in Java. (It's not like Haskell and OCaml stdlibs are free of badly designed APIs)

                                      I was actually unsure whether to include this or not, as it feels like beating a dead horse by now. Still, I think it shows the utiliy of effect systems / being able to sight-read whether a function is impure or not.

                                      Optionals (Zig has this too, doesn't have anything to do with pure functional programming.)

                                      This is a good point, and something I have struggled with myself—there is a certain "aesthetic" which really isn't bound to FP; Rust shares more of this aesthetic with ML than with Clojure, in spite of Clojure being "functional programming" whereas Rust really isn't. I think we need a better name for this.

                                      Bunch of vague graphs about cost of change/progress without any concrete examples/case studies

                                      Yeah, this is very much based on anecdata and vibes. However, I think it's not controversial to say that in the presence of unmanaged effects, the complexity of understanding any given function is O(n). We stopped using global variables for a reason, and I think that same reason also applies to unmanaged effects. I do wish that there were more studies on this, but they seem scarce, and those that exist are often plagued by methodological issues (see https://danluu.com/empirical-pl/).

                                      Unsubstantiated claim that AI tools benefit more from local reasoning.

                                      I think it's quite uncontroversial to say that if the type system is powerful enough you only need to read the function definition to understand what a function does? Ergo, in order for an LLM to update a function it only needs to load the definition into its context.

                                      1. 1

                                        However, I think it's not controversial to say that in the presence of unmanaged effects, the complexity of understanding any given function is O(n)

                                        What do you mean by this? Are you saying that when someone tries to understand a function in an imperative language codebase, they have to go look at the full call graph but that's not the case for purely functional languages?

                                        I think it's quite uncontroversial to say that if the type system is powerful enough you only need to read the function definition to understand what a function does? Ergo, in order for an LLM to update a function it only needs to load the definition into its context.

                                        There's a big difference between this statement and the one you're quoting "Unsubstantiated claim that AI tools benefit more from local reasoning."

                                        In particular:

                                        1. LLMs do not necessarily do the minimal thing they theoretically need to do.
                                        2. In practice, there are multiple orders of magnitude more code for more mainstream languages.

                                        Your comparison is essentially stating "everything else being equal, local reasoning is better" and sure, that may be the case, but in practice, "everything else being equal" does not hold.

                                        So the claim as to whether LLM performance is better on purely functional languages needs to be tested against reality. As one data point, I've worked at one company building AI dev tools, and we've seen much better performance on mainstream and simpler languages like TypeScript, Go and Python compared to more niche languages.

                                        1. 5

                                          What do you mean by this? Are you saying that when someone tries to understand a function in an imperative language codebase, they have to go look at the full call graph but that's not the case for purely functional languages?

                                          In the limit—yes! This isn’t always true in practice of course, but in general the more properties a type system can capture about a function, the less you need to know about its implementation. A lot of the time you can live with this uncertainty, but consider eg a function that allocates resources that needs to be cleaned up; in that case you either need to be sure that none of its callees can throw exceptions, or slap on a wildcard “try … catch … finally” just to be sure. This is not a purely academic exercise—you run into things like this all the time.

                                          As an example of the positive case, take the polymorphic identity function a → a; in a pure functional language, there can only be meaningful implementation of this function! (See https://people.mpi-sws.org/~dreyer/tor/papers/wadler.pdf) When I’m programming I rarely look at docs or implementations of functions, I primarily look at the type signatures.

                                          Your comparison is essentially stating "everything else being equal, local reasoning is better" and sure, that may be the case, but in practice, "everything else being equal" does not hold.

                                          Fair enough, in November, the year of our Lord 2025, the current models / tools cannot do this out of the box. However, it would be interesting to see if you could eg tweak Claude to be more selective when loading Haskell code, and to use a local Hoogle to find functions with the appropriate signatures.

                                      2. 8

                                        What is an equivalent project what would've been impossible without being written in a purely functional language? (Note: the question is about language not about style for a particular piece of code.)

                                        "Impossible" is a pretty high bar, since most programming styles can be can be written in most programming languages, so most differences for this-or-that project end up being second-order (ergonomics, library compatibility, etc.).

                                        The only place I can think of where style versus language truly matters is the expressiveness/reasoning tradeoff, i.e. that a language which cannot express X is guaranteed not to do X. The most common example is memory-safety, e.g. Python can't do pointer arithmetic, so Python code shouldn't cause segfaults. We can frame FP styles like this too, e.g. pure FP can't mutate values or trigger side-effects, so code in a pure FP language shouldn't mutate any values or trigger any side-effects.

                                        We don't need such guarantees when writing code for ourselves, but it's important when dealing with user-supplied code. Examples which come to mind are search queries, regular expressions, arithmetic calculations, etc. but they feel more like limited DSLs rather than "full blown" languages. Maybe Nixpkgs?

                                        1. 13

                                          We don't need such guarantees when writing code for ourselves, but it's important when dealing with user-supplied code.

                                          In my own experience, knowing what code cannot do without having to read the implementation is a very nice property to have, especially when dealing with third party code.

                                          1. 10

                                            That's the real value here. This should be what's mentioned when you campaign for any kind of strictness in software period.

                                            Turing complete is Turing complete. You're not going convince someone that functional programs are by nature superior just because you said so.

                                            "I don't need functional programming, Rust/Java/TS/C++ already have this..."

                                            You also don't need static type checking. A talented python or js developer can create just as robust software as a Rust developer. But we usually work in teams, and it's very hard to maintain this level of consistency and trust. This is the exact same argument in favor of strictness when it comes to fp. For every guarantee, we take a bit of mental load off and can tackle more and more complex problems with more confidence.

                                            By the way, they do state all of this in the video, maybe the examples weren't the best, but I still think it's a solid argument.

                                            1. 9

                                              I am convinced that if all software developers were required to have malpractice insurance (like doctors) then strict programming languages would be much more common in industry.

                                            2. 2

                                              Sure, but that's partially subjective, and can be achieved/approximated in various ways (e.g. curating dependencies, preferring certain vendors/ecosystems, linting/tooling, etc.). In other words, software engineering.

                                              When it comes to "objectively impossible" things, I think it's like the Halting Problem: only applicable when our solution has to work for arbitrary programs (e.g. user-supplied, potentially adversarial).

                                              1. 2

                                                curating dependencies, preferring certain vendors/ecosystems, linting/tooling

                                                Curation is much more work than just looking at a type.

                                                Preferring certain vendors/ecosystem isn’t a guarantee.

                                                Linting/tooling can’t help you track side-effects when the language doesn’t.

                                                1. 2

                                                  I think it's a spectrum. For example, I've worked with Scala which doesn't make any such guarantees; but I can try sticking to pure FP in my own code, and if I'm going to add a dependency then I can be pretty confident that e.g. a "cats" package (Scala-specific, FP-focused libraries; like lenses, monads, etc.) will maintain that purity more than some Java equivalent (which Scala will also accept).

                                                  Even in Haskell, the prototypical example of language-enforced pure FP, there's a notion of "safe" packages; which are even more constrained (to prevent unsound things like unsafePerformIO and coercions which could undermine the type system). However, I've rarely seen people considering that flag when choosing dependencies; preferring fuzzier metrics instead, like download counts and update frequency.

                                                  1. 1

                                                    It might be worthwhile to add that I've spent almost as much of my professional career coding in Elm, as I've done in other technologies. Elm is more strict than Haskell (no unsafePerformIO), and even interop has to go through a verification layer (ports).

                                            3. 5

                                              I think nixpkgs/nix expressions are a very good answer to this question. Nixpkgs wouldn't be impossible in a non functional language, but it would be magnitudes harder and make little sense. This is why the "impossible" bar isn't relevant.

                                              If something would be significantly easier and more maintainable in a certain paradigm, you should probably use that paradigm.

                                            4. 6

                                              A pure functional language is much easier to audit for supply-chain attacks since the attack area is vastly reduced. It's much harder to hide some code that steals your credit cards, because such a function would be clearly marked as a side-effect. And well if the dependency has no side-effects, it can't really do much so you don't need to audit it. Well, at least I can't really think of any exploits that scare me (maybe an infinite loop that causes the system to crash?).

                                              Also since functions are never redefined or removed, you can do more optimizations to reduce the asset size of the dependencies you pull in. This is quite a big win for web development.

                                              1. 5

                                                pure functional language is much easier to audit for supply-chain attacks since the attack area is vastly reduced.

                                                Are you basing this based on practical experience of auditing codebases or based on theory? Because Haskell code can definitely do this via unsafePerformIO or FFI. Cabal can also run code at build time.

                                                Also since functions are never redefined or removed, you can do more optimizations to reduce the asset size of the dependencies you pull in. This is quite a big win for web development

                                                In practice, if you look at bundle sizes, there are several JS/TS frameworks with sizes competitive to that of Elm. Additionally, in Wasm usage, the size of the language runtime that needs to be shipped adds substantially to the bundle size, so much so that uptake of Wasm outside of systems languages like C++ and Rust seems to be relatively low.

                                                1. 4

                                                  I'm thinking specifically of elm, which is stricter than Haskell with purity. Side effects can only really happen in functions marked as Cmd or HTML. It is true in Haskell its not as easy due to unsafePerformIO.

                                            5. 1

                                              Statewise equivalence is no substitute for a meaningful equals method designed by a class author. In some cases, two instances of a value class with different states should still be considered equal. So the best practice, as usual, is to avoid the == operator and prefer equals for comparisons.

                                              Slightly confused by this directive, since it seems like the increased ergonomics of the == operator is partly the point? If instead the reasoning was that "we shouldn't force users to constantly think about what's a Value type and what's not when comparing things", then I'd understand.

                                              1. 4

                                                So there are a few different trade offs here. == has traditionally been thought of as extremely cheap by Java programmers because it has been simply a pointer comparison, but the equality of value objects may be more complex than that.

                                                There are always some potential issues with things like doubles (how do treat NaN?) and a few other sharp edges.

                                                The change in == semantics isn’t really the point of value objects, it’s more an unavoidable consequence of introducing them.

                                                1. 2

                                                  Statewise equivalence is no substitute for a meaningful equals method designed by a class author

                                                  Completely off topic but as a former Java dev I used to think this was resonable and correct until I started using UATs in plpgsql in which the equals operator works naturally - and now, as a Go dev, every time I need to fieldwise compare a struct I shake my fist at the sky gods.

                                                  So the ability to statewise compare Java value objects is fantastic news IMO. Even if I don’t use Java any more.

                                                  1. 1

                                                    I believe changes in the behaviour of == is a byproduct of loosing identity semantics, but not part of the original motivation for the feature itself.

                                                  2. 2

                                                    Nice, Java starting to catch up with Scala's features from 2015. Let's hope they learn from its mistakes too.

                                                    1. 18

                                                      With all due respect, that's a blatant misunderstanding of this feature. Scala and Kotlin had some support for masking a primitive type into an object, but they fundamentally can only support this in case of classes with a single primitive field.

                                                      E.g. you can make a new unsigned int "class" which you can call methods on and this would just compile down to unsignedFunction(int value), but you can't make a Complex type with two long fields that wouldn't be boxed - and this is the very point of this JEP.

                                                      With this feature you can make a value class that can be reliably stack-allocated everywhere, or stored inline, as its semantics and JVM changes allow for it.

                                                      1. 4

                                                        Doesn’t Scala compile to Java bytecode? How could Scala have this feature?

                                                        1. 1

                                                          Yes. Just like all the other JVM languages, it has/had those concepts on the native language level (e.g. type-level) and then emulated them on the bytecode level.

                                                          1. 9

                                                            Seems like you’re talking about value classes which are like Kotlin’s inline classes. Both of these have pretty strict limitations in comparison (e.g. only one field allowed) and thus are basically just used for the newtype pattern. Value classes go much further in terms of perf benefits and ergonomics.

                                                            Also the reason this took so long was to find the right underlying language/bytecode/JVM model to ship this feature. Compared to previous iterations, Scala and Kotlin should be able to adopt this feature very easily.

                                                          2. 1

                                                            An analogy might better illustrate why Scala could have a feature not seen in the Java language: my Rust program and C program both compile to machine code. That doesn't mean they share all features.

                                                            1. 4

                                                              Value classes, as described in the linked article, strips a class of identity semantics and lets the VM do things like heap flattening. This requires support in the VM itself, and cannot be entirely emulated at the language level.

                                                              1. 1

                                                                My understanding is the superficial features could be emulated, but the optimizations like those you point out would require JVM support. That makes sense and is probably the source of my (and other's) confusion!

                                                                ...value objects are instances of value classes, which have only final fields and lack object identity...

                                                                I guess in this case, you'd have to have some alternative concept of object identity in the hosted language if you were to do this without the VM support...but you'd get no efficiency gains at runtime.

                                                                1. 2

                                                                  Exactly. The same is true for functions etc. You could pass them around and create them on the fly in Scala for a long time, but only when JVM support was added, they could make them very performant.

                                                                  1. 2

                                                                    My understanding is the superficial features could be emulated [...]

                                                                    I am not sure what you're referring to as superficial features here? Kotlin, Scala and Java already offer immutable objects (Data Classes, Case Classes(?) and Records, respectively).

                                                                    JEP 401 is about removing identity-semantics. Changes to things like == are incidental, as they're currently defined to work by identity.

                                                                    I guess in this case, you'd have to have some alternative concept of object identity in the hosted language if you were to do this without the VM support...

                                                                    I don't think it's that easy. Generics in Java work by type erasure, so you'll quickly lose track of whether an object is value-based or identity-based.

                                                                    Also, during interop this becomes a leaky abstraction, as there is no way to prevent arbitrary Java code from looking at the identity of an object.

                                                                    I don't see how you can emulate a feature like JEP 401 without VM support, or why you would want to. Which is probably why no JVM language does it. Project Valhalla (which JEP 401 stems from) is about a decade old at this point.

                                                          3. 9

                                                            I’ve been learning swift recently and the “compiler is unable to type check this expression in reasonable time” really makes an otherwise great language frustrating to program in. It really feels like one of those “computer says no” kind of errors, so I’m really glad this is being handled.

                                                            I wonder how other languages handle this? I know languages like Haskell also have insane type inference, but I don’t know of any that have the same hard limit as swift does. Do they just accept the time complexity, or does the language allow for some better optimizations?

                                                            1. 9

                                                              I can't remember the name of the actual type inference algorithm used by swift, but my recollection is that its approach to bidirectional inference is much more, we'll say, powerful. It does allow some things that most other languages cannot do.

                                                              The Damas-Hindley-Milner type system (sometimes Hindley-Milner, I'm not sure to this day why Damas is included by some, and not by others, and honestly at this point I'm not really interested in finding out :D my education has had Damas so that's my default) is the basis of more or less all functional languages, and is linear time. But that is largely possible due a function named 'f' only has one declaration, and one type (that type might be generic, but under these type systems that is a single type)

                                                              Now consider an expression: f x (f y (f z 1))

                                                              In DHM, f has a type for all a->b->c, all of which are fixed - they may be explicit types or they might be type parameters, so in the above we know that the type of x must be a'0, the type of (f y (f z 1)) must be b'0. In f y (f z 1), y must be a'1, the type of (f z 1) must be b'2, but also in this case we know the type of c'1 must be the same as b'0, so we've unified those types, hurrah!. The same then happens in f z 1, z must be of type a'2, and 1 is Int, so b'2 is unified with Int, e.g. b'2 == Int, and similarly to earlier we know that the return type c'2 must be the same as b'1.

                                                              From that you can probably see the general gist of how this inference operates in linear time.

                                                              The first complexity swift has comes from the ability to have overloads of a function, so the type of f might be a->b->c, or c->b->a, or a->b->c->d, or even a->b->c, a->b->a, a->b->d. We can discard the function with a mismatching number of parameters[1], but we have multiple possible types to select from, so in f x (f y (f z 1)) (ignore the non-swift syntax) swift cannot immediately assign a type to x, it also cannot immediately assign a type to f y (f z 1), it cannot immediately assign a type to y, it cannot immediately assign a type to f z 1, or z. We'll leave 1 until later - for now just assume it's an Int. Swift's type system is much closer to a generalized constraint solver: instead of a fairly direct linear path for assigning types, it has to try to find the a combination of the correct types and the functions those that correspond to those types, ensuring that the return types of each function works in the bounds of the caller. For example you could have (excuse the syntax)

                                                              f(Int, Int) -> Int // f_0
                                                              f(String, Int) -> String // f_1
                                                              f(Float, String) -> String // f_2
                                                              f(Int, String) -> Int // f_3
                                                              

                                                              Now consider what happens in the above nested calls: f z 1 this could be either f_0 or f_1, now consider f y (f z 1), currently we have two options for f z 1, one returns an Int and one returns a string, but that means we could be calling f_1, f_2, or f_3, so the return type can still be String or Int. Which means f x (f y (f z 1)) can now be f_0, f_1, f_2, or f_3. If you wrote let v : String = f x (f y (f z 1)) (ignore the syntax nonsense I'm just trying to be consistent with the expression), then you've provided a concrete type so it can reduce the possible options of the outer expression to just f_1 or f_2.

                                                              Basically to get to a complete solution it has to determine the specific types of x, y, and z for a call to f, while at the same time the type of f determines the types of x, y, and z.

                                                              You can probably see how it's possible to construct expressions where this can start taking a, uh... long time.

                                                              Despite this complexity, most code decays to linear time, but "most" is not "all", and it is possible to write reasonable looking, and probably objectively reasonable, code where the inference becomes exponential over the set of types and overloads.

                                                              So that's the basic model of the type system which leads to this, but there are ways in which this is exacerbated. Let's go back to 1, earlier we said it is an Int, but it actually isn't, it is an integer literal, and types can implement a protocol (this is exactly equivalent to trait in rust, type class in Haskell - they're all literally the same type theoretic construct) - something like ConvertibleFromIntegerLiteral though definitely don't quote me on this :D. While that may not sound super useful, but it allows somethings someDouble + 1 to work entirely from within the logic of the type system, with no special casing of "I am adding this numeric type to an integer literal".

                                                              However I believe in the original version of swift, that meant

                                                              let x = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1....;
                                                              

                                                              Would take a lot of time very quickly because without being careful about how you prioritize search and similar, every call to + (in type theory and reason arithmetic operators are functions) might be to Double + Double or Int + Int, and that therefore mean each of the +s around them could be Int or Double, so that's fun. But that's also too simple because other types implement convertible from Int: Int8, Int16, Int32, UInt, UInt8, ..., Float, etc so suddenly there are many options.

                                                              In my experience the easiest way to cause this to happen in code that isn't written with the intent of demonstrating edge cases (e.g. the original 1+1+1... example above isn't what you would write in practice) is actually closures, I assume because absent an explicit type signature the type system simply has a single gigantic expression so essentially the entire body of the closure becomes exciting. Given the above complexity of the type inference system, at a base level it seems surprising to me that it generally handles most even relatively complex code, and that's because the actual algorithm involved is quite a bit more clever than the naive explanation above, however at a fundamental level their are expressions where inference becomes an exponential operation, but I think in some cases they become undecidable (which would assume is considered an error, as saying "we can't tell if your code is correct" seems somehow worse than "this takes to long, please provide us with some type information" :D).

                                                              I think a lot of the type system and language for swift is really nice, I also think a lot of rust is really nice, they've just taken different approaches and tradeoffs (ignoring the very different decisions around object lifetime as that isn't really involved in how the type systems and interfaces feel) that result in complexity in different areas. Swift does also have other considerations that I feel are less of an actual choice (it's possible it would have done this anyway): ignoring the different inference algorithms most of the type system of each is the same - they both inherit from the "true" functional languages, they both model type polymorphism in the same manner. But swift also has classes in the traditional polymorphic inheritance model, because it has to interact pretty much seamlessly with objective-c (to the extent that the vtable pointer itself is compatible with objective-c's isa pointer).

                                                              Again there are tradeoffs everywhere - the class portion of swift has a bunch of costs, but at the same time means that it is much easier for the type system to understand the C++ object model, which is much harder in rust (for instance, it is very difficult for rust to model the behavior of C++ types that have things like copy constructors). Noting of course that easier does not mean easy, because, you know, C++ :D

                                                              [1] In general most functions written in languages like Haskell are literally a->b->c: e.g if you write a function that takes two arguments and returns a value, it is the same type as a function that takes one argument and returns a function that takes another argument. At a core level in fact all functions only take one argument, and everything else is syntactic sugar. The equivalent of a two argument function in languages like C, rust, Swift, etc is (a, b)->c.

                                                              1. 5

                                                                Hindley-Milner is usually efficient but it can be exponential, for example in code that looks roughly like a billion laughs attack.

                                                                1. 3

                                                                  I can't remember the name of the actual type inference algorithm used by swift

                                                                  I would guess that it's based on Pierce's "Local Type Inference" because that's what most languages with subtyping seem to start with, but that's just a guess.

                                                                  I agree with all of your analysis. I think the crux of the issue is around overloading + implicit conversions = exponential.

                                                                  But there must be something more subtle going on because C# and C++ also have overloading and implicit conversions but as far as I know don't get wedged as badly as Swift. I would guess that those languages have some restrictions in how implicit conversions or overloads can be selected specifically to spare the type-checker from needing to explore the entire constraint solution space.

                                                                  For what it's worth, Dart avoids this problem. Type checking is (I believe!) linear or at worst maybe quadratic in some odd corners. We've avoided overloading and user-defined implicit conversions in large part to maintain the good performance of type inference and type checking.

                                                                  1. 3

                                                                    As far as I can tell It’s the addition of overloading on the return type.

                                                                    Ie Int f() Float f()

                                                                    Is invalid in most languages, even into() in rust which is still to a single type - albeit a generic one.

                                                                    1. 1

                                                                      I don't believe return type overloading is necessary to cause problems. It's enough to have:

                                                                      1. Overloading by parameter types.
                                                                      2. Generic methods or implicit conversions. (I suspect either is enough but Swift has both.)
                                                                      3. Type inference that pushes a surrounding context type into subexpressions and also uses subexpressions to infer surrounding expressions.

                                                                      Once you've got those then given a call like a(b()):

                                                                      1. You don't know which overload of a() to call until you know the return type of b().
                                                                      2. But if b() is generic or returns a type that has an implicit conversion, then you don't know the return type of b() until you've selected an override for a() that provides a context type to infer the generic type argument or implicit conversion.

                                                                      This sets up a complex nest of constraints that has to be solved. Now imagine that expression have multiple arguments and nested calls.

                                                                      1. 2

                                                                        The point is that without return type overloading in your above example a(b()) you by definition know the return type of b(). The only way you do not know that return type is if b() has multiple definitions with different return types.

                                                                        b() being generic isn't particularly relevant because there is only ever one option - inferring the return type is either easy via unification with the parameter type of a, or it is undecidable if there are multiple versions of a which is where you get ambiguous overloads.

                                                                        It's very important to recognize the a function that is returning a dependent parameter is not overloaded on the return type - there is only a single return type, and that is the type parameter.

                                                                        But if b() is generic or returns a type that has an implicit conversion, then you don't know the return type of b() until you've selected an override for a() that provides a context type to infer the generic type argument or implicit conversion.

                                                                        This is what you're misunderstanding: if you are not able to resolve the a single declaration for the function b based solely on the parameter types provided, then resolution has failed and you'll get a warning along the lines of "ambiguous overload". By definition overload resolution does not care about the return type, only the parameters to b. e.g. there is no difference between

                                                                        a(b())
                                                                        

                                                                        and

                                                                        auto v = b();
                                                                        a(v)
                                                                        

                                                                        If you can resolve b in b() you know the return type, if you can't or there are multiple choices the initial resolution of the function b will fail.

                                                                        C++'s inference doesn't even try to handle inference over a generic return type but other languages like rust do, so consider the common trait function into(), it has a declaration along the lines of:

                                                                        fn into<T>() -> T;
                                                                        

                                                                        Now if you call something like f(someValue.into()) there's no return type overloading: there is only a single candidate function into to choose, which means that in the above expression we can unify T with whatever (potentially also generic) parameter type of the argument to f.

                                                                        1. 2

                                                                          Oh, sorry, I was in a hurry and didn't come up with a good example. You're right that my example doesn't work.

                                                                          1. 2

                                                                            No worries, it's easy to conflate generic return types with overloaded return types :D

                                                                  2. 1

                                                                    Thank you for this write-up, that’s been really insightful! I guess the overloading really screws up the time complexity!

                                                                    1. 3

                                                                      I think it’s specifically the return type overloading - for other systems once you find a candidate function you can immediately use the concrete return type of that function because all functions with the same identity must have the same return type

                                                                  3. 3

                                                                    Haskell (and other languages like it) doesn't support overloading, which helps a ton AFAICT.

                                                                    1. 7

                                                                      Haskell has overloading, that's what typeclasses are for.

                                                                      1. 7

                                                                        Yes Haskell has "overloading", but this is not a thread about Haskell, so what @robinheghan wrote was

                                                                        Haskell (and other languages like it) doesn't support <overloading as defined in swift, java, c++, c#, etc>, which helps a ton AFAICT

                                                                        Which is correct, and what you replied with is

                                                                        Haskell has <overloading as defined in swift, java, c++, c#, etc>, that's what type classes are for

                                                                        Which is incorrect.

                                                                        Your comment here is like someone going to a baseball game, and calling a "ball", but using the cricket definition of that term. It's same word, in a similar context, but similar is not "the same as", and so even if the definition of "ball" that they used is correct in cricket, it's still incorrect because that's not the game being played.

                                                                        Given the thread and article being discussed, Haskell calls "overloading" is what these languages simply call "generic functions", and you have to accept that, because that's what the definitions being used are.

                                                                        If Haskell did support <overloading as defined in swift, java, c++, c#, etc>, you would be able to write

                                                                        f = 1
                                                                        f = "foo"
                                                                        

                                                                        This is not mutating a value, nor is it replacing one. There are now two functions, both named f, one of type Int and one of type [Char]. That is not something Haskell allows, and type classes don't change that. + is not an overloaded function, there is just one function named +, and it has the type forall a. (Num a) => a -> a -> a, that is the type it has in every place it is called, there are not different versions for different types.

                                                                        1. 3

                                                                          Both languages allow overloading functions, they obviously differ in how they do it. It doesn't matter what names you Swift-style overloading vs. Haskell-style overloading, it's the same feature serving the same purpose, implemented in different ways.

                                                                          1. 1

                                                                            Right, and the definition of overloading is not the same, so claiming that they're equivalent is incorrect.

                                                                            Haskell "overloading" exists in these other languages, and that does not cause any of the problems discussed.

                                                                            Moreover the overloading that they support has literally no equivalence in Haskell, and is not possible in any way. There is literally no equivalence. Please stop trying to claim that they are, or that your comment is correct.

                                                                            Your comment is like "both languages have identifiers, and Haskell can distinguish type variables and type names trivially, because Haskell type variables start with lower case letters".

                                                                            They are not the same, stop trying to claim your comment was correct.

                                                                            I explained why it is not.

                                                                            You could simply accept that you were wrong through and easily made error, or you can double down. Please stop trying to double down, I do know what I'm talking about, I have literally implemented a Haskell compiler.

                                                                        2. 2

                                                                          That’s the first time I’ve seen type classes referred to as overloading.

                                                                          I was specifically referring to function overloading, which Swift supports in addition to ad hoc polymorphism.

                                                                          1. 2

                                                                            That’s the first time I’ve seen type classes referred to as overloading.

                                                                            It's called overloading since at least 1989:

                                                                            This paper presents type classes, which extend Hindley/Milner type system to include certain kinds of overloading, ...

                                                                            From https://dl.acm.org/doi/pdf/10.1145/75277.75283.

                                                                            1. 3

                                                                              Oh, I didn’t mean to imply it wasn’t correct. Just expressing surprise :)

                                                                              1. 2

                                                                                Haskell just has a different definition of overloading, so in this thread your statement was correct :)

                                                                                1. 1

                                                                                  It's the same feature, obviously with differences.

                                                                                  What you're saying is like: Haskell and Java have different ways to do polymorphism, therefore one is not polymorphism.

                                                                                  1. 2

                                                                                    No.

                                                                                    The entire point of your comment is "Haskell has overloading, therefore it is not a problem".

                                                                                    Haskell's overloading is not overloading in other languages, claiming they're the same is simply false.

                                                                      2. 4

                                                                        This piece does not have an auspicious start:

                                                                        As all developers, I’ve been using git since the dawn of time

                                                                        Guess I'm not a developer, because my first experience with svn. Git has only been around since 2005.

                                                                        Needless to say, I just don’t get git. I never got it, even though I’ve read a bunch of stuff on how it represents things internally. I’ve been using it for years knowing what a few commands do, and whenever it gets into a weird state because I fat-fingered something, I have my trusty alias, fuckgit

                                                                        While "I couldn't figure out git, but I like jujutsu" might be a good indication that jujutsu is easier to use than git, it also means that this is a very bad piece to read if you want a mental model for either.

                                                                        1. 7

                                                                          Git has only been around since 2005.

                                                                          So you could have 20 years of developer experience and still have used nothing but git.

                                                                          Where I work, developers with 20+ years of developer experience is rare.

                                                                          1. 5

                                                                            That may show a bias in your hiring practices...

                                                                          2. 4

                                                                            Mine was CVS, I just didn't realize you SVN newbies would take things in the post so literally.

                                                                            1. 2

                                                                              It's written in a pretty literal-sounding way. Guess I missed the intended facetious tone?

                                                                              If so, you may want to reconsider the phrasing. It makes you sound like someone who is a lot less experienced and really has only encountered git. (There are assuredly many such people...)

                                                                          3. 33

                                                                            I don't get it. All those sound like anti features to me, and a messy way to work. Maybe git adapts to my mental model better

                                                                            1. 22

                                                                              I think of it as a shift from taking each commit as the unit of work and the code files as the object of manipulation to taking the entire series of commits as the object of manipulation. Some people already use git this way, squashing and rebasing aggressively, but jj makes it easier.

                                                                              1. 2

                                                                                There are two camps, each commit is atomic and represents work, and a series of commits are a change and the intermediate commits are just checkpoints to be squashed or a merge-commit at worst.

                                                                              2. 13

                                                                                I find it really hard to convincingly explain why Jujutsu is nicer to use because several features sound scary or weird in isolation but work together to make a great UX:

                                                                                • Automatic snapshotting means there is no such thing as “uncommitted changes”. All commands modify commits, not the working copy.
                                                                                • Anonymous branch heads means it feels like no big deal to start a new branch. There’s little friction encouraging you to pile things onto the tip of an existing branch.
                                                                                • Automatic rebasing means editing commits earlier in a stack is one step instead of (commit changes, then rebase interactively).

                                                                                If it still sounds weird, I encourage you to try it on a pet project for a couple days. Most people seem to have one of three reactions:

                                                                                • Jujutsu is missing a feature I rely on so I can’t switch (LFS, submodules are probably most common, but even submodules have a workaround). Understandable.
                                                                                • Jujutsu makes more sophisticated operations doable for me (editing history).
                                                                                • Jujutsu makes the operations I was already doing easier.

                                                                                I’ve no doubt some people try it earnestly and decide it’s not for them, but that doesn’t seem to be the norm.

                                                                                1. 6

                                                                                  I tried it, based on the Klabnik tutorial, and I was struggling. My mental model now is that jujutsu is two version control systems; one for managing the local unpushed commits, and one (basically git) for managing the commits already shared with other developers.

                                                                                  Git may have its problems, but once I understood it's object model, everything clicked into place. And yes, I'm aware that it's actual object model is a lot more complicated than its conceptual one. So far, I haven't had this experience with jujutsu. And I'm a bit worried that it's so aggressively pushed as a "succesor" to git.

                                                                                  Keep in mind that when using git, I treat rebase and cherry picks as the occasional operations they're supposed to be. git works best when it has the full merge history available, and I don't think rebase was ever supposed to be a core part of the workflow.

                                                                                  pijul makes a lot of sense to me, but I've yet to use it in a project myself. But I think its failing might be that it's even more "git-like" than git.

                                                                                  1. 13

                                                                                    git works best when it has the full merge history available

                                                                                    I used to work on source control. This is actually not true, because (among other things) having a full merge history leads to the likelihood of what are known as criss-cross merges: situations where there isn't a single nearest common ancestor/merge base between two commits. (Merge bases are a crucial piece of information for many source control algorithms.) Criss-cross merges are a pain to deal with, both within the version control system itself and within tools written on top of the VCS. As a result, many tools simply consider criss-cross merges to be outside of their design parameters.

                                                                                    In contrast, a linear history can never have criss-cross merges, greatly simplifying many algorithms.

                                                                                    In general, linear history is friendlier for both tools and humans.

                                                                                    edit: to give an example, a tool I helped build for work needs to load files from the merge base of a commit so it can ensure that "blessed" files don't change in nontrivial ways. The tool simply (and correctly) considers criss-cross merges to be out of scope: https://github.com/oxidecomputer/dropshot-api-manager/blob/bb71488cfa2f642b232ef619bbb3d2c287c8e336/crates/dropshot-api-manager/src/git.rs#L22

                                                                                    Opting into a history full of merges means imposing a burden on the entire ecosystem.

                                                                                    1. 3

                                                                                      There is no total ordering of commits; that's just a fact of people working in parallel. I accept that pretending that such a total ordering exists simplifies things in people's minds and in code, but it doesn't simplify the actual problem.

                                                                                      1. 5

                                                                                        I'm not sure what this has to do with criss-cross merges. There absolutely is a total ordering of when commits were integrated into main, and linear histories do simplify the actual problem.

                                                                                    2. 7

                                                                                      git works best when it has the full merge history available

                                                                                      I think this is something on which reasonable people may disagree, and may be a function of what work they’re involved with. I’m happiest looking at a linear history of cleaned-up, well-described atomic commits. And, I’ve never worked on a project like Linux where a maintainer is pulling from multiple independent lines of development that may not have been recently rebased as a matter of course. As such jj is an ergonomic improvement for me, having previously spent a lot of hours doing things in git rebase -i.

                                                                                    3. 1

                                                                                      I see. In my case, i don't rely in other than the base features (commit, branch, remotes), don't edit history often, and normally --amend is enough if I need it. What i see is that people happy with it do an awful lot of local history editing, maybe that's the big point

                                                                                      1. 4

                                                                                        Well, since your working directory is auto-commited, everything you do in jj is technically local history editing.

                                                                                        As a side effect, this makes history editing intuitive/easier and changes your behaviour in subtle ways.

                                                                                        In git I tend to avoid history editing, even when I think I'd possibly benefit from it in the future, because I can't be bothered to google the commands required for what I want to do (It's rare enough that I don't keep it memorized). In jj, the commands are intuitive enough that I can figure it out on my own, so I just do the edits when it makes sense.

                                                                                        Then there are several small things that I really appreciate, like not having to create branches anymore because most of them was created for the purpose of creating a PR anyway (I just do jj git push --change @), or not needing git stash due to the auto-commit feature.

                                                                                    4. 5

                                                                                      I can't even count anymore the number of bogus commits jj has prevented me from making. I just edit the thing I'm working in place and push back to github.

                                                                                      1. 5

                                                                                        It's probably easier to start by comparing mercurial to git, and then take the learnings and apply them to JJ. The undo/redo feature just by itself is worth it (although if you're a git expert, may be less relevant), and you need to do a rebase in JJ to see how much better it is.

                                                                                        1. 1

                                                                                          Jujutsu seems to me like the revenge of mercurial. Some people seem to have a mental model that aligns better with mercurial. I never liked mercurial when it was relevant, it perpetually felt odd to me in every way that it diverged from git. Now that mercurial is irrelevant, seems like the mercurial-minded people now gather under the banner of Jujutsu.

                                                                                          1. 4

                                                                                            As Steve said, there's a lot of hg in jj, though there's also a lot new that came from hard-won experience deploying git/hg in production and teaching them to users at all skill levels. (It is a very common and unfortunate belief that only developers use VCS.)

                                                                                            1. 2

                                                                                              There’s a lot of hg in jj, for sure. But I never used hg, and found parts of it confusing, because I loved git. And now I love jj.

                                                                                          2. 2

                                                                                            Am I right in assuming that Radicle repository discovery is based on raw broadcasting (or whatever the technical term for that is), i.e. everybody is expected to have a big table with all repositories and the nodes that host them?

                                                                                            I've tried to use Radicle last week and was unable to git clone even the Radicle codebase itself.

                                                                                            1. 1

                                                                                              I don't think so.

                                                                                              I believe each peer has a list of nodes it can contact to search/clone new repositories. But each peer is only responsible for sharing the repositories they've cloned themselves.

                                                                                              1. 2

                                                                                                That's what I said. You only host the repositories you've cloned, but every node still needs a big map of the network.

                                                                                                And every time a repo is created or changed or cloned that information has to be broadcasted to all nodes in the network.

                                                                                            2. 4

                                                                                              I simply don't want to provide the tools for people to write programs in Clojure with N^2 performance - that benefits no one

                                                                                              1. 4

                                                                                                That's a bit odd. You can do (cons x some-vector) and it'll be O(n), which is probably what you don't want. In fact, it's a common pitfall for beginners.

                                                                                                1. 2

                                                                                                  Doesn't cons return a seq, which is O(1)?

                                                                                                  1. 3

                                                                                                    It does, but converting the vector to a seq is O(n)

                                                                                                    1. 4

                                                                                                      This is inaccurate; creating a seq of a vector is basically free (on account of being lazy), but indexing that seq by offset is obviously O(n).

                                                                                                      1. 3

                                                                                                        I stand corrected. Thanks!

                                                                                                2. 3

                                                                                                  That's interesting, because he would have to forbid nested loops to do that.

                                                                                                  1. 8

                                                                                                    The full quote has the context:

                                                                                                    You can't just swap out sequences or lists for maps or sets in a program. So, you need to use the right data structure for the job, and then the right functions will be available. When it makes sense, some functions are maximally polymorphic (e.g. seq, or into). But lookup, under any name, shouldn't be, IMO, so it isn't in Clojure. Similarly there is no insert at the beginning of vectors, append to end of lists, lookup for values of maps, insertion in the middle of sequences etc.

                                                                                                    And he's right, I have fixed the following N^2 bug MANY times in other people's Python code:

                                                                                                    for x in mylist:
                                                                                                       if x in otherlist:
                                                                                                          pass
                                                                                                    

                                                                                                    It's quadratic, but there's no double loop. On the other hand, this algorithm is linear:

                                                                                                    for x in myset:
                                                                                                       if x in otherset:
                                                                                                          pass
                                                                                                    

                                                                                                    I'd go as far as to say this is the most common Python performance bug I've encountered.

                                                                                                    The key is the broken polymorphism -- you should not use the same syntax in for an operation that is either O(1) or O(n).

                                                                                                    O(1) and O(n) operations should have a different spelling, to make it clear they have different performance characteristics


                                                                                                    I might go even further and say that if you've used Python for say 10 years, and you haven't fixed this bug, there's a good chance you have it and don't realize it :-)

                                                                                                    At least for web and data science stuff, where it tends to come up

                                                                                                    1. 4

                                                                                                      Actually this is pretty much the same point I made the other day about SQL - some joins are inherently O(M x N), but they don't have a sufficiently different syntax than fast joins IMO

                                                                                                      https://lobste.rs/s/t8fc8a/unexplanations_relational_algebra_is#c_gsdg75

                                                                                                      It's also common in practice for SQL queries to "blow up" in running time. And then the authors of those queries have to spend a lot of time figuring out why, which is not obvious from looking at the code

                                                                                                      If the syntax were more suggestive, that could help


                                                                                                      And I should add that this same philosophy made its way into the YSH language design:

                                                                                                      ysh-0.35$ = 'foo' in {'foo': 42}
                                                                                                      (Bool)  true
                                                                                                      
                                                                                                      ysh-0.35$ = 'foo' in ['foo', 'bar']
                                                                                                        = 'foo' in ['foo', 'bar']
                                                                                                        ^
                                                                                                      [ interactive ]:4: fatal: RHS of 'in' should be Dict, got List
                                                                                                      
                                                                                                      ysh-0.35$ = ['foo', 'bar'].indexOf('foo')
                                                                                                      (Int)   0
                                                                                                      

                                                                                                      That is, we don't have "broken polymorphism" for in. Instead, you use in for the O(1) operation, and mylist.indexOf() for the O(n) operation. (the indexOf() name matches JavaScript)

                                                                                                    2. 5

                                                                                                      In context it's clear he's talking about defaults, in the sense of providing clojure.core functions that would naturally lead developers to poor algorithmic complexity categories. The sentence prior:

                                                                                                      there is no insert at the beginning of vectors, append to end of lists, lookup for values of maps, insertion in the middle of sequences etc.

                                                                                                    1. 7

                                                                                                      Hi. Have you got a paper you can link to? (I generally avoid videos?)

                                                                                                      1. 4

                                                                                                        Check out the repo: https://git.sr.ht/~robheghan/glogg

                                                                                                        There are links in the "further reading" section at the bottom.

                                                                                                        However, the video is mostly a live demo and covers features that I haven't gotten around to documenting just yet.

                                                                                                        1. 1

                                                                                                          Thanks.

                                                                                                          Are you familiar with Prolog? It's text in a file, not in a DB, but that's not really vital. But the fundamental mechanism of Prolog (and related) seems similar to what you've done. Might be interesting/helpful.

                                                                                                          1. 1

                                                                                                            I’m familiar. I’ve implemented a text-adventure game and a language parser for a toy language in Prolog, which was a very interesting experience.

                                                                                                            Gløgg is a datalog (prolog without backtracking).

                                                                                                        2. 4

                                                                                                          Could you give a short abstract of the talk so I could know if it's interesting to me? Thank you!

                                                                                                          1. 7

                                                                                                            Abstract

                                                                                                            There seems to be a new language coming out every month, but most of them are surprisingly similar. They tend to focus on instructing the machine on how to do work through a series of ifs, loops, numeric calculation and object creation. To a machine, this is fine. To a human, this is not ideal. Figuring out what code does requires decoding these instructions into a mental model of the result it produces.

                                                                                                            While this might have been an acceptable tradeoff in a time where every CPU cycle mattered, it's a bad proposition today when we mostly care about developer productivity and time to market.

                                                                                                            Is there an alternative? Is it possible to design a language that puts the human being first, and the machine second?

                                                                                                            In this talk Robin, an earlier Elm contributor and the current lead developer of Gren, shares his ideas for the "programming language of the future".


                                                                                                            The video introduces these ideas in the form of a programming language, called Gløgg, and most of the video is a live demo of what you can do with it.

                                                                                                          2. 4

                                                                                                            I'd be interested in how much you were influenced by systems like Fossil, which also puts everything into a SQLite database, or Idris, which also supports a rigid interactive-editing experience. I would also be interested in whether you looked at the more esoteric corners of the constraint-and-logic design space, particularly CHR (WP, esolangs), which isn't a programming system on its own but is meant to be attached to a host system, and Pantagruel, which is a language for specifying specifications by checking coherency without correctness. I don't doubt that Eve was your primary influence, but the secondary influences are interesting too.

                                                                                                            You might be interested in some of the programming systems I've built. Zaha is an attempt to reason solely with posets and stores data in PNG files which can be interactively edited. Cammy is a fairly boring minimalist language that can be content-addressed like Unison or Dark. A v3 Cammy hive is merely a git repository with some special objects and bookmarks.

                                                                                                            1. 2

                                                                                                              I actually use Fossil for several private projects. I wouldn't say Fossil has had a great influence on the design of Gløgg, at least not any more than SQLite itself has. I've heard Unison also stores code in a database, but I haven't looked at Unison myself other than a talk I saw at Lambda Days in '23.

                                                                                                              I've heard of Idris, and read through some of the docs, but I haven't really written any code in it, so I don't know about the editing experience.

                                                                                                              The rest of the links you mention are entirely new to me, and I've added them to my "read later" queue. Thank you!

                                                                                                              I responded to a sibling post about other influences I've had :)

                                                                                                            2. 4

                                                                                                              If You like DataLog, have you seen Egglog? (The e-graph rewriting system, combined with data log) https://github.com/egraphs-good/egglog/

                                                                                                              Would the ideas from it fit in your programming language?

                                                                                                              1. 2

                                                                                                                Oooo! This sounds interesting!

                                                                                                                I know of e-graphs, but Egglog is new. Thanks for the link!

                                                                                                              2. 2

                                                                                                                Thank you for posting your talk. Very interesting. I heard you talk about React and Eve (being a higher abstraction). But I was wondering if you had other breadcrumbs to other languages, tool chains, etc... that you found inspiring and influential.

                                                                                                                1. 3

                                                                                                                  I believe I first came across logic programming in Clojure with the core.logic package. Clojure also has the Datomic database, which uses Datalog(Prolog without backtracking) as a query language.

                                                                                                                  I've also implemented a text-adventure game and a language parser for a toy language in Prolog, which was a very interesting experience.

                                                                                                                  I also believe I've been influenced a great deal by being a full stack developer. In the backend, you can always call out to the database to retrieve some values, which has always seemed easier to me than the different ways we have of managing state in the frontend world. If you imagine that a database is the invisible first argument to every function, and that database uses Datalog as a query language, you have something similar to Eve/Gløgg.

                                                                                                                  1. 2

                                                                                                                    Thank you. I got strong hytradboi vibes watching the demo. Particularly this talk - DatalogUI: rubbing datalog on UIs

                                                                                                                    1. 1

                                                                                                                      Thank you for the links, I’ll check them out 😊

                                                                                                              3. 5

                                                                                                                Funny, I always felt the exact opposite. As a language nerd growing up in the 2000's I was starved. Between 2000 and 2010 or so your choices for writing practical programs were: C/C++, Java, and C#, plus PHP and some other weird web stuff like ColdFusion. There was also Python, Perl and Ruby and JS, but nobody wrote much real stuff in those. And if you were a weird nerd like me you also had a handful of functional languages or other off-beat things: Common Lisp, Scheme, OCaml, or Haskell if you went really off the deep end. Then slooooowly scripting languages got into the mainstream for actual application dev, Go pried open the minds of C programmers and node.js did the same for backend webdevs, and you started getting compelling new languages like Dart, Elixir and such in a rapid-fire explosion between ~2009 and ~2013.

                                                                                                                It also depends where you measure from. Kotlin, Elixir and Dart on might have started between 2010-2013 but they sure as heck didn't become big in that time. Languages take time to build momentum, if they ever do. When did people actually start using Typescript in earnest? If I recall, it took a while.

                                                                                                                The period from around 2002 to ~2013 was an amazing time to be a langdev geek but the years between 2014 and 2021 were humdrum.

                                                                                                                Wat. Rust, anyone? Zig? The half-dozen little nascent spin-offs that they've birthed? Gleam? Roc? Sure it was overall, a time of consolidation; that tends to happen after revolutions. (Does anyone seriously miss Boo or Groovy or Fortress, or expect the world to be taken by storm by HaXe or PicoLisp?) The wheel turns, the cycle continues, and it takes time for new ideas to filter between industry and academia and back, or between different branches of them.

                                                                                                                1. 1

                                                                                                                  Kotlin, Elixir and Dart on might have started between 2010-2013 but they sure as heck didn’t become big in that time.

                                                                                                                  Language nerds probably didn’t wait til they got big, though, at least I didn’t. I wrote a tic tac toe game in the first Kotlin beta that had JS support (0.8?).

                                                                                                                  Rust, anyone?

                                                                                                                  First appeared in 2012, according to wikipedia.

                                                                                                                  1. 2

                                                                                                                    Yeah but Rust 1.0 hit at the end of 2015, and that's when it actually kinda started getting popular. IIRC Graydon noodled around with Rust in private for years as well, looking at the rustc repo the first commit is in 2010. And the first commit is 33,000 lines of OCaml and some early commit messages imply it was imported from a hg repo.

                                                                                                                    It's hard to measure when ideas "start".

                                                                                                                    1. 1

                                                                                                                      To be fair, the article leads with:

                                                                                                                      For programming language enthusiasts, the last decade has felt like one of Ballard’s “long seasons.”

                                                                                                                      And for programming language enthusiasts, it’s fair to assume that they pay attention to a language long before it reaches 1.0.

                                                                                                                      Zig, for instance, hasn’t reached 1.0 but is still well known.

                                                                                                                      1. 1

                                                                                                                        As robinheghan pointed out, the post was written from an enthusiast's perspective. People who create and work on new programming languages also tend to be enthusiasts, so the ecosystem of new languages and ideas tends to find an awareness amongst that group much sooner than it takes for any language to become popular.

                                                                                                                  2. 7

                                                                                                                    Anyone using radicle for a project with external contributors?

                                                                                                                    I've tried it with some of my private projects and it seems promising, but I wonder what it'd be like to use it on one of my more public projects instead of Github.

                                                                                                                    1. 4

                                                                                                                      Huh. Veem looks interesting. Similar to Elm but also runs server-side?

                                                                                                                        1. 2

                                                                                                                          I think Gren is a fork of Elm?

                                                                                                                          1. 5

                                                                                                                            That's right. I believe the idea with is to both allow community improvements instead of everything being tied to the BDFL's say and to allow code to run on more platforms than just browsers.

                                                                                                                        2. 1

                                                                                                                          I'm curious about Gren's motivation, design and lineage, but didn't find much on the website. It does look a bit like Elm, and seems to be geared towards full stack development, but what's the approach, why does it stand out?

                                                                                                                          1. 6

                                                                                                                            I’m the lead developer of Gren, I’ve also contributed to Elm’s core packages in the past.

                                                                                                                            I started Gren to make the language I initially thought Elm would become, with a few additional changes based on years of using Elm professionally.

                                                                                                                            More information here: https://gren-lang.org/book/appendix/faq/

                                                                                                                            1. 5

                                                                                                                              My understanding:

                                                                                                                              Robin (the lead of Gren) was an active contributor to elm.

                                                                                                                              Robin wanted a language that was:

                                                                                                                              • Elm. But...
                                                                                                                              • With up to date browser APIs
                                                                                                                              • With node support
                                                                                                                              • With some syntax tweaks and some different design decisions (like Arrays as the default list type)

                                                                                                                              He forked elm, but basically as a shortcut to that first bullet point. He's actively re-writing the compiler to be self hosted and has already made a number of breaking changes.

                                                                                                                              Check out this video to hear it in his own words: https://www.youtube.com/watch?v=NMGShaoRAZE

                                                                                                                              1. 2

                                                                                                                                I'm also curious about the back story. But if you check out the gren compiler project, it is literally a fork of Elm, that Evan Czaplicki stopped committing to in August 2021.

                                                                                                                              2. 4

                                                                                                                                Helix had been my daily driver for two years at this point.

                                                                                                                                Having built in support, including lsp and tree sitter integration, for so many languages means i never need to spend time in config.

                                                                                                                                My config is two lines: relative line numbers and theme.

                                                                                                                                1. 5

                                                                                                                                  I moved from Emacs to Helix because LSP configuration in Emacs felt hellish and it was always so slow - Helix just werks™.

                                                                                                                                  1. 3

                                                                                                                                    I'm pretty sure that my dream text editor is some amalgamation of Emacs + helix/kakoune.

                                                                                                                                    1. 4

                                                                                                                                      Emacs with meow should get most of the way there?

                                                                                                                                      1. 1

                                                                                                                                        I do use meow with Emacs whenever I try out Emacs, but it's not as snappy as helix or kakoune when it comes to language servers and multiple cursors.

                                                                                                                                  2. 2

                                                                                                                                    My helix config is similarly small. A third configuration item that I find useful is binding file_picker_in_current_buffer_directory, especially in projects with deeply nested directories.