Sketches of Elixir
blog.zdsmith.com
blog.zdsmith.com
You could argue this for other languages - But, I am an experienced programmer who has written production applications in quite a handful of major languages and frameworks out there. I've tried almost everything that trended on HN in the past years - Scala, NodeJS, Ruby, Python, GoLang, R, PHP, Java, and a handful more and I can tell you this - there's nothing quite like Elixir for me till date.
These days, even for my consulting work, I use Elixir. It's almost as if I just push code and forget. (Except for watching out for security vulnerabilities, other regular audits). Everything isn't as green since it's a fairly new language, there's still libraries lacking for some basic stuff - such as authentication (only a handful of them are maintained). But still, I wouldn't consider downgrading to anything else.
Elixir has drastically improved my productivity, and you get to find errors in compile time, leading to much, much better quality, reliable code. I love how fast I can develop apps, SPAs and what not with Elixir/Phoenix. Coming from a NodeJS experience, Elixir is a 100-fold upgrade from JS. In fact, you will realize how terrible JS is (as a language) once you start programming in Elixir.
The closest to Elixir that I like is Scala. They are almost like cousins. Elixir runs on Erlang's VM, Scala runs on JVM. In fact, I prefer Scala.js for my frontend these days, which converts Scala code to JS with strong typing support.
But, Scala is more verbose and way too advanced for someone new to the language. There's a 101 ways to do the same thing and it's very academic as well (The Scala book was over 700 pages when I bought it).
If you are looking for a new language/framework to learn in 2019 - pick Elixir + Phoenix. If not, then go with Ruby + Rails, you couldn't be wrong with either choice.
The downside is that it raises your awareness of deficiencies in other languages that you never really noticed before.
https://www.brightball.com/articles/comparing-elixir-and-go
That is just the summary link that includes a link to the original article I wrote for Codeship as well as the big discussion from when it hit HN.
The HN discussion, especially, was great.
Compared to BuckleScript, sure, but that's an extreme outlier, near instantaneous whole project (wtf) compilation times with tiny output is most definitely not the norm.
As a Scala.js user that is also subjected to the Angular/Ionic/Webpack world, the former is very fast, as in, 1-2 seconds incremental compilation, whereas the latter is a lumbering beast, often taking over 10 seconds for "live reload" for even the simplest of changes.
In terms of output size, yes, there is the Scala collections "tax" (@150KB), but thereafter the bang-for-buck factor pays off -- all the power of Scala with minimal increase in bundle size.
I’ve been working in Node for a few years now, on a project that suffers a lot of warts from adopting Node early and not benefitting from the wisdom of the group.
Something I didn’t know about myself as a full stack developer is that I work on UI because I like human factors, and I work on backend because I like solving architectural or data problems and sometimes I’m just tired of writing Javascript.
Trying to homogenize everything at work removes a vey important pressure relief valve, both for individuals and team dynamics.
I figured this out about a year ago and if not for major life events I’d be out there somewhere writing Elixir or Rust or Scala code.
ES6 and beyond has some things worth experiencing so I’m focusing on that as much as I can. Just a few more months and I can pivot outta here.
I haven't used elixir but I my productivity with Django is much bigger than when compared with other solutions like php, node, java/spring, c#.net etc.
I am talking about good ol' traditional web apps, no fancy spa stuff where I can use Django in all its glory: cbvs, forms, admin, auth and the huge package ecosystem.
Give it a try if you haven't, even just the Django tutorial should give you a hint of the productivity enchantment you'll have!
Elixir and Go just skirt these problems altogether because all I/O is async under the hood (no need for "await" keywords), and both languages support efficient concurrency and parallelism. One process can fully utilize a host.
Of course, this doesn't speak to the quality of available web frameworks, which may or may not matter for your application.
Do you really need async IO? Even at Google scale[1] it's kinda a waste of time for web/job servers and only required for proxying etc. The overhead of threads is massively overstated [2]
Ruby scales a long way using Puma or Phusion Passenger with one process per core (just like NodeJS) and adding threads until you hit CPU saturation. Python must have something similar?
[1] https://www.slideshare.net/e456/tyma-paulmultithreaded1 [2] https://eli.thegreenplace.net/2018/measuring-context-switchi...
Probably need a better framework. I've never had such a problem come up unless I was trying to be clever and not use an established framework.
There isn’t a web framework that I’m aware of which magically makes all sync requests async.
Please don't assume you know what someone understands. Try asking questions when comments seem vague or stupid like mine did.
Waitress gets part of the way there. IIUC Request/response is done via asyncio but the actual web app part uses a thread so you can't accidentally block the whole thing.
Working with Elixir/Phoenix is a far superior experience in nearly every way for web/api IMO.
Phoenix wins on parallel jobs because thanks to Elixir it doesn't need ActiveJobs or Celery. Django doesn't win on anything IMHO, except maybe the ease to create an admin CRUD (but Rails has a gem for that.)
To add another tool I used, even Django would win against server side JavaScript. I'm using that only in special cases and for demos, because so many more people knows it than other languages.
I'm mostly convinced to take the red pill thought! Can you recommend any good elixir/phoenix learning resources?
Now that I mentioned learning resources I think that you should agree that this is a point where Django wins: Its documentation and learning resources!
https://hexdocs.pm/phoenix/overview.html
https://hexdocs.pm/phoenix/Phoenix.html
https://elixir-lang.org/getting-started/introduction.html
https://elixir-lang.org/docs.html
If you buy books, look at https://elixir-lang.org/learning.html
You should probably start with Elixir but you can do as I did with Ruby/Rails, start with the framework and learn the language as you need it. If you do, in the case of Elixir you'll probably be puzzled by all the Supervisor and Gen* stuff based on OTP. Hint: GenServers are more or less objects with their own CPU https://hexdocs.pm/elixir/GenServer.html#content and Supervisors are kind of a systemd internal to the language.
Interesting, I haven't seen that as a strong point for Elixir.
Granted I'm comparing it to Haskell and Rust which really give you errors in compile time. They aren't very popular for web dev though and compared to the other languages you list I guess I can see where you're coming from.
I agree, though, that compared to Haskell and Rust it doesn't give you much. Dialyzer is (intentionally) a fail-open checker that doesn't allow expressing generics; I've had a rough time getting it to tell me my code is wrong. Credo is (intentionally) just a style linter. "Just let it crash" isn't useful when you're talking about code that is going to break because of its structural incoherence (a class of errors type-checker are good at catching), and not because of transient errors in external dependencies.
If, though, you make a habit of writing typespecs for everything, and defining domain-specific types rather than relying just on the primitives, it can get you surprisingly far.
It'll never be as powerful as Haskell or Idris, but it's absolutely a tool all Elixir (and Erlang) developers should be using.
Also, if anyone ever wants an easy way to contribute to open-source Elixir, just start running Dialyzer on popular projects. Every warning that pops up will be a bug, and often they'll be easy to fix.
Real life Rust business logic is much, much gnarlier than in a GCed language like JS. You need to manage memory so you need to deal with references and deref, lifetimes, Box, Cell, ARC etc.
For an open source example of what kind of complexity arises in real life check out crates.io: https://github.com/rust-lang/crates.io/blob/363b9e036b2ba5d8...
This is a conversion; * calls the Deref trait. This is used by smart pointers to return the type inside the smart pointer, so for example, Box<T> is kinda like uniq_ptr<T>. So if foo is a Box<T>, then * foo is a T, and & * foo is a &T. Does that make sense?
> And why is triple dereferencing needed?
This can happen in generic contexts, sometimes, you end up with a &&String, so if foo is one of those, * foo is a &String, * * foo is a String, * * * foo is a str, and & * * * foo is a &str. (HN formatting does not like this, haha)
That's what's happening here.
> I thought I had read somewhere that Rust does auto-ref/auto-deref...
Only for method calls, that is, foo.bar() will attempt to auto ref/deref foo.
From a technical standpoint, it's highly suited to our business due to how well it does with concurrency along with its fault tolerance.
Here's a nice blog post by one of our Principal engineers on Elixir, it's a great read: https://www.pagerduty.com/blog/elixir-at-pagerduty/
Some clear syntactic differences (that clearly show that the design decisions were driven by building a good language over erlang, that happens to tokenize similarly to ruby, not a ruby-looking erlang powered language):
- ruby uses sigils on variables to confer metadata, no similar syntax in Elixir on bindings
- access modifiers are different (private vs defp)
- map syntax is different (%{} vs {}), tuple syntax doesn't exist in ruby. (could have been swapped if ruby-syntax was a higher goal vs idiomatic erlang being the driving motive)
- function definition syntax is slightly different, but arguably more consistent in Elixir (no do in ruby)
- most of the control flow operations in ruby do not exist as macros in elixir, or have wildly different syntax and vice versa. the only exception i can think of is the humble "if" statement, which is generally avoided in elixir if it can be anyhow
- closures and anonymous function syntax greatly differ
- the entire syntax of pattern matching and deconstruction in elixir does not exist in ruby
- no ternary operator in elixir
- pipelines are central to elixir, no syntax in ruby
I think Elixir's successful adoption by ruby programmers despite having nearly nothing in common with ruby than a similar choice in tokens for parsing (and even then, only when erlang doesn't pull in a different direction) show that just choosing similar tokens to a language can be a good strategy to enticing developers from that language without actually having to make any impactful design decisions (in either syntax or semantics) to favor that goal over others.
I've been learning Elixir over the past couple months and writing about it on my "Learn with Me: Elixir" series at https://inquisitivedeveloper.com. The idea was to give any interested developers a way to learn Elixir as I learn it. I do make the occasional comparison to languages like Javascript and C#, but any experienced developer should be able to follow along. I'm getting toward the end of the basics of Elixir and starting to learn about the more intermediate functionality.
I'm looking forward to getting into the more advanced features of Elixir and creating real software with it.
I find it interesting that Elixir is a common upgrade path for Ruby developers. I wonder if that's actually a handicap because the syntax similarities would cause them to make the wrong assumptions about how Elixir works. The syntax is completely new to me, so I had no preconceived notions of what the syntax meant.
In my experience, yup. Most Ruby devs, my self included before playing with Elixir, are not accustomed to dealing with concurrency. They try to make everything synchronous, and it goes downhill from there.
2) my number one wish-that-will-never happen for elixir would be the inverse of the mutability issue, variables should be immutable by default but have a sigil or postfix (like !) that lets you make it mutable. Of course, as it is, this is trivially implementable as a credo linter feature.
3) I think generally the biggest problem with macros is the risk of nonlocal side effects in your code, and elixir does a nice job of containing it lexically.
Just to clarify for others, and as the article notes, variables aren't "mutable" in Elixir but rather can be rebound. Even rust, where - just like you desire - you have to explicitly tag something "let mut" if you want mutability, will let you re-bind the variable without that tag.
I've programmed in Elixir full time now for two years and I can't recall any times accidentally shadowing a variable has bit me (but I do `mix compile --force --warnings-as-errors`) as part of my development routine, and that has caught it before for me.
(Oh, unless you mean you wish Elixir had _real_ mutability. That _would_ be quite the change, as obviously no functions in the standard library support that and you have to make do with GenServers and the like. I'm not sure I'd like that as the whole language ecosystem is predicated on immutability and message passing...)
While we're throwing out "one wish-that-will-never happen" ideas, though, here's mine: I'd love for Elixir to be able to automatically add guards to functions based on the type spec. That is, if you had this code:
@spec foo(String.t(), MyEnum.t()) :: boolean()
def foo(str, enum) do
# ...
end
I would love it if that got compiled to essentially: def foo(str, enum)
when is_binary(str) and enum in [:enum1, :enum2, ...] do
# ..
end
In general, dialyzer is the weakest part of the ecosystem for me, and I've had `nil` and other random things sneak into my functions before despite being against the type spec. I'd much prefer to have the function call crash early by just not matching any clauses.I'll write up my credo linter and see if you like it.
Your guard idea is pretty good, but I would appreciate it even more if those guards existed in dev and test and got yanked in prod.
It has one of the best communities around.
Still, someday in the future, it's a great language.
Maybe when my current (remote Ruby) job comes to an end I'll take a look. I fell into my current role, not quite sure where to start finding a remote roll! Anyway, thanks for the reply and encouragement :)
A complaint of mine about Elixir is that `with` is a macro and not a statement of the language. That probably made it easier to implement but moving code inside/outside a `with` is a pain: transform the = into <- and add a comma at the end of the line. This is against programmer happiness.
The feature I love most is pattern matching in function arguments. It makes the code more readable by removing almost every single case/if.
Then you later mention "programmer happiness". And I think an emphasis on "programmer happiness" is exactly what Elixir and Ruby share, and is probably the reason Rubyists are drawn to Elixir.
So Rubyists (I myself have been one since 2011) are drawn to it because it essentially has been billed as a successor to Rails when you need to be concerned with highly concurrent, fault-tolerant, TDD-driven web apps. Most companies who have embraced it are former Rails shops... most devs who use it are former Rails devs.
It could have been implemented without needing the same special treatment with something like:
with do
x <- foo()
y <- bar(x)
z <- baz(y)
blah(z)
else
err -> handle_err(err)
end with
x = foo()
y = bar(x)
z = baz(y)
blah(z)
else
err -> handle_err(err)
end
No do (uselessly verbose, it litters all the language) and = instead of <-
It should be normal code and take the else branch whenever there is any error, pattern matching included. The advantage is that the code can be indented in or out a with block, without any change. Much more convenient.Elixir also have a try/catch https://elixir-lang.org/getting-started/try-catch-and-rescue...
> In Elixir, we avoid using try/rescue because we don’t use errors for control flow. We take errors literally: they are reserved for unexpected and/or exceptional situations.
Using '=' insead of '<-' would break how with works. I can understand why one thinks the syntax looks weird. I avoid with when possible, but sometimes it's the easiest abstraction to use.
-> is like for everything else like case and cond
= is bind operator and is only used when you want to pattern match
I think this post has managed to convinced me to try what's on the other side of the fence for a bit.
Elixir's stdlib is designed completely differently though and the design patterns and execution models are completely different
Maybe the ruby crowd is more pronounced in the elixir IRC inner circles?