1. 36
    1. 15

      Neat writeup, a couple of thoughts.

      It caused some weird cognitive dissonance. Why do I feel vaguely strange hanging out online with all these kind, knowledgeable, friendly and compassionate techbro’s?

      It’s almost like a it’s a space where the primary concern is passion for the technology, and not other identity stuff. I personally think that’s a good thing. I think it’s also maybe more important to note the wide representation outside of the anglosphere for Elixir–it’s markedly less US-centric than other communities I’ve been involved in.

      Erlang/Elixir’s vaunted reliability only kind of involves the language itself.

      Incredibly important observation! I’ve worked on a range of Elixir code (and other code), and the language itself only perhaps gives a nudge towards reliability–it doesn’t (and no language really can) magically solve it for you.

      I avoided Phoenix LiveView with a passion, but I probably shouldn’t

      It’s been around for for a minute, and is strictly better (in my experience, production and personal) than the alternatives. There are a few rough edges, still, but it’s been a trooper and has convinced me that SPAs in the style of the twenty-teens are basically just a waste of time outside of a couple of very niche cases.

      <with clause example>

      I too dislike that part of with, but for the specific example the old cond clause is pretty much just what the doctor ordered:

      cond do 
        not is_email_address?(email) -> {:error, :bad_request}
        not EmailAddresses.is_available(email) -> {:error, :conflict}
        true -> :ok
      end 
      

      Of course, that’s off the top of my head, so run it and see if I’m right.

      Sure the state is all encapsulated into processes, but then those processes are hidden behind an abstraction layer that makes them invisible, so really you’re just touching global variables.

      Welllllll, sorta.

      That idiom of “process own state, talk via messages” is very helpful for mocking and testing. It also lets us use neat tools like rexbug and recon_trace.

      There’s just too many heckin’ ways to import a module.

      Preach. And, the author doesn’t even mention the alias Foo.{Bar, Baaz, Quux} form, which makes grepping a pain in the ass.

      We could probably drop import and require and get by with just alias and use, tbh.

      ~

      Anyways, good writeup.

      1. 11

        It’s almost like a it’s a space where the primary concern is passion for the technology, and not other identity stuff.

        The author says:

        …the Rust user-base continues to be a fascinating case study in how many weirdos you can get together in one place when you very explicitly say it’s ok to be a weirdo.

        …which I think is contrary to the idea that inclusivity is a secondary concern. The author is saying that because the Rust project made inclusivity a primary concern, the ecosystem is full of weirdos, and that’s great because it’s an explicitly welcoming place.

        1. 13

          I’m glad it works for the author and the Rust ecosystem; I rather prefer technical spaces to be technical spaces first. Making a space first and foremost to be inclusive and safe for weirdos is great if you’re the right type of weirdo, but if you’re the wrong type of weirdo or you care about talking to people sorted on skill rather than weirdness it can be suboptimal. If that’s your preference, hey, more power to you–personally, it’s not my thing.

          Happy to chat over PMs about this further if you’d like, but continuing here risks sucking the air out of the thread.

          1. 2

            Ah, I see, I misinterpreted your initial paragraph as if it was what you took away from the article. I was offering a different interpretation, but you were actually making a value statement about the two communities.

      2. 7

        We could probably drop import and require and get by with just alias and use, tbh.

        We did something similar in Clojure; older code had a way to bring in “whatever’s in this other namespace; just dump it all here unqualified” with :use vs “bring in things from this other namespace under a prefix” with :require, and in version 1.4.0 the former was deprecated, making it a lot more consistent and clear. Of course, that happened relatively early on (2012, so only 4 years after the initial release) whereas with Elixir it’s probably too late to deprecate such a core thing.

      3. 5

        It caused some weird cognitive dissonance. Why do I feel vaguely strange hanging out online with all these kind, knowledgeable, friendly and compassionate techbro’s?

        Semi-tangential, but I don’t get what makes the Elixir community “tech bros” (though of course there might be some further context to them I’m not aware of). I thought “tech bro” referred to a kind of behaviour and cultural value set (the one that’s most commonly expressed on eg. the orange website), not just straight male programmers.

        (If this tangent risks draining air out of the conversation, feel free to ignore or send a PM).

      4. 1

        It’s been around for for a minute…

        Oooh, thanks, that article is really good. I’m slowly more and more convinced that this basic model is pretty much how web browsers should actually function, at least in terms of interactive applications.

    2. 5

      I never had problem with Elixir’s way of error handling, though the with expression proved to be limiting in a few cases for me.

      Which is why I wrote this : https://github.com/linkdd/rustic_result It provides an API similar to Rust’s Result type around the {:ok, val} | {:error, reason} tuples.

      It’s the combination of and_then and or_else in a “pipeline” (aka: using |> that was missing from with, which assumes every steps succeeds.

    3. 5

      Nice post … I got excited about Elixir last year, reading most of a book about it, checking out several other books.

      But I never ended up doing anything with it, mainly because of lack of time, but also because a few things did rub me the wrong way.

      One was early returns (mentioned in this post) and actually I brought that up on the comments to the first post (which was also good) - https://lobste.rs/s/rsxxbh/elixir_for_cynical_curmudgeons#c_rn4crd

      The point about global mutable state is a huge pet peeve of mine. I think “functional” really means at least a few things

      1. You use expressions and recursion, more so than statements and loops. As noted in the comment, I don’t really care about this. What I care about is the function’s signature. If you write a bunch of loops and state inside a function, it can still be pure from the user’s perspective. That’s what matters IMO. And it’s probably better because it’s faster and generates less garbage.

      2. State is parameterized. Again this is about the signature of the function. It seems like idiomatic Elixir lacks this, which is deeply disappointing to me. I literally can’t write big pieces of software without state parameterized everywhere (and usually I/O too).

      It doesn’t scale for my brain otherwise. I use plain old objects to manage state. I invert my dependencies by hand. :-/

      I honestly think that plain Python code where all app state is parameterized is more “functional” than Lisp or Elixir code with lots of expressions and recursion, but global mutable state. It has more of the benefits of functional programming, at least for me.

      I don’t care about the style of the inside of the function (or really I prefer mutation there) – I care about the connections between the functions, in-process state, and I/O

      So yeah it was hard to for me to imagine writing big pieces of Elixir code … which is disappointing because Python concurrency is kind of a messy accretion of different styles. I get that other people do it, which attracted to me Elixir, but I guess I have limited brain space and can’t deal with global state anymore


      Now I honestly wonder what happened with “Stackless Python” I used to hear about circa 2009 … It was green threads in Python. I think it changes the Python/C API and was hard to merge? But there were real services deployed in it (Eve online seemed to be a main sponsor) and I think it had a coherent and simple concurrency model.

      (edit - looks like the fork is still maintained - https://github.com/stackless-dev/stackless/wiki/ . Now I remember that “greenlet” was the C extension that did scary non-portable things to make stackless work with stock CPython? But there’s still a fork. It does seem niche but people have built real things with it, for a long time, and it may be easier for my brain, since I already use too many languages. I’m not really on board with asyncio either …)

      (another edit - PyPy has some stackless features - https://doc.pypy.org/en/latest/stackless.html )

      1. 3

        Funnily enough, it is the opposite for me. Elixir worked for me explicitly because that function signature is what i am not smart enough for.

        Mostly because function signature are a big lie in a concurrent environment. Elixir (and Erlang) manage the global mutable by… Not having one.

        Every state is fundamentally local and immutable. Remote interactions with it has to be through messages, that are async. This means that you get true encapsulation and independence.

        Which in returns means you (mostly) don’t have to care about it other than yours. The exception is for when you are truly dependent, but even in this case, the dependency and handling get moved to a 3rd party, the supervisor. So the code can be independent of it.

        Said otherwise, you end up optimizing for the ability to refactor. You can delete a whole module and replace it, and your boundary is naturally still there. I can rewrite whole parts without touching the rest. Or i can kill them live and restart them as needed.

        Context is always limited to what you got as input variable for your function and thing you explicitly call to receive. No mutation, no change under scope. (Some limited exceptions exist here, but they are explicit and marked with dragon’s warnings)

        So it is not that Elixir/Erlang solve the problem like you ask for. They totally sidestep it.

        1. 3

          Elixir (and Erlang) manage the global mutable by… Not having one.

          They absolutely do. It is kept in the process registry and Erlang Term Storage. The actual code is the first genserver anyone ever writes.

          defmodule Cell do
            use GenServer
          
            @impl true
            def init(state) do
              {:ok, state}
            end
          
            @impl true
            def handle_call(:get, _from, state) do
                {:reply, state, state}
            end
          
            @impl true
            def handle_cast({:set, newstate}, _state) do
              {:noreply, newstate}
            end
          
          
            # Interface for interacting with a cell
            def get(name) do
              GenServer.call(name, :get)
            end
          
            def set(name, val) do
              GenServer.cast(name, {:set, val})
            end
          
          end
          
          {:ok, _pid} = GenServer.start_link(Cell, 3, name: :totally_not_global)
          
          IO.puts(Cell.get(:totally_not_global))
          Cell.set(:totally_not_global, 4)
          IO.puts(Cell.get(:totally_not_global))
          

          There, now anyone who knows the name :totally_not_global can access this value. And because people usually don’t like rewriting that, you’ll have functions that hide that name and just do operations on its contained value. Yes, you can log and trace the messages actually sent, which is pretty damn important. But you now have tons of function calls which mutate shared state which is totally invisible in their function signature. You just have to hope that the docs are good, or read the source.

          And this is incredibly common. Ecto does it to store its database connections. ExAws does it to store its S3 configuration. Oban does it to store its job queue. You entirely lack the golden (and infuriating) property of Rust and Haskell and stuff where if a function modifies something, then it has to be passed that thing as an argument. (All those languages have ways around it, but they all use it far more sparingly than Elixir.) “Encapsulation” means “If I call do_stuff(a, b, c) then it only touches a, b and c.” Elixir totally fails at this compared to other languages the moment the process registry or ETS becomes involved, because those are explicitly there to let any process send messages to any other process.

          1. 1

            Yes thanks, and actually I remember this from 2008 when I bought the Programming Erlang book. I have been Erlang/BEAM-curious for a VERY long time.

            But this just doesn’t fit the way I write software. Testing is essential. You noted in your post that testing feels weird, and I think this is probably a big reason why.


            Compare this with:

            How I write HTTP services in Go after 13 years - https://grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years/

            https://news.ycombinator.com/item?id=39318867

            A lot of it is oriented around testing, and it just makes sense to me. I’m not this guy but he captured my thoughts exactly:

            https://news.ycombinator.com/item?id=39320129

            I don’t write go, but I like these patterns. Feels fairly universal for testable code.

            I never want to see another (esp. Python) Quick Start guide that treats dependencies as implicit/static/untestable.

            That’s exactly the problem with Elixir – the dependencies are implicit/static/untestable.

            It’s not a surprise to me that newer languages like Rust and Go are thoroughly object-oriented. That sounds unfashionable to say, but it’s true.

            You pass your dependencies around with objects that conform to an interface. If they were like Elixir, they would be weaker.

            Elixir and Erlang feel “old” in this respect.


            Now I will even try to “steel man” this and say – you don’t need in-process testing, you can use shell-style “exterior” testing.

            I do a lot of this, and it’s often better because you’re testing against stable interfaces, not the “made-up” interfaces of some web framework.

            But then the Elixir interpreter seems to start in ~300 ms, where as Python starts in 30ms. So Elixir is worse for this kind of testing too.

        2. 1

          Hm I’m having trouble parsing this … What’s an example of a good / big Elixir codebase that exhibits these properties?

          1. 1

            Uhm. All of them would be my claim, as this is something the language kinda force on you.

            The problem is more that we do not have a lot of large and open elixir codebases.

            You can try the Oban one i guess https://github.com/sorentwo/oban

    4. 5

      It’s interesting to separate this out into the category of “things Elixir inherited from BEAM” vs “things it just decided to do differently from Erlang just to be different”.

      I think the “Error Handling” section mostly falls in the first category; this just comes with the territory, and you have to deal with it because of the runtime. But the “State Management” bits about the process registry and the “Imports” section fall in the second category; Elixir could have just Made Better Choices and avoided these problems. The “million ways to Import” in particular (and the abundance of syntactic sugar in general) feels like the unfortunate influence of Ruby.

      1. 2

        Sadly no, it is the fact it is a lisp 2. This generates problems at compile time if you do not separate things.

      2. 1

        would it not be possible to overlay something like the questionmark syntax at the syntax level? I don’t believe that there’s a lot of type-based inference required to make that work (beyond “you called question mark on a thing that doesn’t return a result”)

    5. 4

      because Elixir has no early returns, you literally can’t write a macro like Rust’s ? or try!() which is a single expression that bails early. (Exercise for the reader: prove me wrong.)

      Not quite going to prove you wrong but I did write a macro some 5 years ago ex_early_ret which is a very limited form of early return—it doesn’t work in nested blocks—but it’s still the best way I’ve found to write long sequences of checks.

      1. 1

        Yeah I got halfway through something similar before realizing that it would never work in nested blocks, and gave up. Alas.

    6. 4

      reading this, I agree and ended up not finishing my project being lost in the weeds. Especially around umbrella projects and supervision trees.

      I do want to come back to an Elixir project, but maybe instead of Phoenix use something that’s akin to Ruby’s Sinatra or Python’s Flask.

      1. 15

        Easy!

        from here

        #!/usr/bin/env elixir
        
        Mix.install([{:bandit, "~> 0.5"}])
        
        defmodule Router do
          use Plug.Router
          plug :match
          plug :dispatch
        
          def run do
            Bandit.start_link(plug: Router)
            Process.sleep(:infinity)
          end
        
          get "/" do
            send_resp(conn, 200, "HELLO")
          end
        
          match _ do
            send_resp(conn, 404, "Not found")
          end
        end
        
        Router.run()
        
    7. 3

      Thanks for writing this up! Not sure if whoever posted this on lobste.rs is the author, but I’d be curious to know their thoughts on what web frameworks (or libraries) they like to use in Rust. I myself was skeptical of Rails and Rails-inspired frameworks (like Phoenix) ever since I started out as a PHP programmer. But they won me over as the years passed and now I’m sad whenever I write a web app in a language without a real equivalent, notably Java and JavaScript.

      If there are some good alternatives for writing web apps in Rust I’d love to try them out. I tried Actix a while ago and it seemed more geared towards APIs than web apps, but it was a while ago.

      1. 2

        Unfortunately (for you) the last time I dug into the topic was in 2017. I usually don’t want a Rails/Django-style big powerful framework anyway, and tend to avoid projects where they’re the best solution.

        That said for a web app I’d probably reach for Elixir and Phoenix before anything Rust, despite the nitpicks. Not having to roll your own monitoring, logging, clustering etc. is a pretty big win.

        1. 1

          Thanks so much for the reply!! Unfortunate for me indeed! Wow, that is a comprehensive review, thanks for sharing. BTW I think the idea of having a public personal wiki is super cool.

    8. 2

      Let it crash makes sense if crashing means dropping a phone call. It works well for supervision trees. But let it crash code can often obscure what’s going on - see abuse of with blocks.