32 comments

[ 4.3 ms ] story [ 65.3 ms ] thread
> 90ms is a 90th percentile response time on The Outline. Our post page route is even faster! We got this performance out of the box

That's one of the features you get when using BEAM VM -- your code will likely be scalable out of the box as it's already built out of isolated independent small processes. If you want to handle more requests just run on a bigger machine.

> The Community is wonderful!

I use Erlang mostly but one major thing I admire about Elixir is it's community. Jose and team did a great job there. So it really has both - the great technical stuff, built on BEAM VM which was battle tested for decades, and the great community and tooling.

> As soon as you get data from the external world, cast it into a well known shape.

Good advice. Have sane validation at the edges and then convert data to some internal format that's easy to handle. As opposed to defensively sprinkling validation conditionals everywhere in the core code. That's makes the code cleaner and easier to reason about.

When working on V2 of our architecture we tried out both Elixir and Go extensively. For our workload Go was roughly 8 times faster than Elixir, while both handling concurrent workloads exceptionally well. (bit of background on the type of work we do: https://stackshare.io/stream/stream-and-go-news-feeds-for-ov...)

That being said, I'd love to find an excuse to work on a little project with Elixir and Phoenix. Some really cool concepts. Especially the build in support for realtime is really nifty. With Go that's much more work to get up and running.

I can't say I'm an expert in go - I only wrote the cli for my program in go, but I feel like elixir optimized for developer productivity far more than go does. Most of the time you don't really need that level of performance.
You're going to find that for workloads that require churning through requests, or something similar, that Go or Rust or something similar is simply better. Elixir/Erlang is mostly good for dealing with a lot of persistent state, staying up and running, and reliably getting data from point A to point B. But none of those it does particularly fast, unless you can do it without needing to mutate a data structure/variable, or you can take advantage of the fast binary pattern matching.
> consider going old school and rendering your HTML on the server; you might be delighted

One idea I'd wish more sites would consider.

Yeah, writing about using Elixir at The Outline is cool and all, but let's not forget this place wiped out most of its staff, including developers.
I'm bullish on Elixir and I agree that ExUnit deserves to be singled out.

Whether you consider Elixir fast or slow probably depends on what you're coming from and what you're doing. Personally, I wish the performance story would significantly improve.

But, the reason I think Elixir should be your goto language comes down to process isolation and the ecosystem around it (the synergy of the runtime, libraries and language). If you're doing a pure web app, you might not fully leverage it. But, for anything that involves long-lived in-process data (like a workflow/pipeline, or persistent connections), the way to approach this in Elixir (with supervisors, processes, message passing and pattern matching) tends to result in highly cohesive and maintainable code. Message passing is a pretty effective antidote to coupling.

I just finished writing code that had to take a stream of unordered time-based data and dedupe it over a small window. I'm not overly pleased with the final result, but it's completely isolated from the much larger app it sits in, easy to refactor/rewrite, with no risk of someone taking a shortcut and just accessing some internal detail, and testable.

I feel like I wrote a microservice, but without any of the challenges of a separate app.

I can see the logic behind the statement 'message passing is a pretty effective antidote to coupling' but I personally subscribe to the idea that actors don't compose. In light of that I think use of actors and messages implies some level of coupling which may not be obvious. Actors can't be arbitrarily composed together without knowing which actors they will be interacting with, making their addresses available and understanding their protocols so they can communicate together. Functions have no such requirements so I don't totally understand how message passing implies less coupling than conventional function-based architectures
Message passing is less coupled than functions or "objects" that share bits of state.
You can use functions without knowing their inputs and outputs, and your functions don't need environments that somehow allow them to lookup the other functions they're going to call?

Messages and function calls aren't quite isomorphic, but they're pretty close. (They get really close if you include coroutines and coroutine-like things, to cover the non-1-to-1 relationship between messages and replies that messages can have.) I'm not sure the difference can sustain the implication you're trying to draw. Especially if you include RPC in the function world.

I think you're assuming side-effect free functions (and apparently ones whose interfaces are automatically known, since the interface to a function is just like the message handling in an actor; in fact, Smalltalk, the original OO language, didn't have 'methods', it had 'message passing'). But, you can still write libs and share them in Elixir, to benefit from any argument you'd care to make re: functions.

Actors are all about managing state. It's when state starts leaking around or being modified unexpectedly ('wait, why is that being called there?!') that a more typical language (Java, say) starts to get tightly coupled, and sharing that state is the norm, and expected. And I agree, if you have pure functions, that issue is largely controlled for too. Just, that's a really hard paradigm to stay inside of and still be productive.

I think that's why microservices are even a thing; it forces you to decouple state from different 'functional' parts of your system; by creating separate systems you have to be very intentional about what state you share, and how you share it (database or redis or an API call or similar depending on need).

