I'm currently building a video course hosting platform with Elixir / Phoenix and all I can really say is this has been the nicest tech stack I've ever used in 20 years of web development.
IMO it really does feel like you get the best of everything (developer productivity, developer happiness, great language for creating maintainable code, OTP and the BEAM bring a lot to the table, it's memory efficient, tracing code without a ton of experience is very doable, it's fast as balls and Phoenix channels / LiveView is a great abstraction on websockets). I find myself fist pumping all over the place as I make progress towards this app while learning as I go.
I don't think I've liked a technology this much ever and I'm honestly surprised it's not already more popular than it already is. I've been around for the release of a lot of major frameworks and technologies (Rails / Django, Node, Go, etc.). Nothing has impressed me this much. It truly feels like it's a zero compromise environment to write code in and I hope I can still be using it in 10+ years.
I love hearing stories like yours. I wonder how Elixir would compare to Clojure, which I've used and liked for web development for many years. I hear nothing but good things about Elixir but I wonder if it would bring something new to the table for me in the "really liking it" category.
I think you will find Elixir itself very similar to Clojure. They are both functional, dynamic languages with immutable data structures. They both use protocols/behaviors to achieve polymorphism in similar ways (e.g. functions working with collections (Clojure) and enumerations (Elixir)).
While there is a superficial syntactical difference I think the most important differentiation is the runtimes, the JVM versus the BEAM. JVM will outperform BEAM with regards to computations, while the BEAM has better support for true immutability and concurrency.
With regards to web development specifically the Elixir community seems to more or less have converged around a framework approach with Phoenix, while Clojure (too me) seems more geared towards a collection of libraries approach, á la Node.js.
I don't have much experience with Clojure's tooling story, but the tools that come come bundled with Elixir, like Mix (build tool) and ExUnit (testing framework), are very good. Compared to the languages I have experience with (Python, Ruby, JS, Java, C#) Elixir provides the best developer ergonomics out of the box.
I haven't worked with Clojure, but one of my subscribers wrote me contrasting exactly that:
Wanted to thank you again for making these videos, I learned a ton and I'm really into Elixir now. If only there was a way to use it at work!
One of my favorite things about Elixir is Erlang/OTP. It's as if you're in Disneyland and everything's nice and colorful. But sometimes you go into a basement and you discover the entire thing is built on top of ancient Jedi ruins and these immortal, powerful forces lurk just beneath the surface, at your disposal.
Clojure is the opposite. It's all nice and fine as long as you're in Clojureland, but then you look behind the curtain and it's a huge pile of cards and matchsticks that hold everything together, and the glue is made of mutable state and classes.
And man, the tooling. I can't say this enough. It took me about 3 seconds to create a new project with mix new to get the unit tests and everything set up for the flatten challenge. I don't even know how to set up unit tests in Clojure, and I've been playing with it for 5 years.
IMO BEAM is a better VM than JVM despite the number of libraries.
It provides a more robust, performant and observable platform for Web development. (Technically JVM is faster batch operations but for Web, most Elixir apps win)
More importantly, it's a perfect fit for immutable functional languages. Everything is immutable and proper tail-recursion are provided. Clojure is great but it has several different ways to define state and type, and sometimes I found they're a little bit awkward. With BEAM process, there's no need for language to have constructs for state/reference/async handling, which could makes the language itself more simpler and more compact.
> I don't think I've liked a technology this much ever and I'm honestly surprised it's not already more popular than it already is.
I think a big part of it has been a lack of learning resources. A major part of why I started my site and YT channel was the frustration I had learning Elixir in 2016. Phoenix was even tougher. At that time, even the paid resources were mostly out of date. I found about 70% of everything I came across (books, videos, blog posts) wouldn't compile on Elixir version > 1.0! The changes in Phoenix 1.2 and 1.3 were a similar story.
The one exception to this was Dave Thomas's Programming Elixir book. I bought version 1.2 and he religiously updated it (for free!) all the way through its current version. I never found anything in it that didn't work on the current version of the language. Well done, Dave!
Of course they are, but thé article seems to be from someone interested by elixir, rather than someone who used it long enough to discover them (although I might have misunderstood the article.)
From own experience, the problematic parts are :
- deployment being a bit messy if you try to follow 12 factors (might be improving in 1.9)
- absence of a decent debugger
- younth of the ecosystem and size of the community (aka hard the find a maintained lib that works for a few common issues.)
- compiler being a bit slow to my taste (YMMV)
- then, of course, the elephant in the room: the absence of the kind of static type checking that facilitate refactoring ord relatively large codebases.
Nothing unbearable or permanent, but some things to keep in mind. Go try it !
BEAM based languages lack "good" debuggers, because they historically depend on tracing instead of debuggers. I recently wrote a blogpost about the tracing landscape in Elixir. https://www.erlang-solutions.com/blog/a-guide-to-tracing-in-...
But the main reason is that BEAM languages are mainly used for concurrent computing which is a very difficult problem to debug with classical debuggers. I know it is not a mainstream way to "debug" but a very useful one once one learns it.
Author here. You are right in that I haven't had a chance to see what a few-years-old Elixir project feels like to run and maintain. I've been following and experimenting with Elixir for a few years and I'm doing more stuff with it recently.
And the post was very much intended to be enthusiastic about cool things, rather than nuanced and looking for problems. So I think your take-away is entirely fair.
- average performance ( like 10x slower than Java even worse for CPU intensive tasks )
- dynamic language ( this is the worst part ), working on large code base means problems ahead
- lot of features from BEAM / OTP that are not that useful and done better on modern cloud platforms ( Kubernetes for instance does a lot of similar things but better and more flexible, apply to any languages ) People like to talk about hot code update which imo is a terrible idea, you should have a proper CI/CD pipeline to do that and not rely on dangerous features like that.
> - dynamic language ( this is the worst part ), working on large code base means problems ahead
IMO, I the fact that Elixir is a dynamic language is not that much a problem thanks to pattern matching.
Elixir's pattern matching is really powerful, allowing for things like:
def move(_state, _x, _y, kind) when kind not in ~w(forward backwards) do
{:error, :invalid_move_kind}
end
def move(state, x, y, kind) do
get(state, x, y)
|> do_move(state, x, y, kind)
end
In this case, I am asserting that "kind" is either string "forward" or "backwards". If not, I return the an error :invalid_move_kind. I don't need to check if pos is nil or not string, since I can assert using pattern matching that everything with my inputs are right.
Also, there are things like structs:
defmodule RobotVsDinosaurs.Robot do
alias RobotVsDinosaurs.Robot
@enforce_keys [:pos]
defstruct name: nil, pos: nil
def set(state, x, y, robot = %Robot{}) do
case Matrix.elem(state, x, y) do
%Robot{} -> {:ok, Matrix.set(state, x, y, robot)}
_ -> {:error, :invalid_type}
end
end
end
What I am saying in the code above is that %Robot{} must have a :name and a :pos entries, and by default they're nil. However, to create a new %Robot{}, :pos must be different them nil. And of course, I can pattern match if my input is actually a %Robot{} and not a simply dict.
You can also do crazy things like pattern matching if a byte array have some specific characteristics, like a magic number.
So while I think it would be nice for Elixir to have types (there is Dialyzer however it is kinda a PITA to use), it is less necessary than Python/Ruby/Javascript thanks to the above.
Regarding Elixir being a dynamic language, I've programmed in Java, C#, and other strongly typed languages as well as Ruby, Python, Elixir, and other dynamic languages since the mid 90s. Throughout my entire career, I cannot think of a single time when I've thought, "It would be so much nicer if this language was statically typed." It just hasn't ever been an issue for me.
If typing is something you really want to catch at compile time, and pattern-matching and guards aren't enough for you, you can always define Dialyzer specs and run Dialyzer during your build process. I'm currently working on an Elixir codebase that does this, but I've worked on others that don't, and I haven't seen much of a difference either way. Personally, I think strong typing is overrated.
Not to mention that for a decade and more, HN was full of "dynamic languages are the best! Who needs types! Dynamic FTW!" comments and articles. It wasn't fully accurate, and neither is the about-face to "Types FTW! Typing is the best thing ever!".
I have come to enjoy the more self-documenting nature of static typing (in PHP of all languages). Yesterday I worked on a legacy project with no typing, and to determine what was passed to an untyped method (with no documentation) required running the interactive debugger and inspecting what was passed in different scenarios...
Well I mean there are a class of bugs they help to catch but with pattern matching it really don't come up very often. Just don't match the values you don't want. Sure it's a runtime error but you'll catch them pretty fast and elixir is fault tolerant.
I've seen this stated many times as one of the big drawbacks of Beam languages. But using Distillery with Elixir, it's very easy to generate a release that's completely self contained. Just extract a tarball on a freshly installed system and you are ready to go.
If your target platform differs from the one you are compiling your project on, there are various options for cross compiling too
We just bumped to elixir 1.9 and I'm enjoying seamlessly deploying to our dev environment with a single command from Linux and my juniors are working on getting it to go for Mac via vagrant.
There might be (and probably are), but I haven't really come across anything yet that made me think I chose the wrong tech or I'm deeply upset about a certain design decision.
On the other hand, there's so many nice things that you can take advantage of without being a functional programming / Elixir god and every time you figure something out you think "how the heck was I able to survive before being able to do this?". I'm hardly an Elixir veteran but I'm past the Kool-aid phase. I still get amazed almost every day working on this project for 2-3 hours a day.
In practice, Elixir data structures are rarely dynamic in nature. All structs have the same fields. Virtually all lists and maps and whatnot you end up writing will have the same type for all elements. Unlike in the JS ecosystem, there is no culture of having functions with, say, optional arguments in the middle, or arguments that can be either a number or a function or a Date object or stuff like that. Unlike the Ruby ecosystem, there's no runtime monkey patching, runtime attribute generation, runtime anything generation. Everything is as robustly typed as your average C# program, but there's no way to write that down.
Or, well, there is, typespecs. And typespecs are imprecise, hard to write, borderline impossible to typecheck (dialyzer errors are shit), and generally just barely worth it because they're so hard to use.
It just feels like.. such a waste. Dynamic typing is nice if it lets you iterate faster, but that's nearly never the case in practical Elixir code, because there's so little dynamism and magic abound (this is a good thing!). At the same time, not being able to see what values a parameter can be, not being able to ever make proper IDE support, for a language and ecosystem that is so static in nature, is just a missed opportunity.
I still really like Elixir, and I agree with the GP's love letter, but I wish it had a friendlier way for me to communicate data structures to the program, future me, and my colleagues. I still hold hope that someone (future me, maybe?) will bolt types onto Elixir in the same way TypeScript bolted them onto JavaScript. It should be easier than TS because of how static-y the entire Elixir ecosystem already is.
(Sidenote: the usual argument against static types in Elixir/Erlang is that static type checking across process boundaries is hard, if not impossible. Genius Haskellers tried it and failed. My answer is that not having type checked messages would solve 95% of my itch. In the end, most lines of Elixir code is single-process, just transformations and function calls like in any other language. I'll be happy to tell the compiler what type I expect a received message to be and crash at runtime if it isn't - just like I do when I get sent a JSON payload over a REST API or IPC or whatever)
I've never used a proper statically typed language for web development but with Elixir can't you solve the 90% with pattern matching and guards?
For example (taken from the docs on guards):
def foo(term) when is_integer(term), do: term
def foo(term) when is_float(term), do: round(term)
If you tried to pass a string into foo at run time, no function pattern would be matched and it would blow up. The above should also give IDEs a way to potentially say "hey, when you call foo, it accepts a term and expects an int or float", and it's also very human readable to figure out what you need to provide as arguments without IDE support.
What's interesting about guards is you can also put expressions in there. Like `when foo > 3` and now suddenly you have a new type of integer that only accepts integers greater than 3, except it's not an explicit IntegerGT3 type you have defined somewhere.
In practice, how much worse is that vs a "real" statically typed language, other than putting guards on things is optional?
OTP releases let you build something akin to this, where the runtime and all the dependencies are bundled in; so long as you built it for the right architecture (much like a binary), it's a single command to run your app.
This used to be done with Distillery[1], but I suspect its use will go down now that Elixir has native OTP releases.[2].
In either case, there's still work behind it, but it's not like deploying Ruby, Python, or Node, where you have to be pretty careful about library paths and runtime versions.
> I like the convenience of deploying a golang app, just one executable.
This is crazy. What about configuration, systemd unit file / initscript, documentation, manpage, shell completion. And the ability to tell what is installed, where, and at which version.
HN forgot how to do software distribution to the hype of statically compiled languages.
Deployments are a mess and as it is interpreted, performance is closer to Ruby than Go. I found the community to be pretty disappointing (but YMMV obviously) — it seemed more alternative oriented than the Rust community (which seems more solutions oriented).
Performance is definitely not close to Ruby, this is just blatantly incorrect. Elixir and Erlang can easily hold their ground vs Go.
The BEAM was originally designed to run software for telephone switches. It's often referred to as soft realtime because it is so responsive. It has been battle-tested for now over 3 decades.
Where the BEAM falls flat is pure number crunching but it's blazing fast with binary processing (e.g. string parsing).
A fresh phoenix project where you render a nontrivial template easily reaches sub millisecond response times, without any kind of optimization.
For CPU intensive tasks, performance _is_ close to Ruby, while both Ruby and Elixir are far behind Go. Soft realtime doesn't mean fast, it just means good std deviation on response time and not a lot of missed deadlines. Fortunately as the article points out, there's always Rustler if you need to optimize while still keeping BEAM crash proof.
That being said, I agree that performance for certain IO intensive applications like web servers is very good, way better than Ruby. Subjectively Phoenix feels way, way faster than Rails in development and is much more performant in production.
>>For CPU intensive tasks, performance _is_ close to Ruby, while both Ruby and Elixir are far behind Go.
Yeah, but how many companies do you know of that perform heavy computations in such large scale that their choice of programming language has a noticeable effect on the result?
I mean, it might matter for tech giants like Google, who need to squeeze every bit of processing power out of their CPUs. For almost everyone else, it will be a non-issue.
A fresh phoenix project where you render a nontrivial template easily reaches sub millisecond response times, without any kind of optimization.
Going from Phoenix to Rocket.rs I saw a pretty dramatic speedup on even simple pages. Phoenix was faster than the Rails version of the app, but still slower than a compiled language.
It's not a mess. Mostly It was just unclear mixed of runtime and compile time configuration that contribute the inconvenience. Elixir 1.9 just comes up with a simple built-in release mechanism with more clear on how you put configuration.
It's not a mess. Mostly It was just unclear mixed of runtime and compile time configuration that contribute the inconvenience. Elixir 1.9 just comes up with a simple built-in release mechanism with more clear on how you put configuration.
From the sound of it things have gotten better (by throwing out oft-touted features), but Elixir deployments are inherently far too complicated. With Go and Rust you get a single binary and a single program to run. With Java (and other languages targeting the JVM) you get a single jar archive. With Elixir you get to bundle the whole runtime and spawn a few separate supervisor processes to run even a hello world app. Lord help you if the only maintainer for the deployment tooling takes an unannounced vacation or if the various BEAM processes stop talking to each other because the stars are misaligned (and the deployment tools are minimally tested) and you can't even run an Elixir based database migration as a result.
When I started toying around with Elixir and Phoenix I'd just come off of a Clojure kick. You can get most, if not all, of the syntactical sugar that Elixir promises with other languages that all offer infinitely simpler deployments with more mature tooling. In the case of Clojure you get whatever JVM tooling you'd like.
I think most people consider Java to be an interpreted language, it's interpreted by JVM. It's obviously somewhere in between, in the same way that JIT languages are, but it's still not native.
Not sure about the BEAM, but Java isn't considered an interpreted language at all.
First, discerning the compilation step to IR is relevant, which is why for example, Python can be compiled or interpreted. That distinction matters. Even though it is compiled to intermediate representation, it is still compiled.
Now, with regards to the intermediate representation, Java Byte Code is also not interpreted. Java Byte Code is compiled to native machine code either Just in Time by the JVM, or Ahead of Time by the SubstrateVM. Both of these make it a compiled language.
An interpreted language never gets translated into native machine code, it gets executed by a native interpreter, and that's very different. It is more akin to using a language where you read and parse texts, and based on the text, you might execute one or more things. Now if that parsing to execution branching is powerful enough to allow Turing complete behavior, you have yourself a full blown interpreted computer language. This is not what happens in the JVM. The JVM translates Java Byte Code to native machine code Just in Time, and then the native code is run.
We should be more precise: the JVM may or may not have an interpreter. This is implementation dependent. There are JVMs that are interpreted fully and AOT compilers as well.
Hotspot includes an interpreter -- your code may be interpreted until it gets hot and gets compiled.
Still, I don't think it's fair to call Java an interpreted language since the parts that are interpreted are only during warm up or slow enough not to matter.
Yes you are right. Someone could argue Java Byte Code is interpreted, but I think colloquially, that's leading people to misunderstand the nature of modern JVMs, which as you say, will choose to either run the code in an interpreted manner, or compile it first (possibly with optimizations) dynamically at runtime. Those choices are made based on what will result in best performance and safety.
Java on the other hand is 100% compiled (to bytecode). And byte code is machine code for a non existent machine. Actually, I think Sun had built machines that could natively execute Java Byte Code. Someone could build an interpreter for it, but I'm not aware of one.
Java is an interpreted language because you always need a JVM to run it. I even if you distribute the runtime with your code as one package, that’s just an artificial distinction.
Regardless, interpreted does not mean bad, or poor performance for your task.
It’s a technical notion.
Most people would say Python is interpreted, even if you are only running compile python bytecode on a JIT like PyPy. That’s exactly the same situation as Java.
Java is compiled to Java byte code. It cannot be interpreted in its source form. There's nothing preventing it to be, but as far as I know, there are no interpreters for it. Python on the other hand can be interpreted from source, and that's the default behavior, making Python an interpreted language.
This makes Java a compiled language, even though it is compiled to machine code for a machine that doesn't exist. Java Byte Code is an assembly language, and in fact, Sun had at one point machines that were natively using Java Byte Code instruction sets.
Now Java Byte Code is trickier. You could consider it to be interpreted or compiled or both. It is fair to say that in general it is interpreted, but it is just as fair to say it is in general compiled.
What matters most though is to understand that modern JVMs make use of a JIT compiler and optional AOT compilers.
In the latter mode, you can compile Java Byte Code to native machine executables ahead of time or pre-runtime and no JVM beyond that point is required. So you can distribute the app as a self-contained executable. In most cases though, this will be less performant in the average and peak performance, but it will speed up the worst case, such as start time.
In the former mode, the JVM will analyze runtime behavior and based on the frequency of use of various code paths, it will either compile the code to native machine code (cache the compiled code), and then run the freshly compiled code block, or it will choose to interpret the byte code directly. This allows aggressive optimization from information which is only available or easily available at runtime when performing compilation.
That is incorrect. I don't know of any mainstream Python implementation that has ever interpreted the code directly, except perhaps in a context like evasl(). CPython has all ways compiled to byte-code first.
Actual sourced-based interpretation is _very_ slow, and pretty rare in anything that sees meaningful real world use. Ruby was, back when it was an order of magnitude slower than Python, but no other example jumps readily to mind, and Ruby went bytecode with the release of 1.9 in 2007.
Ah you are right, I didn't know that about Python. Since it can compile to IR on the fly though, I think it gets more tricky as well. Probably fair to consider that interpreted or compiled both. Like they say in their doc:
> Python is an interpreted language, as opposed to a compiled one, though the distinction can be blurry because of the presence of the bytecode compiler. This means that source files can be run directly without explicitly creating an executable which is then run.
OpenJDK can't do that. You need to pre-compile the source to byte code first.
I love Elixir, but the learning curve was steep. Async everything, dialyzer, macros, Erlang errors, charlist vs string vs iolist... It was a lot to take in.
Edit: 4 upvotes in 4 minutes. I guess people are reading this... for anyone who hasn't already discovered it, the saving grace for me was the Elixir Slack channel. I also learned while working with someone who really knows Erlang, which helped quite a bit, too. If anyone learning Elixir gets to the point where they want to scream, feel free to email me. If I can help, I will! Email in profile.
Hmm, I'm primarily a C# dev and recently spent a morning getting to know Elixir, and was actually surprised with how flat the learning curve was.
Now, writing idiomatic functional code while coming from an OO background, now that is difficult!
I have to say though, I loved the short time I spent with Elixir so far - it's hard to put a finger on it, but it just feels right. But... I do wish it was statically typed :(
What do you mean by that? Most code in Erlang/Elixir is synchronous, meaning it will block the process (but not the scheduler). Maybe you meant concurrent (or message-passing) everything?
I've written about 2,000 lines of code on this course platform so far and everything except for 1 function is synchronous from a "this is my code" standpoint. I only used Task.start once to launch something in the background.
This is what I like about it: my junior had an ask from management to profile a customer task, as a single process, and as a concurrent job to simulate more than one simultaneous client. I'm nonzero suspicious it was asked to try to get him to trip up and have an excuse to can him. I instructed him to do the job in elixir. He got it done, then I wrapped it in task.async one-line, and it just worked and he understood what was going on, and was able to generalize the concurrency rules.
The elixir standard library is just full of things that make your concurrent programming seamless, easy, and bulletproof, even for a junior that's never done things that way.
I can't imagine him getting this correct with python async, JavaScript promises and callbacks, or having to create go channels.
In the Erlang VM, process execution can stop at an arbitrary point and resume later on. This time slicing is done to ensure that no process, takes up too much time. So you must treat your code as asynchronous since if you get a value from another module, then get it again in the next line it might have changed because your process was suspended in between those two lines.
All of the GenServer stuff is built on top of regular Erlang processes. It's all async. You'll might find out when your server gets busy and GenServer calls start timing out
I don't think it's a good idea to worry about dialyzer, iolists or especially macros for someone just learning. In fact, I don't think OTP should be the first step, either.
You can be very productive with Elixir before learning the things that it's famous for!
• ETS ordered_set tables, for mutable AVL trees (for when you want to cheaply apply a sequence of random mutations to a collection of terms without building a lot of garbage);
• The process dictionary (https://www.erlang.org/course/advanced#dict), a mutable per-process hash-map data-structure, for when ETS ordered_set tables have too much inter-process copying overhead.
All that, plus Erlang lists (singly-linked lists) are really cheap the way Erlang does them. (Each Erlang actor-process gets its own heap that gets independently GCed; short-lived processes never experience even a single GC, so allocating linked-list nodes (by consing to the beginning of a list) is just a matter of incrementing the actor-process's heap's free pointer—about the same level of optimality you'd hope to get from pushing to the end of a vector.)
is the one that's in production and doing its work. Most developers including me, get bored of doing something in x,y,z framework, and we gladly go try something else for a while.
I have a good friend, that jumped on the band wagon of elixir. he openly now admits, he wish he just used python.
I hate band wagoners.. no language , no framework is the cure all.
IMO it really does feel like you get the best of everything (developer productivity, developer happiness, great language for creating maintainable code, OTP and the BEAM bring a lot to the table, it's memory efficient, tracing code without a ton of experience is very doable, it's fast as balls and Phoenix channels / LiveView is a great abstraction on websockets). I find myself fist pumping all over the place as I make progress towards this app while learning as I go.
I don't think I've liked a technology this much ever and I'm honestly surprised it's not already more popular than it already is. I've been around for the release of a lot of major frameworks and technologies (Rails / Django, Node, Go, etc.). Nothing has impressed me this much. It truly feels like it's a zero compromise environment to write code in and I hope I can still be using it in 10+ years.