Elixir and Phoenix after two years
nts.strzibny.name
nts.strzibny.name
As he says, the templates are compiled and are blindingly fast compared to Rails.
Pattern matching is really really nice when used in the right places (and you'll miss it if you go back to Ruby); but it can be overused. There's a faction of Elixir folks who attempt to avoid all conditionals and instead seem to prefer multiple dispatch/multi-methods to handle different cases. That's nice and very concise, because then you can simply call a function and let the pattern matching resolve which of the various implementations you've defined handle it. The big downside here is, as a reader of the code, you have to basically mentally imagine what all cases are covered and what they mean. Sometimes simply reading a switch statement or if/else/then is much clearer.
The super special magic is in the Erlang VM. If you put more energy into learning it and its capabilities, and using it where appropriate, it can shape the structure of your greater system beyond just one webapp; and it can provide a lot of features without you having to cobble together many other (good but independent) solutions.
Lastly, single thread performance is basically a dog. In my anecdotal experience, the same external service written with Elixir+Ecto was 25-50% as performant as a Python+SQLAlchemy program. So the lesson there is, find ways to parallelize or otherwise scale your process if it is batch oriented and handling a large volume of data.
If you asked me today if I would prefer to use Elixir (and Phoenix) over Ruby and Rails, I would say yes... but honestly mostly just because it's a new fascination with different tradeoffs and a better functional story. Function is the past and the future, and it makes your life easier and simpler. Elixir as a language... borrowed too much from Ruby and has too much syntax. It is noisy in a Perl-like way, and perhaps there could be a more concise enhancement of Erlang which would get the job done and not have you spending time visually parsing code.
You can do a ton with it without ever importing sqlalchemy.orm.
I'd be interested to see an example here. Using Plug+Ecto has been roughly on par with most real world examples I've seen with SQLALchemy and Flask for single request performance. Python might be a bit faster for some workloads but once you start saturating the CPU, Elixir/BEAM really shines.
The case was a recurring calculation system which would take a few hundred thousand records and do about a dozen different calculations against those (some calculations requiring additional lookups).
The original script was in Python, and then after the new webapp was built in Elixir, I rewrote it also in Elixir. I discovered that even tuning the batch sizes, the Elixir/Ecto version was much slower than the Python version. Then I looking into per-thread Elixir performance and found out that that is not its strong suit :). So the answer there of course is to leverage the strength of the tools and parallelize it; but I had no need to do that since I already had a working Python version. Could Elixir have been faster if multi-threaded? Probably. Single? No, I doubt it.
The real gain here would probably be streaming chunks of rows out of ecto and maybe firing those off to tasks per CPU core. Which is easier than it sounds.
That said, you had working code so no real reason to rewrite it except as a learning exercise.
Taking some time to fiddle with the startup settings for a short running task can make a big difference. You might actually get better results by setting it to single cpu, because there's less setup that way. Really depends on how much total cpu time your work takes. If your VM is going to run for a year (or more), startup time hardly matters and so the defaults aren't highly tuned for a 30 second script.
Is that still the case? I thought BeamVM recently added JIT? I don't expect it to be LuaJIT or JS V8, but Ruby and Python Single Thread performance should be reasonable expectation?
I also wish some of these experience has more context in terms of code base size and team size. A Production environment of a small project with a team of 2 is very different to production environment of a project that is large and team of dozens if not hundreds.
Are you sure you weren't just doing something sub-optimal with Ecto?
[1] https://amp.reddit.com/r/elixir/comments/46v1l1/is_elixir_fa...
One specific case that cripples Elixir - is as far as I know Elixir can't handle large multiplications efficiently. Ruby, Python use karatsuba or some derivative - from what I can tell multiplication in Erlang doesn't. Though I haven't actually asked anyone and was just poking around on my own.
example: Multiplying the first 100_000 numbers together I get
ruby multi.rb 3.47s user 1.91s system 99% cpu 5.396 total
elixir multi.exs 14.32s user 0.62s system 103% cpu 14.441 total
This shows a significantly different story between the two with at least similar code. Note I am not trying to optimize for the fastest factorial here as both can be made much quicker but roughly equivalent code seems to tell a different story for me.
(NOTE: I put the code in a module as iex interpreted expressions on the top level are always slower than compiled modules, even when pasting them into iex. Doing the same for Ruby didn't show any difference for me.)
It's included in the not yet finalized release that is currently only a release candidate. It's a little early to expect people to have experience with it. Some organizations might update quickly, but others will take quite some time. Also, the JIT isn't on all supported platforms; i think it's amd64 and aarch64 only at the moment, which probably covers most performance oriented servers, but maybe not everyone.
The other thing is it's not an optimizing JIT like Hotspot or v8; it 'only' turns the beam opcodes into native code as it's loaded. This eliminates interpretation overhead, but there's potential to optimize the native code in the future.
I think that's a symptom of the lack of a return keyword (and therefore early returns).
Erlang already is a more concise language than Elixir but also noisier as there are more punctuation characters. I would love if Erlang would drop some of its punctuation, as some of it is frankly unnecessary.
Elixir syntax is definitely simpler than Ruby’s. Probably in the same ballpark as Python complexity wise: Elixir has less keywords and less rules thanks to the macro system but on the other hand more affordances, such as optional parenthesis.
What?
Here's more-or-less the entirety of Erlang's "noisier syntax with more punctuation characters"
-spec f(A :: any(), B :: some_type(), C :: list()) -> any().
f(A, {B}, [C | _]) ->
A1 = fun() -> io:format(a, []) end,
A1(),
case C of
<<1, _/binary>> -> 1;
_ -> #person{ok = ok, field = C}
end.
Edit: %% plus map syntax
#{"tuple" => {1,2}}
M#{"key" := "new_value"}
%% plus list comprehensions
[X || X <- List, X /= 10].
Elixir has all that plus more. The equivalent in Elixir is something along the lines of @spec f(a :: any(), b :: some_type(), c :: list()) :: any()
def f(a, {b}, [c | _]) ->
a1 = fun() -> IO.inspect(a) end,
a1.(),
case C do
<<1, _ :: binary>> -> 1
_ -> %Person{ok: :ok, field: c}
end
end
## and don't forget the capture and pipe syntax
x |> (&f(y, {&1}, list)).()
## and default function parameters
def f(a, b, c \\ []), do: something()
## and sigils
String.replace(str, ~r".*", "")
## and Ecto adds its own characters
id = 1
(from p in Person, where: p.id == ^id, select: p) |> Repo.all()
## and dropping down to Erlang is a function call on an atom
:io.format(x, y)
And I'm definitely forgetting more...Edit:
## and string interpolation
a = 1
s = "The value is: #{a}"
## and two different map syntaxes
%{a: :map, with: "various", keys: "as", atoms: 0}
%{"another" => "map", "but" => "keys", "are" => "strings"}
## and different map access
map[key]
map.key
## and map update
%{ map | key: "new_value" }
## and list comprehensions
for x <- list, x != 10 do
endThe equivalent of "hello world" in erlang is
<<"hello world">>
Not the best. Keyword lists in erlang are unpretty m:f(value, [{foo, <<"bar">>}, {baz, 47}])
Where in elixir you would do: :m.f(value, foo: "bar", baz: 47)is "hello world".
Edit:
I know that "" in Elixir is binaries which are <<>> in Erlang. In Erlang "hello world" is really a list of ["h", "e", "l", "l", ...].
What was surprising to me is how rarely that matters (unless you actually do a lot of text processing). And the functions that matter (like I/O) deal with io_lists anyway.
And since strings in Elixir are binaries, when you actually need to work with binaries, you're back to the same/similar syntax:
<<0:1, 0:1, 1:1, 1:1>> == <<3:4>> %% Erlang
<<0::1, 0::1, 1::1, 1::1>> == <<3::4>> ## Elixir
> Keyword lists in erlang are unprettyKeyword lists in Elixir are nice, that's true.
From personal experience:
- You're comfortable with Erlang syntax within half a day. There's just so much less of it
- You're comfortable with Elixir syntax within a day of two of active coding. Function captures may trip you up once or twice more, but then it's fine.
You are conveniently not showing many examples where Elixir is considerably less noisier such as keywords, utf-8 binary strings, etc. Module definitions in Erlang use -(). while Elixir doesn’t use a single punctuation character. Erlang has special syntax for list and binary comprehensions while Elixir has unified both and uses no special syntax. Maps in Erlang also have two syntaxes, more ways of doing try/catch, etc.
If you take examples of actual code implemented in both languages, instead of cherry-picked invalid gibberish, you will find that Erlang has more punctuation per character than Elixir.
I was typing this inside the HN comment box. And no, not many changes.
> So you forgot to remove most of the punctuation noise
For the two examples you could come up with for my entire text Elixir adds half a dozen new punctuation characters on top of Erlang.
> If you take examples of actual code
Of course. Here you go:
Enum.sort(["some", "kind", "of", "monster"], &(byte_size(&1) < byte_size(&2)))
I mean, Erlang doesn't even have `&` as a part of syntax, but sure, "Erlang has more punctuation".Besides, Erlang's syntax is significantly more consistent than Elixir's. Just take Elixir's two separate syntaxes for maps, for example. Or the dichotomy between syntax for functions and lambdas (both for declaration and invocation).
The only reason Elixir feels like it has less punctuation is that Erlang is actually more terse than Elixir.
Just having pipes and function captures everywhere makes Elixir code have more punctuation than Erlang.
lists:sort(
fun(X, Y) -> byte_size(X) < byte_size(Y) end,
[<<"some">>, <<"kind">>, <<"of">>, <<"monster">>]
).
I will let others decide which one is noisier.(And no, using Erlang strings here is not the same, especially when popular libraries like Hackney and Cowboy work only with binary strings)
> Erlang is actually more terse than Elixir
Which was my point since the beginning but you quoted only part of my reply. It is clear Elixir has more syntax than Erlang. The function definition delimiters (->/.) vs (do/end) is a good example in itself of how Erlang is more compact and using punctuation.
I stand with my position that if you take actual code, a file or a project, and write it in both idiomatic Erlang and Elixir, Erlang will have more punctuation per character and will feel noisier.
Here is an actual example. I got the hex decoding/encoding code recently committed to OTP in Erlang (https://pastebin.com/qMtj8mSY) and rewrote to Elixir (https://pastebin.com/zUCLeXZG). I ran them through the Whatsapp formatter and the Elixir formatter respectively. Both snippets compile and define proper modules. If you remove all whitespace, the rate of punctuation character per alphanumeric character for Erlang is 35%. For Elixir it is 25%. And FWIW, in this particular case, the Elixir implementation has roughly the same amount of characters as the Erlang one: 718 vs 721.
I will be glad to continue this discussion if you want to use actual code instead of fictional examples.
EDIT: I fixed an error in the Erlang snippet and updated the stats.
> Erlang already is a more concise language than Elixir but also noisier as there are more punctuation characters.
Nope. Elixir has more punctuation characters, and I've shown that.
Now, when we talk about "punctuation per characters of code", then yes, Erlang may have more punctuation in this regard. But it has another thing going for it: there's significantly less syntax in general, and it needs less brainpower to disambiguate (BTW, Elixir's syntax is ambiguous to the point that the compiler can't figure it out in certain contexts).
Even in the examples you provided:
defp encode_hex(<<a::4, b::4, rest::binary>>, acc) do
a = encode_hex_digit(a)
b = encode_hex_digit(b)
encode_hex(rest, <<acc::binary, a, b>>)
end
defp encode_hex_digit(char) when char <= 9, do: char + ?0
defp encode_hex_digit(char) when char <= 15, do: char + ?a - 10
Oh, look, it was `f() do ... end` but then all of a sudden `f(), do: ` (with no end). Whereas Erlang's syntax is (mostly) the same forms everywhere.And where Erlang is consistent due to terseness of syntax:
decode_hex_char(Char) when Char >= $a, Char =< $f ->
Char - $a + 10;
Elixir is an agglomeration of punctuation: defp decode_hex_char(char) when char in ?a..?f, do: char - ?a + 10
I have complained about Elixir syntax in the past [1] when I knew little to no Elixir (I'm now building a big-ish side project in it), but the complaint mostly remains.Meanwhile Erlang may look like it uses more punctuation, but that punctuation can be much easier pattern-matched by our brains because it's consistent and means the same in (almost) all cases.
[1] https://medium.com/@dmitriid/in-which-i-complain-about-elixi...
Regarding the compiler, most compilers have ambiguity. It is the reason why you have to put a space between = and binaries in Erlang. The question is if the compiler is going to pick a side or require the developer to be explicit. Modern compilers prefer to fail early in such cases, rather than letting a syntax error pop up later on.
Furthermore, the use of do/end vs do: is completely up to you. My original draft had only the first, which reduced the amount of punctuation in Elixir further, but I decided to include both styles because you will find both in practice. But if you want to stay consistent, you have the option.
Finally, happy to disagree on the “agglomeration of characters” in the Elixir example. The Elixir code has less punctuation and is clearer, despite the use of “do:” (which, as I said above, is optional).
Case statements are pretty common, and for code I read the with statement[1] is even more common. This allows you to code a happy path broken into small steps and then collect your edge cases. It doesn't solve every case for if/else or cases, but it's a nice tool.
[1]https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1
>you have to basically mentally imagine what all cases are covered and what they mean
Why? Isn't the logic clearly specified in some way by the multiple dispatch/multi-methods? Because if not, how does the program work? So shouldn't a user be able to read the multi* code, and understand it, just like they can with if/else statements?
If anything, my impression is that a significant portion of the community still uses Rails for many of their applications, and some occasional grumblings aside, it's not a big deal.
It could be a big productivity hit actually...
One thing to that stood out was how the author found deployment easy, the same as I first did 5 years ago:
> "The deployment of Phoenix can be as easy as copying the Mix release I already mentioned to the remote server. You can then start it as a systemd service"
I was using Distillery to make the release, but the workflow was virtually identic. Back then, the command to make a Distillery release was even "mix release", just as this author types for Elixir 1.9+ mix releases now.
> "Of course, you can make a light way Docker container too, but maybe you don’t even need to. Mix releases are entirely self-contained (even better than a Java’s JAR)!"
Also true!
elixir and phoenix are wonderful. the phoenix liveview is a fantastic piece of technology; it moves the processing to the backend and allows the web application to be developed in the same language (mostly).
i just recently discovered microsoft blazor and it seems like it's an even better improvement. compared to liveview, the computation is moved back to the client while the development experience is still a single language. there is no more javascript (at least that's the claim) and the platform takes advantage of webassembly to deliver a high performance UX. really compelling stack. I hope it continues to drive the innovation in web development. I'm so excited to see the javascript eat dust. Maybe light at the end of the tunnel to a horrible 10+ year period of web development.
If you're using Phoenix PubSub for your message queue, then it scales out automatically over PG2 clusters.
https://elixirforum.com/t/phoenix-channels-horizontal-scalab...
https://www.phoenixframework.org/blog/the-road-to-2-million-...
I have used Erlang very successfully in a semi-embedded context, but that's quite different from a web server that can usually be scaled horizontally pretty easily.
One obvious one is if you have to hold open a lot of concurrent connections like web sockets. It'd be great for that. Others?
I think it's also really good if you need to hold state server side for any reason.
I'm looking for those use cases where someone picked Phoenix and it was just clearly a better tool than, say, Rails because of X, Y, and Z, despite maybe being inferior for one or two other things.
I would also argue that Plug is a large improvement over Rack, and the idea of explicitly passing a single context map all the way through the request is just a better way to build http responses. I also think that the Fallback controller is obviously a better way to handle common errors.
Anything related to web sockets will be leagues better in Elixir, because the BEAM is built to do a thing like that.
SSR html as a compiled linked list is a better idea than runtime string interpolation.
Sure, Rails has libraries for everything, but some of the core parts just aren't a nice. So if you don't need all of that breadth of ecosystem then Phoenix is a better choice IMHO having worked professionally with both for a number of years.
How does it know what associations to eagerly load without knowing about the views? It can't eagerly load all of the data on complex objects.
Ecto Repo vs Active Record: https://youtu.be/IFKG4Hgt-zM
How does that differ from specifying an includes on associations for eager loading?
It's not saving you any mental energy; you still have to discern what should and shouldn't be loaded up front.
To be fair...
If you want to protect yourself from these with Rails you can install Bullet[0] and get protection through in your face notifications, and you have the option to let it slide because you're taking advantage of caching with Rails and in this case you know what you're getting into and the N+1 query with caching ends up being better because you understand your domain.
Rails also has the strong migrations[1] gem which is a huge help for not shooting yourself in the foot for running migrations in production by helping you avoid table locks and other issues / errors. But AFAIK there's no Ecto equivalent, but strong migrations is really really useful.
Rails also has the data-migrate[2] gem which is a nice little abstraction for splitting out your schema changes and backfilling data in an automated way. There's nothing like this with Ecto. This one isn't as useful as strong migrations IMO but it's still very handy to have this problem taken care of for you without having to re-invent a new strategy in every project or copy code over.
Basically all 3 of these things are something I'd use in every Rails project but with Phoenix I wouldn't have these things except for N+1 query protection.
I'm pretty sure you could technically create strong migrations and data-migrate for Ecto but the reality of the situation is today neither of them are available and there's no sign of them coming anytime soon. Meanwhile strong migrations has had ~6 years worth of real world testing at this point.
[0]: https://github.com/flyerhzm/bullet
I think this is a really great discussion point btw because depending on a ton of 3rd party libraries that come and go can be an issue for sure.
But at some point you need to live in the moment and align your expectations with the current reality and think about what happens to your code base if the library stops being maintained.
For example, strong migrations has a 5+ year track record and shows no signs of dying. It doesn't make sense to live your life in fear and avoid 3rd party libraries because one of them might go away in the future because the maintainers get bored or stop using it. Because what happens if that doesn't happen and it ends up being wildly popular and maintained for years?
The reality is a lot of people develop libraries, use it for a while time and then move on. This happens in Elixir too. The ExAWS package comes to mind (the most popular AWS package for Elixir). Similar things happened with Arc (file uploads) too. Eventually those tools got replacements or new maintainers.
If you're developing an application it makes sense to weigh the pros and cons on using certain libraries but it's not like these libraries immediately go away. Usually there's a huge amount of notice before a library becomes officially dead. There's also tons of opportunities to talk with the authors and collaborate with the community to keep it going. And in this case, the outcome is likely a lot better than you trying to develop a custom solution in total isolation that's not open source.
So yeah, there's really no downside to using most 3rd party libraries. Either it fits your app well enough and continues to be maintained while you happily use it (and maybe contribute back changes), or it fizzles out and you got some value out of it and now you have a great head start in implementing a custom solution from it for your unique use case.
You can opt in to preloading with Ecto when you consciously want to make the tradeoff, which I think is a better default approach.
- Channels in Phoenix are just a joy to use compared to ActionCable. This is partly due to the language (pattern matching, especially) but not having to deal with a Redis instance (and/or AnyCable) is also appealing. It just works out of the box and is ridiculously performant.
- I find the Repository pattern much easier to wrap my head around than ActiveRecord. I feel like with Rails, I always have to know the state of an object whereas w/ Ecto, things are more explicit. I know some people prefer AR here, but I prefer not making an accidental query.
- I feel like I am lost every time I'm in a Rails project with so many abstractions now. Maybe this is me being a curmudgeon, but I remember being able to trace request all the way from where it hits the machine (say, Nginx) to the HTML rendering even with Rack. Now, if I had to do something similar, there are so many layers to peel. This is again partly due to the language, and partly to Rails' age.
I would reach for Rails when I'm concerned about finding developers (Elixir devs are fewer and more expensive, generally), or if there are Ruby libraries I want that aren't available in Elixir.
If you hire smart and flexible people, and give them a bit of time to learn the syntax, Erlang and I assume Elixir will bend their mind to a new shape, and they'll be fine.
Figuring out what you want the computer to do, and what steps will be needed are much more important than the specific syntax you use.
The performance for typical stateless webapps is great, but honestly the thing I love is the amazing tooling and libraries. Elixir libraries are often very high quality.
Which other ecosystems have you tried?
Just asking because with Python and WTForms they've kept validations separate from your model for the last ~10 years.
You could for example create "sign up", "sign in" and "profile" forms based off a single user model and each form has its own fields that you can define with their own validations. Very similar to how you can create separate changesets with Ecto and use a single user schema.
Other than that, I would say that any place with a complex service oriented environment where you could leverage the Erlang VM would be an obvious place to use Phoenix for doing your webapps.
Elixir is really quite good in the semi-embedded space with several successful companies having their IOT bread and butter in Elixir Nerves platform. The deployment story is getting really mature in Elixir.
The article has some really salient points: Testing and Documentation and really amazing in Elixir. Concurrent tests are amazing. So for example normally a "database" would be a global resource and running concurrent tests against the database is really tricky. It's basically turnkey in Elixir (need to set up two settings).
With a bit of work, it's not terribly hard to write concurrent tests that exit the VM and come back into the vm and use a test-shard of a global resource. So, for example, you write a test that issues an HTTP request to itself, and the request is instrumented with parameters to connect it back to the test process and use the correct "temporary shard" of the database. (In the case of the database it's a transaction, I use the phrase "temporary shard" because the same mechanism can extended to other concepts too, like module mocks, ets tables, process registries, etc, which all use the same mechanism, you only have to set it up once).
Yeah, I don't need any convincing there. Erlang was a perfect fit for the device I worked on. High level enough to get things done quickly, but with a really solid, predictable runtime.
So a lot of the other responses are comparing it against Rails and the like, but it sounds like you may be asking specifically around scaling.
Erlang (and by extension, Elixir), is nice even when scaling because the actor model will scale to I/O or CPU bound workers simply, without having to tweak threadpools or worry about thread starvation or etc. Other languages that provide n:m concurrency here have the same benefit though (i.e., Golang).
Where it shines in comparison to those is in its fault tolerance and memory model. Immutable non-shared data + supervisor hierarchy gives you tools to tackle state that are less error prone than most other languages.
And the fact it has a Rails like environment in Phoenix (and Plug, and Ecto, and the whole ecosystem) means you get the same quick-to-build app functionality, without giving up the performance and state management.
It's basically having all of these that make it desirable on this front; you can write code as quickly and simply as in Rails, getting the braindead simple scaling behavior of any actor/CSP based model, with the state management of an immutable language, and the fault tolerance of Erlang.
But the actual number of concurrents per instance, and the effect on latency, is quite another thing.
And it also provides you a better story around shared state within the instance, and better controls around its access.
Right now, I’m trying to find a way to speed up builds in CI because they’re the biggest bottle neck to deploying. Building an umbrella with 5 apps, 3 of which are phoenix, leveraging parallelised docker buildkit, will still take 8~ minutes.
[0] https://github.com/elixir-lang/elixir/blob/v1.11/CHANGELOG.m...
Sign me up. I hold hope that Elixer is the thing that starts pushing the knife into OOP. Microsoft has added a ton of features to make functional style a thing in C#, and nearly everyone hates Java...so maybe the stars are aligning.
This brings up the pain of scala... only this time im not going to join the fan club.
I've spent the last year building out a web application for service providers to use to manage disaster recovery software for multiple clients using Elixir, Phoenix, LiveView, and TailwindCSS. We haven't used a single line of JavaScript code, including for the Tailwind components that typically rely on JavaScript to function. LiveView has been able to handle it all.
I wish I could have better integrated tests with the javascript, but considering, the live view suite of tests is really quite good.
In my mind, a pragmatic language doesn't do things like:
1. Have multiple ways to define a function. 2. Handle functions differently depending on how they're defined. 3. Allow silly things like 2 + "2".
If you subscribe to the philosophy of sanitizing and validating user data on the edge as soon as you receive it and then stick to using whatever data type you decided on, this is much less of a problem (any mismatch in runtime would then be considered a bug).