Actors do that automatically for you; the state is contained within them; there's no chance(1) it will accidentally be changed by some other process/code, and the 'controlling' process gets to decide what messages affect changes, and how those changes occur. Or, in other words, all state changes relating to a context happen in one place, rather than state being passed around to be changed anywhere willy nilly across contexts.

1 Technically there are system level things that allow you to munge state inside of an actor. This is more frowned upon than using reflection for arbitrary reasons in Java, however, so the only reason you'll ever do it is because you're debugging what's going on in something, or you have decided you absolutely -hate- your toes and need to footgun them off.

You can certainly achieve the same low coupling with anything. But ....

Messages tend to be lightweight. Often a symbol/atom "command", and a few scalar values. Like command line arguments. If the process needs more data, it sends its own message to the owner of said data.

With conventional functions and OO, you're often dealing with complex objects (with references to references ...) which often leads to breaking the Law of Demeter.

Message passing also gets your asynchronicity. You're also decoupled from specific runtime implementation (the receiving can be on a different machine, the semantics are the same). You can achieve the same without message passing, but it's certainly more baked-into the runtime/tooling/language.

Joe Armstrong's popular quote on this:

"The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle"

> I feel like I wrote a microservice, but without any of the challenges of an separate app

You feel that way because you did :)

I keep wanting to move to Elixir but always go back to Erlang. Any advice?
Speaking as someone more into Elixir than Erlang, I'm wondering why you want to move to Elixir at all? If you're happier in Erlang, then why not stick with that? For me, the best thing about Elixir is the BEAM, and you've obviously already got that. If you don't want or need metaprogramming, or the various Elixir conveniences feel less than convenient to you, it seems like you may as stick with what you find comfortable.
> Personally, I wish the performance story would significantly improve.

Before asking for performance improvements, have you tried:

1. tuning the BEAM? It has a lot of knobs. For example, there's "initial heap size", which can effectively turn any subset of short-lived processes that would otherwise need to do GC into simple "allocate an arena, do work, deallocate the arena" workloads.

2. tuning your OS? WhatsApp had to tune their (FreeBSD) kernel quite a lot to get it delivering packets to the Erlang runtime efficiently.

3. disabling tracing support? Part of the reason ERTS focuses on being a bytecode interpreter, rather than an AOT compiler, even in production, is that this allows ops engineers to connect to production nodes and trace/debug/profile code that's not doing what they'd expect, watching what happens as real user data flows through it. Because of this, the interpreter is "instrumented" even when a "production" (embedded, etc.) profile is used to generate a release. If you aren't going to use this tracing support, though, you can tell the runtime to globally disable it—for big wins in interpreter speed. (I separate this from #1, because this is basically the last-resort BEAM knob to turn. You're throwing away a lot of cool stuff by doing this.)

3. using HiPE? Specifically, sticking `@compile {:erl_opts, [native: :o3]}` into a subgraph of your modules (like a parser, a lexer, and the code that calls them both) can yield major improvements.

4. really using HiPE? I.e. recompiling both OTP and Elixir, and your project, using `ERL_COMPILER_OPTIONS="[native,{hipe, [o3]}]"`. This makes some things slower, so it's not the default, but it can beat #3 for modules that make a lot of stdlib calls (e.g. anything that loops using calls to Enum/:lists.)

4. using NIFs? (I don't recommend going this route until you've found that all of the above fail to get you enough performance, because the above all let you continue to just write plain Erlang/Elixir, while this step requires worrying about FFI. But it can be a big win, where and when it makes sense, just like in any other interpreted language.)

If your Erlang code is still slow after all of those, then I'll be surprised and would like to hear your specific story.

I would like to use Elixir more at work, I'm hoping that we'll be able to use it for some new projects. I love the language and the community.
Can you print an example of the random test error? I run a decent sized phoenix project (150K lines) and likely can help you out if you give a bit more detail.

With regard to associations, for simple belongs_to associations I will usually write separate changeset functions where the first argument is pattern matched against the record. I will give an example with Post/Comment

    defmodule MyApp.Blog.Comment do
      def changeset(%Post{} = record, params) do
        record
        |> build_assoc(:comments)
        |> changeset(params)
      end

      def changeset(%Comment{} = record, params) do
        record
        |> cast(params, [:body])
        |> validate_required([:body])
        # Continue with validations, constraints, etc.
      end
    end
In this case the changeset function calls itself once it handles building the association. For more complex conditions, I will usually write a "Form" module that may handle that case usually with a Enum.reduce combining multiple calls to `put_assoc` so that I can make nested associations in one insert.

Hope this helps a bit.

>>def changeset(%Comment = record, params) do

Small typo: should be %Comment{}

> 90ms is a 90th percentile response time on The Outline

How much of that is the DB, and what DB are you using?

