1. 2

    Blessed day!

    1. 2

      Sounds and looks very interesting. Can’t wrap my mind around the way message passing can be done, though.

      1. 2

        We use a channels abstraction to type messages, similar to Go or Rust. They can be combined to make type safe selective receives.

        See the OTP library for details -> https://github.com/gleam-lang/otp

        1. 1

          IDK how Caramel does it, but one way to do it is to type functions on the messages it can receive. This type then gets “inherited” by processes that are spawned with that function. This means all match clauses inside the function’s receive / all messages sent to the process must match the function’s “receive type”

          1. 2

            We originally tried this but found it was too limiting. It was impossible to implement things like supervisors and gen:call, so at best you could write a thin typed layer on top of untyped Erlang code.

            Gleam OTP by contrast is a small ~200 line Erlang core, and the rest is type safe Gleam.

        1. 4

          Love to see it. I use Elixir everyday at work, and am a big fan of static types, so in principle I’m the target audience. However, I’m reluctant to give up the Elixir ecosystem I love do much. E.g. I’m really excited LiveView, Livebook, Nx, etc.

          What are the benefits / necessity of a whole new language as opposed to an improved dialyzer, or maybe a TypeScript style Elixir superset with inline type annotations?

          1. 11

            One issue is that existing Elixir code will be hard to adapt to a sound type system, more specifically in how pattern matching is used. For example, consider this common idiom:

            {:ok, any} = my_function()
            

            (where my_function may return {:ok, any} or {:error, error} depending on whether the function succeeded)

            Implicitly, this means “crash, via a badmatch error, if we didn’t get the expected result”. However this is basically incompatible with a sound type system as the left-hand side of the assignment has the type {:ok, T} and the function has the return type {:ok, T} | {:error, Error}.

            Of course we could add some kind of annotation that says “I mismatched the types on purpose”, but then we’d have to sprinkle these all over existing code.

            This is also the reason why Dialyzer is based on success typing rather than more “traditional” type checking. A consequence of this is that Dialyzer, by design, doesn’t catch all potential type errors; as long as one code path can be shown to be successful Dialyzer is happy, which reflects how Erlang / Elixir code is written.

            1. 5

              Of course we could add some kind of annotation that says “I mismatched the types on purpose”, but then we’d have to sprinkle these all over existing code.

              This is what Gleam does. Pattern matching is to be total unless the assert keyword is used instead of let.

              assert Ok(result) = do_something()
              

              It’s considered best practice to use assert only in tests and in prototypes

              1. 1

                What do you do when the use case is “no, really, I don’t care, have the supervisor retry because I can’t be bothered to handle the error and selectively reconcile all of this state I’ve built up, I’d rather just refetch it”?

                1. 1

                  Maybe we need another keyword:

                  assume Ok(result) = do_something()
                  
                  1. 1

                    That is what assert is for.

                  2. 1

                    That’s what assert is. If the pattern doesn’t match then it crashes the process.

                    1. 1

                      So why not use it in production?

                2. 3

                  Great, succinct explanation!

                  1. 2

                    This is an interesting example. I’m still not sure I understand how it’s “basically incompatible”, though. Is it not possible to annotate the function with the possibility that it raises the MatchError? It feels kind of like Java’s unchecked exceptions a bit. Java doesn’t have the greatest type system, but it has a type system. I would think you could kind of have a type system here that works with Elixir’s semantics by bubbling certain kinds of errors.

                    Are you assuming Hindley Milner type inference or something? Like, what if the system were rust-style and required type specifications at the function level. This is how Elixir developers tend to operate already, anyway, with dialyzer.

                    1. 1

                      I don’t see how that’s a problem offhand. I’m not sure how gleam does it, but you can show that the pattern accommodates a subtype of the union and fail when it doesn’t match the :ok.

                      1. 4

                        The problem is the distinction between failing (type checking) and crashing (at runtime). The erlang pattern described here is designed to crash if it encounters an error, which would require that type checking passes. But type checking would never pass since my_function() has other return cases and the pattern match is (intentionally) not exhaustive.

                    2. 4

                      One thing is that TypeScript is currently bursting at the seams as developers aspirationally use it as a pure functional statically-typed dependently-typed language. The TypeScript developers are bound by their promise not to change JavaScript semantics, even in seemingly minor ways (and I understand why this is so), but it really holds back TS from becoming what many users hope for it to be. There’s clearly demand for something more, and eventually a language like PureScript / Grain / etc will carve out a sizable niche.

                      So, I think starting over from scratch with a new language can be advantageous, as long as you have sufficient interoperability with the existing ecosystem.

                      1. 2

                        I won’t go too much into Dialyzer as I’ve never found it reliable or fast enough to be useful in development, so I don’t think I’m in a great place to make comparisons. For me a type system is a writing assistant tool first and foremost, so developer UX is the name of the game.

                        I think the TypeScript question is a really good one! There’s a few aspects to this.

                        Gradual typing (TypeScript style) offers different guarentees to the HM typing of Gleam. Gleam’s type system is sound by default, while with gradual typing you opt-in to safety by providing annotations which the checker can then verify. In practice this ends up being quite a different developer experience, the gradual typer requires more programmer input and the will to resist temptation not to leave sections of the codebase untyped. The benefit here is that it is easier to apply gradual types to an already existing codebase, but that’s not any advantage to me- I want the fresh developer experience that is more to my tastes and easier for me to work with.

                        Another aspect is just that it’s incredibly hard to do gradual typing well. TypeScript is a marvel, but I can think of many similar projects that have failed. In the BEAM world alone I can think of 4 attempts to add a type checker to the existing Elixir or Erlang languages, and all have failed. Two of these projects were from Facebook and from the Elixir core team, so it’s not like they were short on expertise either.

                        Lastly, a new language is an oppotunity to try and improve on Elixir and Erlang. There’s lots of little things in Gleam that I personally am very fond of which are not possible in them.

                        One silly small example is that we don’t need a special .() to call an anonymous function like Elixir does.

                        let f = fn() { 1 }
                        f()
                        

                        And we can pipe into any position

                        1
                        |> first_position(2)
                        |> second_position(1, _)
                        |> curried_last_position
                        

                        And we have type safe labelled arguments, without any runtime cost. No keyword lists here

                        replace(each: ",", with: " ", in: "A,B,C")
                        

                        Thanks for the questions

                        edit: Oh! And RE the existing ecosystem, you can use Gleam and Elixir or Erlang together! That’s certainly something Gleam has been built around.

                        1. 1

                          Two of these projects were from Facebook and from the Elixir core team, so it’s not like they were short on expertise either.

                          Oh, wow, I don’t think I’ve heard of these! Do you have any more info? And why was Facebook writing a typechecker for Elixir? Are you talking about Flow?

                      1. 2

                        Hats off to the Python team for being pragmatic and postponing this feature.

                        1. 2

                          Now for the pressing question - what is “yeet” in past tense? Yote?

                          1. 3

                            Yeet / yote / yeeten - I’m totally for that version.

                          1. 3

                            Very excited to see Rust catching on in game development circles, especially since Rust seems to tick all the boxes for Tim Sweeney’s vision for the next mainstream programming language

                            1. 5

                              This is a fantastic talk! The idea that robust systems are inherently distributed systems is such a simple and obvious idea in hindsight. Distributed systems are difficult, and I have had upper managers claim that we need “more robust” software and less downtime, yet refuse to invest in projects which involve distributed algorithms or systems (have to keep that MVP!). I think Armstrong was right that in order to really build a robust system we need to design for millions of users, even if we only expect thousands (to start), otherwise the design is going to be wrong. Of course this is counter-intuitive to modern Scrum and MVPs.

                              Additionally, there is so much about Erlang/OTP/BEAM that seem so cutting-edge yet the technology has been around for a while. It will always be a wonder to me that Kubernetes has caught on (and the absolutely crazy technology stack surrounding it) yet Erlang has withered (despite having more features), although Elixir has definitely been gaining steam recently. Having used kubernetes at the past two companies I’ve been at, it has been nothing but complicated and error-prone, but I guess that is just much of modern development.

                              I have also been learning TLA+ on the side (partially to just have a leg to stand on when arguing that a quick and sloppy design is going to have faults when we scale up, and we can’t just patch them out), and I think there are so many ideas that Lamport has in the writing of the TLA+ Book that mirror Armstrong’s thoughts. It is really unfortunate that software has figured out all of these things already but for some reason nobody is using any of this knowledge really. It is rare to find systems that are actually designed rather than just thrown together, and that will never lead to robust systems.

                              Finally, I think this is where one of Rust’s main features is an under-appreciated super-power. Distributed systems are hard, because consistency is hard. Rust being able to have compile-time checks for data-races is huge in this respect because it allows us to develop small-scale distributed systems with ease. I think some of the projects bringing OTP ideas to Rust (Bastion and Ludicrous are two that come to mind) have the potential to build completely bullet-proof solutions, with the error-robustness of Erlang and the individual-component robustness of Rust.

                              1. 4

                                No. Rust prevents data races, not race conditions. It is very important to note that rust will not protect you from the general race condition case. In distributed systems, you’ll be battling race conditions, which are incredibly hard to identify and debug. It is an open question if the complexity of rust will get in the way of debugging a race condition (erlang and elixir are fantastic for debugging race conditions because they are simple, and there is very little to get in your way of understanding and debugging them).

                                1. 2

                                  The parent post says rust has compile time checks for data races and makes no claim about race conditions. Did I miss something?

                                  1. 2

                                    When you are working with distributed systems, it’s race conditions you worry about, not data races. Misunderstanding the distinction is common.

                                    Distributed systems are hard, because consistency is hard. Rust being able to have compile-time checks for data-races is huge in this respect because it allows us to develop small-scale distributed systems with ease.

                                  2. 1

                                    Yes, Rust prevents data races which is (as mentioned by another poster) what I wrote. However, Rust’s type system and ownership system does makes race conditions more rare in my experience, since it requires the data passed between threads to be explicitly wrapped in an Arc and potentially Mutex. It is also generally easier to use a library such as Rayon or Crossbeam to handle simple multithreaded cases, or to just use message-passing.

                                    Additionally most race conditions are caused by data races, so… yes, Rust does prevent a certain subsection of race conditions but not all of them. It is no less a superpower.

                                    It is an open question if the complexity of rust will get in the way of debugging a race condition (erlang and elixir are fantastic for debugging race conditions because they are simple, and there is very little to get in your way of understanding and debugging them).

                                    I don’t understand this point. Rust can behave just like Erlang and Elixir (in a single-server use-case, which is what I was talking about) via message passing primitives. Do you have any sources for Rust’s complexity being an open question in this case? I am unaware of the arguments for Rust’s affine type system is cause for concern in this situation – in fact it is usually the opposite.

                                    1. 2

                                      “most race conditions are caused by data races”

                                      What definition of “most” are you using here?

                                      Many people writing distributed system are using copy or copy on write systems and will never encounter a data race.

                                      Do I have any sources? Yes. I debug distributed systems, I know what tools I use, and ninjaing them into and out of rust is not going to be ergonomic.

                                      1. 5

                                        Just some quick feedback/level-setting, I feel like this conversation is far more hostile and debate-like than I am interested in/was hoping for. You seem to have very strong opinions, and specifically anti-Rust opinions, so lets just say I said Ada + Spark (or whatever language with an Affine type system you don’t have a grudge against).

                                        The point I was making is that an affine type system can prevent data-races at compile-time, which are common in multi-threaded code. OTP avoids data-races by using message-passing, but this is not a proper fit for all problems. So I think an extremely powerful solution would be an affine-type powered system for code on the server (no data-races) with an OTP layer for server-to-server communication (distributed system). This potentially gets the best of both worlds – flexibility to have shared memory on the server, while OTP robustness in the large-scale system.

                                        I think this is a cool idea and concept, and you may disagree. That is fine, but lets keep things civil and avoid just attacking random things (especially attacking points that I am not making!)

                                        1. 2

                                          Not the parent:

                                          In the context of a message-passing system, I do not think affine|linear types hurt you very much, but a tracing GC does help you, since you can share immutable references without worrying about who has to free them. Linear languages can do this with reference-counted objects—maintaining ref. transparency because the objects have to be immutable, so no semantics issues—but reference counting is slow.

                                          Since the context is distributed systems, the network is already going to be unreliable, so the latency hit from the GC is not a liability.

                                          1. 1

                                            Interesting point although I don’t know if I necessarily agree. I think affine/linear types and GC are actually orthogonal to each other; I imagine its possible for a language to have both (although I am unaware of any that exist!) I don’t fully understand the idea that affine/linear types would hurt you in a multi-threaded context, as I have found them to be just the opposite.

                                            I think you are right that reference counted immutable objects will be slightly slower than tracing GC, but I imagine the overhead will be quickly made up for. And you’re right – since its a distributed system the actual performance of each individual component is less important, and I think a language like Rust is mainly useful in this context in terms of correctness.

                                          2. 1

                                            Can you give an example of a problem where message passing is not well suited? My personal experience has been that systems either move toward a message passing architecture or become unwieldy to maintain, but I readily admit that I work in a peculiar domain (fintech).

                                            1. 2

                                              I have one, although only half way. I work on a system that does relatively high bandwidth/low latency live image processing on a semi-embedded system (nVidia Xavier). We’re talking say 500MB/s throughput. Image comes in from the camera, gets distributed to multiple systems that process it in parallel, and the output from those either goes down the chain for further processing or persistence. What we settled on was message passing but heap allocation for the actual image buffers. The metadata structs get copied into the mailbox queues for each processor, but it just has a std::shared_ptr to the actual buffer (ref counted and auto freed).

                                              In Erlang/Elixir, there’s no real shared heap. If we wanted to build a similar system there, the images would be getting copied into each process’s heap and our memory bandwidth usage would go way way up. I thought about it because I absolutely love Elixir, but ended up duplicating “bare minimum OTP” for C++ for the performance.

                                              1. 2

                                                Binaries over 64 bytes in size are allocated to the VM heap and instead have a reference copied around: https://medium.com/@mentels/a-short-guide-to-refc-binaries-f13f9029f6e2

                                                1. 2

                                                  Hey, that’s really cool! I had no idea those were a thing! Thanks!

                                                2. 1

                                                  You could have created a reference and stashed the binary once in an ets table, and passed the reference around.

                                                3. 1

                                                  It is a little tricky because message passing and shared memory can simulate each other, so there isn’t a situation where only one can be used. However, from my understanding shared memory is in general faster and with lower overhead, and in certain situations this is desirable. (although there was a recent article about shared memory actually being slower due to the cache misses, as every update each CPU has to refresh its L1 cache).

                                                  One instance that I have had recently was a parallel computation context where shared memory was used for caching the output. Since the individual jobs were long-lived, there was low chance of contention, and the shared cache was used for memoization. This could have been done using message-passing, but shared memory was much simpler to implement.

                                                  I agree in general that message passing should be preferred (especially in languages without affine types). Shared memory is more of a niche solution (although unfortunately more widely used in my experience, since not everyone is on the message passing boat).

                                        2. 4

                                          I think a good explanation is that K8s allows you to take concepts and languages you’re already familiar with and build a distributed system out of that, while Erlang is distributed programming built from first principles. While I would argue that the latter is superior in many ways (although I’m heavily biased, I really like Erlang) I also see that “forget Python and have your engineering staff learn this Swedish programming language from the 80ies” is a hard sell

                                          1. 2

                                            You’re right, and the ideas behind K8s I think make sense. I mainly take issue with the sheer complexity of it all. Erlang/OTP has done it right by making building distributed systems extremely accessible (barring learning Erlang or Elixir), while K8s has so much complexity and bloat it makes the problems seem much more complicated than I think they are.

                                            I always think of the WhatsApp situation, where it was something like 35 (?) engineers with millions of users. K8s is nowhere close to replicating this per-engineer efficiency, you basically need 10 engineers just to run and configure K8s!

                                        1. 11

                                          I just skimmed the tutorial (so may be missing something) but it seems like many of the benefits can be reaped using a workflow I often use when working on sets of unrelated changes simultaneously:

                                          Say you are making commits related to subjects A, B, and C (maybe A is “formatting cleanup”, B is “adding feature X”, etc).

                                          1. Move among A, B, and C freely, adding commits (using git add -p if needed) with subject lines like B - add new class or even just A.
                                          2. When finished with your work, git rebase -i to put commits from A, B, and C together, using fixup. You may end with only 3 commits, or you may have a few more if you want to keep some commits from the same theme separate for whatever reason.
                                          3. During the interactive rebase use reword on the final commits as needed to write the final draft commit messages, which will now reflect the melded “fixup” history of the combined commits.

                                          This lets you work without worrying while you’re in flow, while retaining enough context to easily and quickly clean things up at the end.

                                          1. 3

                                            Agreed. And being able to work like this was a major reason why I (and I suspect others) left Mercurial for Git.

                                            I used to use a “patch stack plugin” for Mercurial as well, but two issues that kept cropping up was that:

                                            1. Since you weren’t working with real commits the tool support was lacking
                                            2. Since you hadn’t actually committed anything you couldn’t push stuff to a WIP branch just so you could have a backup somewhere

                                            There was a recent post on here that also took similar issues with Git’s workflow but I never understood why committing as you go and fixing up later wasn’t a viable solution. But maybe I’m just too set in my ways.

                                            1. 1

                                              I was going to say something like this. You can also use —fixup to appropriate commits to one topic.

                                            1. 10

                                              My usual git workflow is sort of like what the author describes but I use git add -p and consider the entire branch a working draft until the changes have been made public. Commits can usually be changed quite painlessly with —fixup or —amend so I don’t fret about getting them right the first time

                                              This also allows me to easily undo a set of related changes while I’m working by deleting commits, which I find much easier than hunting down the relevant changes manually or attempting to “undo” back to a desired state.

                                              I haven’t found that this breaks my flow; I guess I consider staging changes part of the workflow, just like opening a file or editing a buffer. I see how it could be disruptive if your editor of choice doesn’t have git integration so you have to context switch every time you want to stage a change. (I’m using Vim + Tmux)