Elixir for cynical curmudgeons
wiki.alopex.li
wiki.alopex.li
I too had an ephiphany when I realized Elixir was actually a Lisp. The language went from mildly interesting to "holy crap I think this could be my favorite language." Add on some wildly productive and pragmatic frameworks like Phoenix, Nerves, and increasingly Nx, and you've got a hell of a toolkit.
My biggest criticism of Elixir has been the difficulty of writing quick scripts, but that story has improved greatly over the last couple of years. I still turn to Ruby for scripts (especially since it's often present on systems and so easy to install), but this is a good reminder that it's time to try Elixir again for that. In some ways it may be better, such as dealing with dependencies. For exmaple I try hard to avoid using any non-standard libs with Ruby scripts so I can just use the system ruby, and don't have to ship a Gemfile and/or install any gems, or mess with chruby/asdf. With Elixir that story looks even better since the deps can be part of the script itself.
Is it actually? Last time I looked at Elixir (granted, that was a while ago), the syntax for writing macros was different than the syntax for writing ordinary code, so it fails even at a simple thing like that, it doesn't seem to support homogeneous meta-programming at all.
But again, I feel like I might be wrong here and I'm happy to be proven wrong.
Even the regular syntax is sort of lispish, though, if you remove the syntactic sugar. It's all macros and datastructres.
defmodule Foo do
def bar do
"baz"
end
end
is really: defmodule Foo, do: (def bar, do: "baz")
...which is lispish if you squint the right way. defmodule(Foo, [{:do, def(:bar, [{:do, "baz"}])}])
Even aliases (like Foo) are also a kind of syntactic sugar for a specific type of atom (Foo is actually the atom :"Elixir.Foo")Curious what you find to be the biggest pain point(s) here. Is it runtime containment? Library access? Etc.
$ elixir test.exs
hello, world!
$ hyperfine "elixir test.exs"
Benchmark 1: elixir test.exs
Time (mean ± σ): 471.1 ms ± 2.6 ms [User: 187.4 ms, System: 83.1 ms]
Range (min … max): 467.1 ms … 475.6 ms 10 runs
Compared to: $ ruby test.rb
hello, world!
$ hyperfine "ruby test.rb"
Benchmark 1: ruby test.rb
Time (mean ± σ): 36.3 ms ± 2.6 ms [User: 26.0 ms, System: 8.4 ms]
Range (min … max): 34.5 ms … 53.6 ms 53 runs
In practice, I've been able to get the ruby startup time to as low as 6ms - this is what I use for scripts running as keyboard macros, for example. I cannot practically use elixir for that.BEAM should still be able to compile new code on the fly, for one-off scripts
Maybe, but that comes with two disadvantages:
- I would have to re-architect my scripts to work like that, and run a server with uncertain memory costs perpetually; and - I would have to use something with faster start time to run the call on my keyboard macros, e.g. bash, which would mean writing bash instead of elixir/ruby and/or being clever.
hyperfine "./derp"
Benchmark 1: ./derp
Time (mean ± σ): 241.0 ms ± 7.4 ms [User: 146.9 ms, System: 170.8 ms]
Range (min … max): 235.1 ms … 261.7 ms 11 runsThe ruby "story" for Gemfile dependencies for single-file scripts have gotten a BIT better since possibly last time you looked at it.
It will of course necessarily involve installing gems still though. Ideally for your use case, I feel like you'd want to install them to a local (not system-default) location ... which is ordinarily possible with bundler, but oddly the maintainers seem to think it's an anti-use-case for inline bundler? https://github.com/rubygems/bundler/issues/7131. I dunno.
Anyway, I put this here not to convince you, your decision is reasonable, but just other info for other readers.
It's not the first tool I'd reach for, I'm still a filthy bash hacker at heart, but I could definitely see using it for the right problem.
It's also just fun to write, which is valuable in its own way.
If your point is that the potential use cases overlap too much with Ruby, I mean, fine, write Ruby. I like Elixir more. I'm not picking either language for pure performance, but rather for ergonomic purposes, so we're into kinda subjective territory.
I don't think it will be great without heroics, but a little work and I think you can get it down to < 100ms.
"Over and over again, it turns out that there is no such thing as Magic, and anyone who says otherwise is skimming over details that will bite you in the ass later on. This is fine for many purposes, because 90% of programs are small hacks and one-off tools and so “later” very seldom actually happens."
Oh, yeah. This. So much this. A dump-truck load of this.
Even better when everybody is using different Magic, or the guy who set up this Magic left two years ago and everything has been done my cargo-cult since.
- concurrency is intuitive!
- Elixir is a two-for-one language. You can reach into Erlang std library as easily as you can that of Elixir. So, depending on your work, you get a chance to discover a wide range of goodies that come for "free". Examples include gen_tcp, gb_tree, etc.
- You can remote into a running virtual machine in production, pod or whatever, and have access to a repl that let's you inspect state, manage "processes", and manually invoke commands to troubleshoot issues. The observer, and tui counterpart, are quite useful too.
- everyone in the community writing libraries uses the standard approach to emitting telemetry, saving time and effort for users to take what is available
- fast compile times
- hardest working BDFL (although this presents risks, too), who also has a pleasant, respectful disposition
- the community is respectful
Things I don't like:
- dynamic language leads to pattern matching or type mismatch errors at runtime that could have been caught at compile time
- Elixir Phoenix doesn't have a large community, nor much up to date reference material, or literature. If you're not experienced with Front-end dev, it's easier to onboard with a modern SPA framework like React.
- supply of Elixir talent exceeds demand, although this isn't unique to Elixir (e.g Rust)
- slowly growing ecosystem, relatively speaking
Once you get away from those things get sparse. Several really common libraries have glaring omissions (or entirely missing) module docs, and others just barely cover the common web use case.
It’s not uncommon for me to have to open the source code to try and understand what some function actually does.
Elixir is still my language of choice and has a better docs ecosystem than most languages but the community demonstrates a bit of a blind spot also.
I also feel that Pharo has too small if a community. The ESUG conference is coming up soon though. It’s good to see they’re coming together
Why is concurrency intuitive?
Granted, I did learn the hard way about introducing cycles but the error messaging lead me to the root of the problem quickly.
Is this true?
Every time they go on the market for an elixir one, they cannot find good jobs and the few good one reject them.
As I regularly point out, i have hired whole elixir teams in 2 months multiple time in the past. If you cannot find us, the problem is that your hiring process and work practices are pushing us out. Not that we do not exist.
It's still early, but there are plans to introduce a type system for Elixir:
https://elixir-lang.org/blog/2023/06/22/type-system-updates-...
It will also have benefits for LSP feedback. It could also lead to more information being passed to the new BeamAsm JIT compiler for more compile time optimizations and faster execution.
If you're on the fence on Erlang or Elixir, watch The Soul of Erlang and Elixir by Sasa Juric: https://youtu.be/JvBT4XBdoUE
If it doesn't make you excited about a language, I don't know what will. It is an astounding feat that the Erlang team at Ericsson were so ahead of the curve and remain so today.
I just can’t get past all the extra verbosity of Elixir, but this writeup gives me hope that someday I will.
I just go all in on Elixir module design just like I would in an ML dialect. So structs, module types, custom types, typespecs, docs, Credo turned up to 11, run Dialyzer, etc. So I embrace some of the verbosity. Some don't prefer the style, but those are all features already there, and for me, it helps bridge the gap for Elixir being dynamically typed. Although there are developments coming to give it a static type system.
> `def` is a macro.
> `defmodule` is a macro.
> It’s all macros. It’s ALL MACROS! ALL THE WAY DOWN! AAAAAAAAAA AHAHAHAHAHHAHAAAA!
Also see the definition for `defmacro`, the macro for defining macros, which is defined using... defmacro
https://github.com/elixir-lang/elixir/blob/v1.15/lib/elixir/...
IMO the biggest tripping point for people (including, clearly, this author) is the freedom. They said that the "Big complicated project structure" was a stumbling point - but there is no structure! You can actually do it any way you want. But there are conventions. Mix suggests that you lay our your project in a certain way (and most Elixir projects are laid out that way) - but you are not required to by the language.
Nearly everything in the language is a convention (including the test / dev / prod run modes). This applies to Erlang as well. It has standard approaches, but you don't /have/ to follow them. I frequently get confused about the way libraries tend to approach something (do you have to do it this way? Is there a requirement?) and the answer is generally "no, but the community tends to think this is the best way to do it and so people generally do it this way."
A great example of everything being a convention: structures aren't a language feature. They're maps with special keys and Elixir library support. The upside and downside to this is that you could actually do almost anything with them - if you have the time and the skill to implement the system. MANY things are like this when you get into it.
I'm all for the immutable / message-passing architecture in the large, but I wish that you could just write Python-style code within processes. The VM copies all data structures at process boundaries anyway.
I think that language would be very popular!
I wrote Lisp 20 years ago, before Python, but now it feels a little annoying to "invert" all my code, for what seems like no benefit
e.g. from https://learnxinyminutes.com/docs/elixir/
defmodule Recursion do
def sum_list([head | tail], acc) do
sum_list(tail, acc + head)
end
def sum_list([], acc) do
acc
end
end
I would rather just write def sum_list(L):
result = 0
for item in L:
result += item
return result
But then also have the Erlang VM to schedule processes and pass immutable messages.There is obviously a builtin in Python that does this, and I'm sure there is a better way to do this in Elixir. But I think the general principle does hold.
Mutability within the loop is useful, and it can be controlled by processes and message passing.
Besides being in that magical sweet spot (executes imperatively but reads declaratively), a reduce forces you to declare ahead of time what things can be carried over and mutated between each iteration.
> it can be controlled by processes and message passing.
DO NOT DO THIS YOU WILL REGRET THIS SIMCITY MEME
iex> for n <- 1..4, do: n*n
[1, 4, 9, 16]
Or for your example: iex> l = 1..3 # equivalent to [1,2,3]
iex> Enum.reduce(l, fn item, acc -> acc + item end)
# or
iex> Enum.reduce(l, &(&1+&2)
# or the built in
iex> Enum.sum(l)
# all return:
6
https://elixir-lang.org/getting-started/comprehensions.html
https://hexdocs.pm/elixir/1.12/Enum.html#reduce/2
https://hexdocs.pm/elixir/1.12/Enum.html#sum/1None of which take away from your point though: it's definitely a different mental model than you use for Python/Go/Javascript.
I should have picked a different example -- the idea I was trying to get across was that mutability within a loop is useful.
Here’s a similar question I asked with Python vs. OCaml and Lisp. Short-circuiting loops seems to cause the code to become fairly ugly in functional languages
https://old.reddit.com/r/ProgrammingLanguages/comments/puao1...
sum_list = fn list ->
for item <- list,
reduce: 0,
do: (sum -> sum + item)
end
sum_list.([1,2,3])
Other comments are correct that `Enum.reduce/2` is probably better: Enum.reduce(list, &(&1 + &2))
Obviously you don't have the flexibility / mutability you refer to in other comments with either of these, but you can always put more in your accumulator: get_sum_and_update_count = fn list, map ->
for item <- list,
reduce: {0, map},
do: ({sum, map} -> {sum + item, Map.update(map, item, 1, &(&1 + 1))})
end
(A contrived function that returns a list sum and an updated map with the count of the values found in the list)Admittedly, I haven't worked with Erlang or Elixir, so their solution to this might be much more usable. Ray is aimed primarily at ML use-cases, though it is quite general.
The biggest offenders IMO are commanded, Tesla, absinthe, exmachina and now most recently ash. Then there's tons of tiny libraries that litter hex.pm with macro-heavy nonsense.
At least the core++ libraries make macros whose syntax/keywords matches something outside elixirland that you'd have to know anyways (e.g. 'get' from plug, all of SQL from ecto), but the big offenders list are making up clever names for things, which adds mental overhead to literally everything you do, building crazy complicated compilation pathways (especially for overeagerly lazy stuff) or writing function names from whole cloth that make refactoring or searching your codebase a nightmare (or more than one). E.g. if you're working in a domain where non-proud camel case is the convention, don't automatically snake-case it.
Overall though this isn't something I've generally run into too much with Elixir.
Anyways my prediction is about Non-core++ libs, which I listed in gp.
- Many of the non-core++ libraries are considered done
- Many popular and upcoming projects have zero macros: Decimal, Jason, Telemetry, Mint, Finch, Req, etc
- All machine learning projects have either zero macros (Axon, EXLA, Bumblebee) or the macro layer is built on top of the functional API (Nx and Explorer)
- Phoenix LiveView, which is the most recent project from the Phoenix team, is very minimal on the use of macros
- Most of the Phoenix sub-projects have zero-macros: phoenix_pubsub, phoenix_html, phoenix_ecto, esbuild/tailwind, etc
I mentioned this in another comment but in my opinion Phoenix is low on the use macros. They are present in the endpoint+router but then you are working with functions. I speculate the reason why they stand out is because they are the entry-point of your code. But even then the socket API in the endpoint is being replaced by a functional API thanks to contributions from Mat Trudel and the new verified routes are way less magic than the previous route helpers.
These are all truly fantastic Non-core++ libraries. Also some really good ones on the horizon like Bandit.
But I'm love elixir and I'm not calling out the good ones. I'm calling out the less good ones. Because I see a lot of young libraries that pick up on those patterns, and so I can only believe it's spreading (can't be bothered to actually quantitfy, sorry). I don't think I'm bringing a curmudgeon. Legibility and debuggability are real concerns.
While Elixir people usually say this, they don't at all follow it in practice because of the terrible example set by all the libraries. Phoenix is especially egregious and I can't believe the grandparent didn't bring it up.
Looking at Clojure's solution that is (as far as I can see) very popular it makes me annoyed at the relaxed attitude towards macros in Elixir and how bad it actually is:
Elixir and `Plug.Router`:
defmodule MyRouter do
use Plug.Router
plug :match
plug :dispatch
get "/hello" do
send_resp(conn, 200, "world")
end
forward "/users", to: UsersRouter
match _ do
send_resp(conn, 404, "oops")
end
end
Clojure and `reitit` (https://github.com/metosin/reitit): (def router
(r/routes
[["/hello" {:get (fn [r] {:status 200 :body "world"})}]
["/users" {:name :users
:router users-router}]
["*" {:get (fn [r] {:status 404 :body "oops"})}]]))
basically everything in the Elixir case is a macro and non-standard syntax whereas literally of the components of the Clojure case are just standard data types, and yet it manages to be more readable.“Readable” is mostly a matter of “fits the grooves already worn in my brain”; to me, the Elixir there nonstandard-macro-based-syntax-and-all, is vastly more readable than the Clojure.
I'll admit to being able to read Lisp syntax without choking on the parens, but I don't even use Clojure (or any other dialect that uses special syntax for the different collection types) and I think it's way more readable and malleable (you can literally do whatever you want that makes sense to this data and it'll work, meaning you're free to write whatever middleware you want that takes and produces the expected data).
I've used Elixir since 2015 so I have no problem actually reading the syntax at all; I just think it's factually much worse than the Clojure example because it's basically just a bunch of macros you can't do much with and lots of implicit behaviors.
This also applies to Elixir's greater position in the BEAM ecosystem. I think the idea was that Elixir was going to be a boon to Erlang and other languages in there over time but I think it's demonstrably pretty useless as a BEAM citizen except for some of the patches they've submitted; most big Elixir libraries can't even be used outside of Elixir because they're full of macros and don't even provide interfaces with just functions.
Not sure whose idea it was but I would say this is a very inaccurate take. Of course, we have taken much more from Erlang than given (that's expected from hosted languages), but I personally sit on more than 100 PRs to Erlang/OTP. I made binary matching several times faster by using SIMD. I improved the compiler performance (including Erlang programs) by (conservatively) 10-20%. I contributed compiler optimizations to reduce memory allocation. There is a now faster sets implementation which I contributed. Erlang/OTP 26 ships with faster map lookups for atom keys based on a pull request I submitted. The new logger system in Erlang takes ideas from Elixir logger. The utf-8 string handling in Erlang is based wholesale on Elixir’s (and extended for lists). And this is in no way a comprehensive listing [1].
And this is not counting everyone else in the Elixir community who contributed and those who are now actively working on Erlang tooling and were on-boarded to the ecosystem via Elixir. The Erlang ecosystem now has a package manager [2], used extensively by both languages, rebar3 and Mix, all implemented and powered by Elixir.
And then there are efforts like the Erlang Ecosystem Foundation, which is made of developers and sponsored by companies working with both languages, and it delivers important projects around observability, documentation, and more. For example, you can find several Erlang projects now using ExDoc for documentation, such as Recon [3].
That’s my take. What are your demonstrations that we are pretty useless for the BEAM ecosystem?
1: https://github.com/erlang/otp/pulls?q=is%3Apr+author%3Ajosev... 2: https://hex.pm/ 3: https://hexdocs.pm/recon
It is. That they have not replied to any of your responses shows they don't have much argument.
I mean using "factually" in this subjective opinion is just ridiculous.
> I just think it's factually much worse than the Clojure example
My hyperloglog erlang lib is one of the most advanced in any language (behind what Omar Ertl does for Java) and i come wholesale from elixir.
I have my own code, as an elixir person, in OTP, for the formatting of float to string. The maybe construct was only possible because elixir with showed the way.
I could probably keep going.
There are lots of silent fans of your teams' work. Please keep it up, and please feel proud of your collective outputs!
I was just reading the documentation, where it says "remember that macros are not your API", and smirking. If you write macros and things depend on them, such that if you change the macros, things will break, macros are your API. Syntax is API.
My huge beef with plug is that it creates an implicit conn variable in all of the plug bodies.
But yeah, phoenix is a combination of good choices and some real stinkers, and I did mention it.
Edit: I forgot about match. match "isn't great".
What do you mean? That only happens if you use Plug.Router; otherwise, you have to define a call function that takes the conn as the first argument. You can even go as far as explicitly calling the init functions for each Plug manually. It's init(opts), call(conn, opts) all the way down.
That is one thing that really bugs me, is I can't automatically see where the arguments are coming from. I realize it makes code a little more verbose, but a requirement around explicitly showing the dependencies used is much more useful when you're refactoring, for instance, or scaling code.
not a hard requirement by any means, sure, and it definitely doesn't mean something won't scale as it were, however I am constantly frustrated by languages that allow for implicit scoped dependencies / arguments.
https://github.com/elixir-plug/plug/blob/main/lib/plug/route...
Everything else is explicitly passed, AFAICT.
When I pivoted from Ruby to Elixir, I was expecting to do a lot more metaprogramming, but I found that regular functions with pattern matching already does a lot that I want for many cases.
Macros in business code, however, are pure evil. I feel like anyone who spent any time in Rails over the past decade has learned that lesson.
Thank goodness they got rid of views though I still am annoyed that the default path resolution on templated content isn't explicit.
The crazy thing is that the most amazing part of phoenix, liveview, is almost no macros.
What's wrong with the `use MyApp.Web, :x`? You can easily get rid of it as it's just generated boilerplate. It's a nice little pattern to allow all the different things to share the view helpers. If you'd like to be extra explicit, though, just delete the file and include everything manually.
I'm indifferent on views because I never really used them, haha.
defmodule MyApp.Web.Router do
use MyApp.Router
end
over defmodule MyApp.Web.Router do
use MyApp, :router
end
but that’s a preference. defmodule MyAppWeb.Router do
defmacro __using__(_) do
quote do
use Phoenix.Router, helpers: false
import Plug.Conn
import Phoenix.Controller
import Phoenix.LiveView.Router
end
end
end
But why the dislike for passing a second argument to `use`? It keeps everything together pretty nicely.How would you prefer it worked/looked?
> I still am annoyed that the default path resolution on templated content isn't explicit.
What do you mean by this? In the new 1.7 approach to views, you explicitly set the path in each view using `embed_templates` (if you're not defining your HEex directly within the view module itself.) What part isn't explicit?
For example, if you wrote a liveview called FooView in /path/to/my_liveview, a default render() will look for /path/to/my_liveview/foo_view.html.heex
(Also note auto-snakecasing)
Btw, this is still a different semantic if you do the deadview equivalent, which will try to do a path resolution in /templates.
I guess these are minor complaints but I would much prefer they use the same default strategy and, say, pass the path in `use Phoenix.Liveview`
Not true in 1.7 - views* don't have any kind of "default" template resolution; you have to explicitly state the template path with `embed_templates`. That is if you're not just defining your HEEx directly within the view module itself, as I already mentioned.
*By "views" I mean the new style that uses `use MyAppWeb, :html`, not the old style you describe which is now (in 1.7+) only available as the external dependency `phoenix_view`. The new style of module is still called a "view" by the docs: https://hexdocs.pm/phoenix/request_lifecycle.html#from-endpo...
I don't have experience in Elixir, but in my general experience what you say is fine until you need to debug something going wrong (or something you don't understand and _think_ is going wrong) in a library, which always happens eventually, no? Or until you want to PR a feature, etc.
Edit: lol looking at the log in elixir slack, and one of the most recent posts is exactly trying to debug something too magical in one of the aformentioned non-core++ libries!!
No matter how slice it, however, macros are a massive part of what makes Elixir Elixir. A large portion of the language's "keywords" are just macros and they reduce a lot of the boilerplate Erlang forces you to write.
GenServer is kind of a "bad example" of a macro. It creates a totally hidden function. Though I guess to be fair 99.9% of the time you really shouldn't be writing genservers in elixir.
Woah! Why is that?
I've written at least one GenServer in nearly all the Elixir projects I've worked on. They seem like one of the base building-blocks to me. Also, if you squint a bit, you'll see that many libraries you're working with are essentially exposing a GenServer interface, with a few extra features.
Like LiveViews :)
> Also, if you squint a bit, you'll see that many libraries you're working with are essentially exposing a GenServer interface, with a few extra features.
I never said you shouldn't be using GenServers. I said you shouldn't write GenServers.
Wow, that's quite surprising! We use GenServers quite extensively at work. The most interesting usage is probably this application, which uses GenServers to model the state of real-world hardware (in this case, transit stop countdown clocks): https://github.com/mbta/realtime_signs/blob/main/lib/signs/s...
Curious what you use instead of GenServers. Plain processes? Agents? Something else? GenServers are probably overused but I still reach for them when I need a concurrent bundle of state with a specific interface and behavior.
I think we're generally trying to write more GenStages than GenServers at the MBTA these days, but that's mainly because we're moving towards being event-driven. GenServers still have their place.
Pedantically I guess gen_statem isn't a genServer, but I'm generally referring to the pattern, which gen_statem is.
Mostly webdev, so, that stuff is all taken care of for me.
MBTA is modeling reactive systems that exist in the real world, so yeah, that's a place where i would be surprised if you didn't use genservers, though I suppose you could just use a database-backed state machine. Surprised you use GenServers instead of gen_statem (though you might have been, as I was, less than pedantic)
The question of when to introduce databases is a really interesting one and also one I'm still coming to terms with a few years into writing Elixir full-time. Many of our apps don't use them at all, but I think at least a few could benefit from more structure and durability in their datastores.
What do you use gen_statem for? I'll admit its intended use-case has always seemed pretty narrow to me, but maybe I just don't understand it very well.
I understand why the callbacks in gen_statem are so messed up though, they want you to be able to organize your code by state. Since Erlang and (weakly) elixir requires you to group handle_x callbacks linearly in the module, you can't group by state unless you have a `handle_anything`. It's all so messed up :(
Are you talking about child_spec/1? `use GenServer` honestly provides some pretty good QoL enhancements over `@behaviour GenServer`. I don't know when or if there was a change to recommending `use GenServer`, but I was pleasantly surprised when I started using it. Especially because I'm not writing new GenServers all the time, having guard rails is nice to avoid being slowed down by remembering all the boilerplate: init/1, handle_x/y, child_spec/1
It's even nice enough to give additional information about why its crashing so you don't get confused why e.g. the handle_call function is working. I make this mistake almost every time I write a new GenServer:
def handle_call(:ping, state) do
{:reply, :pong, state}
end
Before, you would just get this meaningless gen_server stacktrace that basically just says "it crashed lol". Now you get the additional hint of "hey you didn't define handle_call/3" when it crashes.Edit: just checked, it warns if you don't... I have compiler warnings as error on all my personal projects so that's why I thought you had to do it manually
1. They make it easier for consultancies to templatize what they do, which:
- keeps their employees tied to the hard earned knowledge
- keeps their clients tied to their services
At least though you know that those patterns are (usually) common and battle tested (or, at least, will be battle tested - cough cough ash), and unlike a bigtech "their problems are likely to be your problems".
I have heard this claim so many times but it is easy to prove it wrong given how much we focus on documentation and understandability around Elixir and main libs. I have had clients asking me questions and I would answer it by improving the docs or writing a guide, committing it to the repo, and then asking the client for feedback. Half of the Ecto guides were written like this.
It is not about keeping clients at all. It is about having a great on-boarding (and beyond) experience.
I'm not saying that these libraries are deliberately designed to be malicious. I'm saying the incentives don't always align. And some of that has to creep into the mindset of some of the design.
Also I wouldn't level the "crept into the mindset" accusation at Phoenix core or ecto or absinthe even, because for the most part they conform to expected terminologies.
I've been using Ash in a side project and it's slowly growing on me. You have to twist your brain a bit to grok it (and I haven't, not fully).
Pros:
- a lot of things happen where you want them to happen.
I found that writing stuff with Ecto involves more boilerplate than with Ash because you have to write your queries, and your changelogs, and your API accesses, and wire them together, but what if you want a separate API.... With Ash it's a single resource with a few macros.
- quite a few escape hatches into regular Elixir
You don't want magic behind actions? There are several ways to hook into what's happening, or even write everything manually. And to the calling code... it will still look the same
- Despite all the macros the errors are usually quite good and pinpoint the problems
Cons:
- too few people who truly understand Ash
- documentation is often too terse and concise. But you can't really blame the author who is doing a lot
- errors are not always good :)
I do agree with you on overreliance on macros in parts of the ecosystem
The data science project did a sql query that I couldn't figure how to optimize in SQL and I turned it into simple elixir code. Queries that took minutes now took seconds.
Now, I know that ash can do both but I don't know how I would diagnose that problem or test alternatives in ash, because it hides way too fucking much of the underlying code.
But I do get what you mean.
- Tesla has a mode which can be used completely without macros, and I am increasingly encouraging that it be the only way that it is used. So does the author (as of 2020): https://github.com/elixir-tesla/tesla/issues/367#issuecommen...
There is also `req` mentioned in a recent post as an alternative (it looks good, but I am still playing with it to see if it is a suitable replacement for Tesla in all cases).
- Absinthe is something of a compiler itself, because it has to strictly define things the way that is specified in the GraphQL spec. You can now import an SDL file, but you still need to hook resolvers and middleware into it. Honestly, I don’t think that the schema definitions in JS/TS are much better for GraphQL in terms of readability.
Being heavily macro-based means that there are sharp edges that are harder to work around when you want to add your own macros for code reuse purposes. That said, aside from the schema definition, Absinthe is entirely usable without macros. Within the schema definition, Absinthe isn’t making anything up, it’s using the same basic definitions that the GraphQL spec do, adapted for Elixir syntax.
Exmachina didn’t interest me because I don’t think much of factory_bot (which used to be called factory_girl), as I saw it abused far more than used well (IMO, it’s impossible to use correctly). Ash…looks like an interesting experiment, but I don’t know that there’s a lot of pick-up with it compared to Phoenix. And I have yet to find a use for CQRS/ES, so there’s no reason for me to play with commanded. I certainly wouldn’t consider any of these three to be "major" players in Elixir. Tesla and Absinthe? Yes.
The circular compiler dependencies of absinthe really ticked me off. Was on a project where one small change elsewhere would mysteriously trigger absinthe recompilation which would trigger even more recompilation. Might or might not been fixed in one of the newer compile-slim elixir releases
There's a fine line between making things easier and more readable and having less boilerplate code, and pouring a bit too much magic and macros into things.
I'm always curious to see what people do with Elixir. The BEAM environment offers a lot in terms of a platform, but if you're just using it to serve web dynamic web pages, Rails is going to be a better pick in some circumstances because of the huge number of libraries, people who know it, documentation, hosting and the rest of the ecosystem.
There’s quite a big push for ML in Elixir right now, and then there’s also Nerves for embedded programming, which I’ve never dabbled in but it looks nice.
You just have to look. Blazor does what LiveView do and on upcoming. NET 8 it will leapfrog it.
Where can I see what .NET 8 will bring to Blazor to change this?
The big thing that LiveView has going for it is that it transforms the client session into just another process to communicate with, using the same message passing tools as the rest of the language, with very little headache because concurrency is already an expectation.
Blazor can never achieve this, because C# is not a concurrency focused language.
This isn't really as big a problem as many people think. I've been out of Rails for a while now but haven't really felt the pain of missing anything with Phoenix. I've noticed there aren't often Elixir wrappers around third party APIs, but those are really not a big deal to create a small version on your own with only what you need. I always used to wrap the wrappers anyway! As far as UI stuff goes, there are actually a lot of really nice vanilla JS libs out there which play really nicely with LiveView's JS hooks.
I would be interested to hear what people have been sorely missing, though.
EDIT: Said "are" when I meant "aren't"
If I'm putting on my business hat, I need a reason to use Elixir instead of Rails and that probably has something to do with a better concurrency story, but I'm always curious to hear what other people are getting out of it.
My previous experience with BEAM was using Erlang in a semi-embedded environment and it was fantastic for that. Solid, dependable, deterministic, great response times... all of that. But while we did have a web interface to the system, it wasn't a web site.
In a current project I'm also able to use OTP to create a single microservice for my monolith that runs on its own node and the two can communicate with run-of-the-mill message passing without the need for an HTTP API.
Dist messaging and mnesia scale pretty far. Although it must be noted that they were original built around rock solid networking (two nodes in one chasis), so if your networking is flakey, you have to handle that; my recommendation is to make your network not flakey, but that's easier to say than it is to do.
That said, redis is in memory key-value, and Mnesia does in memory key-value too. If you're using Postgres's relational stuff or data bigger than memory, that's a different thing. Mnesia has some relational facilities, but I dunno if anybody uses them much; mnesia can use disc_only_copies, but that's not a good time. Of course, memory only gets you pretty big tables these days; I ran some mnesia nodes with 768GB, but a couple TB isn't too hard to put together these days.
The reality is that asynchronous, parallel, distributed computing is hard, Erlang makes it easier. Not easy, but easier.
1. Web apps work awesomely because they tolerate sudden bursts of load much, much better than Rails. Rails deployments regularly fall over on bursty workloads unless complex load-balancing setups have been put in front of them.
2. Which brings me to: operations / infra story is much simpler, and it is thus cheaper and easier to maintain Elixir apps, even when they span multiple nodes (because Erlang's builtin mechanism to do mesh networking / distribution works well enough for 99% of the projects out there).
3. Complex service trees f.ex. scrapers that also present their data in a visual CMS backoffice UI and background tasks infrastructure -- all of that can live on literally the same node, share the same code repo and be very easy to manage and maintain as a result.
4. Oh, that also reminds me: almost anything and everything you would need Redis for, you can have it natively with Erlang/Elixir.
5. Corollary to point 3: long-lived daemons that are not even web apps / APIs work really well under Elixir due to the supervision tree guarantees of Erlang's underlying OTP framework. If you configure it well (the defaults are not always optimal) you can have your invisible mesh of services survive extended outages of the 3rd party APIs it depends on.
6. In the last years, LiveView and Livebooks are amazing additions that help severely reduce the need of JS and provide an alternative to Jupyter Notebooks, respectively. Though it has to be said that LiveView works best when the latencies to the backend aren't too big, say, 200ms or less. Beyond one certain human perception threshold it starts to feel sluggish however.
I could go on and on and give plenty of examples from my career. I've replaced a lot of shell scripts and PHP / Python / JS services with Elixir, and each migration was a crushing success in every way. We're talking 15+ such projects. Only 1-2 of them got back to their original language simply because the companies / teams were too risk-averse and didn't want to hire Elixir devs (or train their current people). But the network effects are always a chicken-and-egg problem; if people never want to bet something on the tech then it obviously is never going to be appealing to them.
There are many who made the plunge though and are happy with the results.
This is something that annoyed me a bit with OTP. The basic strategies aren't really enough for that, so you need something like https://github.com/jlouis/fuse
I wrote something like that myself, but it hasn't seen a ton of use: https://github.com/davidw/hardcore
In my comment above I was simply addressing the fact that you can e.g. configure 500 retries before the supervisor gives up as opposed to the normal 3 (or was it 5?).
We only really encountered the lack-of-libraries issue a single time (we needed a Neo4j driver that supported their hosting platform).
But aside from that, Elixir was a great choice and served us well. When our app went semi-viral all we needed to do was increase our instance size in AWS, never really needed to worry about horizontal scaling thankfully.
Our app also needed a lot of realtime messaging and updates, which was genuinely a breeze with Phoenix. I don’t think I could ever to back to trying to finagle a websocket API in Python.
It makes for an interesting time when deploying to Kubernetes. But we figured out how to increase the number of cores per erl node during peak daily traffic.
I’m working for a Nodejs shop now. For all its use of async stuff, it simply does not scale as well as Elixir (not to mention the deep dysfunctions in the Nodejs ecosystem).
If you do a lot of cross node talk with say, the Phoenix channels, then lowering the total number of nodes helps (n*(n-1) for a full mesh). And since the BEAM scheduler will try to use up all available cores by default, past a certain threshold of nodes, it makes more sense to use smaller numbers of nodes with more cores per node. When you also have say, an observaility agent taking up 1000 mcore, doubling the instance size makes a difference. In the last shop, we would idle at 4-cores, jump to 16-cores instance sizes for the morning rush, and go back to 8-cores for the late afternoon traffic. I had tried to horizontally scale it with 4-cores machines, but saw a lot more instability and performance issues.
This is one of the advantages with using Elixir or Erlang. You simply don’t get this kind of operational characteristics with Ruby, Python, or Nodejs.
One of the things I had success with in a non-web applications is writing Kubernetes Operators in Elixir. Though I also want to experiment with the embedded Prolog-in-Elixir for those.
That's me vs web development in general and especially in the JS ecosystem. Other, similar red-flags: hand holding, "best practices", "anti-patterns" and other voodoo and cargo culting.
The most suspicious one is prettiness:
- custom DSL syntax without data representation
- cosmetic wrappers that are just thin layers without substance
- "everything is an X" for the sake of faux consistency
This is an interesting comment in a post about Elixir because the Elixir ecosystem is actually full of things that should just be data but are instead macros being used in modules. Phoenix in particular is an especially egregious example of a library that is badly made in almost every regard
> - cosmetic wrappers that are just thin layers without substance
This one is also funny because Elixir itself is little more than a fairly small set of convenience libraries, a syntax and a very good tool for project management (`mix`) on top of Erlang. You can skip the syntax and the convenience libraries for the most part and actually just use Erlang in a `mix` project and get the best of both worlds. The convenience libraries usually are bad makeup on top of Erlang in many cases and you'd be better served just learning the Erlang way anyway.
Can you elaborate on this? Why do you think this?
The non-standard (won't work with anything else) and basically superfluous websocket protocol has bad fundamentals and has been marred by bad JavaScript code being needed for it. On top of that it's pretty bloated.
The actual channel processes could've simply followed a standard Erlang interface but someone (...) felt they needed to put their fingerprint on it, I guess, so it kind of doesn't. And yes, again everything surrounding channels is a bunch of macros until you at least get to write and use normal functions in the actual channel module.
They've had a long history of arbitrarily deciding to hide settings/configuration from dependencies they use and having no actual argument for doing so when asked to expose them.
If the part about macros being overused wasn't bad enough Phoenix also promotes this type of thinking by overusing them even in generated code, so people can follow the bad example.
This is not true. The endpoint and router are macro based, but once it is routed, it is all functions. Your controller actions are functions, calling the view/template is a function, your channel is functions, the model/context layer are functions.
When it comes to behaviors, we do our best to follow Erlang/OTP conventions, but deviate where it makes sense. For example, channel has “join” instead of “init” because you can refuse a join but you can’t refuse an init. They have different return types and augmenting init could be confusing if later you were to write a plain GenServer.
It is easy to look from outside and say “someone wanted to put their fingerprints on it” but actual discussions were had in pretty much all of these topics. I suggest asking around. If you asked for something and we had no actual arguments, then please point me to such occurrences. I will be eager to clarify (and as a rule we generally do).
Since you're here, can I ask why Elixir doesn't have an explicit `return` statement? I do miss it sometimes.
If not, your criticisms seem to hold no water.
It may be "little more than" convenience, but especially consistency decisions like always data-first functions plus the pipe macro is already a monumental ergonomic leap over erlang. I love erlang but, like php, it is a language where I have to look up the argument order and config options of every function every time I use it.
For most of the people who actually use Phoenix, the DSL makes intuitive sense and so there isn't much learning curve, and it is such an important library to applications that use it that its worth taking the time to really learn it, whereas it might not be the case for other libraries that are macro heavy. Most libraries don't go that route though.
If you've ever tried to write keyword lists in erlang.... The keyword list cosmetic wrapper is fucking amazing, not only because it makes it easy to read, but it makes it easier to debug and easier to one-glance see "this is a keyword list".
I heard the argument before that JS (and Nodejs) is popular (top 3), so you should go into it.
I’m now in a nodejs shop. Turns out, the way Erlang and Elixir is designed serves as a filtering function for hiring. People who just wants to code without putting a lot of thought into it, don’t go into Elixir and land in JS instead. (All the current coworkers I have wants to make things better but we are saddled with code that has been … oddly written)
At the last Elixir shop I was at, I was part of a small crew that was more experienced as a whole than other shops I have been at. And I liked it.
One caveat to any of it is the easier a language is to grok and the more use cases it has, the more likely it will attract all types of developers and there will always be more "bottom end" developers than "top end" developers, as is patterned out in any large distribution.
That said, here's some facets to consider about this
- Its not likely its the language (or to be charitable, not just the language) at play here. As noted above, the more popular something is, the more likely you end up with a wider varieties of folks at very different backgrounds / skill levels etc. Python and PHP have similar issues.
- The more popular the language is, is often correlated to its use cases. For example, JavaScript is hugely popular in a wide swath of problem domains, from the server, to the web (its de facto monopoly right now) to desktop apps to even now mobile (Ionic, NativeScript etc). This leads to the top developers in the language often going to very lucrative positions that one would also presumably have to be working alongside or as part of to be working with them
- Its harder in a bigger pool to work with the top N % of developers in any given language or (better yet, any given environment / problem space deploying that language as part of its core technological backbone). The big "sea" in the middle is what you're most likely to be apart of
I think smaller communities benefit from organic interest, people who are enthusiastic to use the technology and therefore often the perceived quality of those developers is higher (and often it does correlate, see: the history of Clojure)
I don't think a language in and of itself is to blame, except in that maybe, because some languages like Python and JavaScript are so easy to pickup as opposed to say, Java, Kotlin, C# or Rust, that they attract more developers in general. That said, I think its the laws of distribution at work, and not really the driven by the language per se.
Typescript's type system isn't sound and you can still get runtime errors. (Type systems should be bomb-proof or just get out of the way.)
Examples:
type Day is
(Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday);
subtype Business_Day is Day range Monday .. Friday;
subtype Weekend_Day is Day range Saturday .. Sunday;
subtype Dice_Throw is Integer range 1 .. 6;
type Arr_Type is array (Integer range <>) of Character;
type Color is (White, Red, Yellow, Green, Blue, Brown, Black);
type Column is range 1 .. 72;
type Table is array(1 .. 10) of Integer;
type Device_Register is range 0 .. 2**5 - 1 with Size => 5;
type Commands is (Off, On);
type Commands2 is new Integer with
Static_Predicate => Commands in 2 | 4;On the other hand, I get it all the time with Python, for example.
Like any ORM, the abstraction leaks, but it really throws you off course when you're trying to map the host syntax to raw SQL.
from(t as SomeTable, where: x.z == ^y, join: x on assoc(t, :y))
Like, it's macro enough to make SQL feel more first-class...but not enough to actually make it first class, so you've gotta learn Ecto's quirks.It's also chainable so it's a macro over a query builder that eventually resolves to SQL at runtime.
I am interested in Elixir though, especially Phoenix, but found it to be difficult to manage and get started with in Windows.
Still looking for the easy way into learning to work with web sockets as a 41 year old man with very little spare time.
You could also use containers pretty easily as well with docker or podman for windows and the official Elixir images[2].
Water.css, MVP.css, sakura, and Tacit are among the most popular.
I know the SASS issue was solved (as a side effect of eliminating the node dependency), and I believe the bcrypt issue was, too.
1) probably not stick around for long
2) need to be trained from scratch
3) not have a solid development background
Would Phoenix be a good choice, given that you can do so much with it without needing to learn many other tools, or would it be better to start with something like Supabase + a javascript front end framework and hope we'll never have to deal with monsters like kubernetes?Elixir is great, and developers love it but training new people without a solid development background might be hard.
https://elixirforum.com/t/elixir-vs-python-performance-bench...
They compare things that have nothing to do with production, use non realistic code for most language and keep refusing PR to fix it for elixir.
The community simply decided to stop trying to engage and let that code die.
Perhaps if it was served by Phoenix... /obvious comment is obvious
It says at the bottom: powered by https://github.com/jgm/gitit
Readme states that: "Gitit is a wiki program written in Haskell. It uses Happstack for the web server and pandoc for markup processing."
Excellent writeup!
there are other datalog-y tools as well, ie https://github.com/naomijub/translixir
It has the interactive features for inspecting and modifying a running system that Lisps often have.
It has reflection APIs
Its ints are big like Lisp
etc.
Seems pretty Lispy to me.