I like the BEAM environment quite a bit, myself, and have used it successfully in several semi-embedded environments, where it really shines because of its robustness and reliability.

- Ecto is still a climbing hill for me. In fact I'm currently trying to warp my head around changesets and associations.

- Same for GenStage. I wanted to use them for workers pulling from an episodic queue but the docs with the emphasis on consumers pulling multiple events at once from a never ending stream didn't really speak to me. Tailoring to my usage looked verbose: storing demand etc. I'll have to revisit when I'm better at Elixir.

- I still think caching should be part of Ecto in some way, at least in the docs.

- I would still like more straightforward template inheritance in Phoenix (think Jinja2, Twig).

Excited for the future releases of Phoenix 1.4 and Ecto 3.

As a long time Erlang developer I was suprised when I first heard of Elixir, and even more surprised at how nice it was.

That said I think Elixir is very unerlangish. It’s very much Ruby ported to a new platform, it’s heavy on meta programming with macros and using a giant solve all your problems web framework (Phoenix). Compared to Ruby on Rails I think it makes a lot of good arguments. It’s faster, mix is a better package manager, and the BEAM is reliable at small to medium scale.

Of course I recently posted about how I was moving away from the BEAM to Rust. I think Elixir has reached peak adoption, and I think the days of large frameworks like Rails and Phoenix that ask you to program in a DSL are sunsetting in favor of client side frameworks and smaller web services built off of thinks like Go’s net/http.

I also think the reliability of BEAM is happening at the level of orchestration with containers and serverless. The BEAM itself doesn’t fit well into the cattle model of reliability and it has scalability issues when you reach a certain cluster size.

Erlang is also quite slow at processing data and performing computations. What it does provide is reliable latency and load as you scale, which lets you grow your build mess quickly without buying more hardware or performing software miracles.

I’m more optimistic about Rust because it provides the reliability I’ve come to expect from Erlang. We’ve been able to scale Rust to handle the same load as our Erlang cluster with a more manageable code base, lower average client latency, and lower memory usage on the same number of machines.

I also know that people like to treat dynamic vs static typing as a matter of taste, but in my experience dynamic typing only works until you reach a certain level of scale. I’ve watched several python and erlang codebases turn into incompressible messes because of this. This is why we’re seeing things like typescript and mypy become requirements rather than nice to have. So I’d rather just bite the bullet and get a really nice static type system up front.

>>I think Elixir has reached peak adoption, and I think the days of large frameworks like Rails and Phoenix that ask you to program in a DSL are sunsetting in favor of client side frameworks and smaller web services built off of thinks like Go’s net/http.

Is this just a hunch or do you have numbers to back it up?

edit: This is the second Elixir/Erlang thread you have attempted to hijack by spreading FUD about them and pitching Rust as an alternative. You come across as a Rust shill.

You don't have to use Phoenix... I'm currently doing a plug-only deploy and it feels great.

> I’m more optimistic about Rust because it provides the reliability I’ve come to expect from Erlang.

Im very curious about rust myself, Out of curiosity, how does Rust protect you against wierd things happening like errors in drivers, or dropped packets, e.g.

Phoenix is really a very small framework, and its codebase can be easily understood in just a couple of days. Phoenix being "large" is a common (but unfounded) criticism that seems to stem from Elixir's superficial resemblance to Ruby.
I’ve read the book on it, and I disagree. It’s certainly its own platform and you’ll have to deal with its ups and downs and learn its DSL.
The only way Elixir resembles Ruby is the syntax. Everything else is totally different.
I mean, I read the codebase, which is why I'm saying it's small and easily understood. Sure, it has some specific things to know, and some ("magical") helpers like automatic view functions. But it's no more its own platform than any other micro-framework.

I'd be interested to know what you believe the ups and downs are, aside from speed.

> "I think Elixir has reached peak adoption"

I'd say the opposite. Core libraries have reached stability and engineering hours are switching to making easy what classic imperative/OOP paradigms have trouble doing:

- Dynamic web development without giant Javascript dependencies(Liveview, Drab, etc)

- Scalable distributed systems without giant teams (Firenest, Phoenix Channels/PubSub)

- Concurrency and data-infrastructure (Flow, Genstage, OTP)

- Reactive event-driven systems: OTP and the Actor model makes event-sourcing easier than you'll see anywhere else.

- Usual conveniences: User-management, HTTP clients/libraries

The next 2 years are going to get wild for Elixir.

Does anyone miss or want type checking when working with Elixir? After working with Ruby to build large systems for so long, I'm missing a little bit of the safety that types provide. Typescript has been great.
Agreed. I used to remove parenthesis as often as possible. Looking back at the code, that may not have been the brightest idea.

I really do like the coding convention though that lack of parenthesis can indicate a function meant to be a part of a DSL.