so, I’ve been learning Elixir and Phoenix for some time now, I’ve built some web apps for practice(CRUD), used Ecto quite a lot, just started playing with LiveView etc. There are more advanced Elixir features and concepts like protocols, OTP, Genservers… when should I start learning those? Do I even have to learn them? Everything I’ve built so far was without them and I don’t really understand where exactly those advanced features come in.
You should understand how to use GenServer, as it is your main mechanism for concurrency (technically processes are, however you will never use the low-level process API) in your application, you will encounter it more often once you get out of phoenix framework.
I don’t have a 100% clear understanding about protocols even to this day, even though I have written elixir in production for about 4 years, so you can skip that.
The most important thing, and I always enforced this when I had the possibility, is to learn to write code in a functional manner, don’t write defensive code, write small and concise functions, use the power of pattern matching. A great tool to help you guide on code style is Credo, I always include it in all CI pipelines and it really helps having clarity in codebase.
Another thing to understand is that, while you’ve been using Phoenix, Ecto, etc., you have been using all of these tools – they’ve just been implementation details. One way to better understand some of these things is to dig into the source a bit. Elixir is very readable, and I’ve personally found myself looking at source code as often as documentation to more deeply understand a tool I’m using – moreso than in any other language. In particular, I make heavy use of the </> icon next to basically anything in HexDocs, which will take you to the source for that thing.
Overall, though, I wouldn’t worry too much about it. Keep building and you’ll eventually run into a need for this stuff. If you want to get there faster, next time you’re looking to implement a new thing, especially in a toy project, consider building it entirely yourself instead of pulling in a dependency. (For example, let’s say you’re using an external API and want to handle the rate limit – instead of pulling in a lib to handle that, you could use a GenServer to ensure that only a certain number of requests are being made in a given time span.)
Erlang / Elixir are one of the very fastest compiled dynamic languages out there and that’s a huge benefit on its own, however if you are not using OTP you are missing out. As @zachallaun said you actually are already using it if you use certain libraries. The true value-add of Elixir for me lies in being able to have a huge number of parallel tasks without that requiring a lot of babysitting (as is the case in most other programming languages, sadly).
I don’t particularly agree; sure, it would be hard to take in the entire thing, but that shouldn’t be the goal. If you’re looking to understand a particular feature, much of it is still very accessible. And after you dig into one part, you get some footing and the next bit is easier, etc.
To each their own! There’s a lot of value to be had from digging into even those libraries, but if you find yourself in over your head, there’s no shame in looking for examples elsewhere.
I agree to start learning right now! There is a chance you won’t need them but it’s useful stuff to know. There may be times where a GenServer is actually a really good solution for your problem. I wouldn’t go looking for reasons to use it in production code, but definitely no harm in starting to learn how it all works right now.
I’m a big fan of shallow/wide learning. I recommend learning just enough about them to know, when a specific problem comes up, whether or not they are a good fit for it; then coming back to the tool when you’ve decided you need it and need to learn more.
This is, of course, easier said than done, and when to “call it quits” when studying something is heavily dependent on your personal learning style.
Personally, I feel confident enough in classifying a concept as “sufficiently understood for evaluation later” when I’ve read enough about:
How it is meant to be used
What it is good at
What it is bad at
It’s normally this last point that is difficult to surface. Very few libraries spend a lot of time polishing a description of what they suck at, so I really like to hone in on things like post-mortems and the ‘x’ in “we migrated from x to y” blog posts, to learn from other’s mistakes before making them myself. (In fact, I’m often more ready to adopt a tool that has a lot of stories of switching off of it—it makes me feel more confident in knowing when it is a poor choice, so I get to make better ones!)
I’m confident enough in my abilities and the excellent quality of documentation in this community to trust that if I can recognize the utility and trade-offs of a tool, I can navigate the implementation details when I decide it is time to reach for it.
I think one thing to learn asap is GenServer. In my experience, once that “clicks”, suddenly everything in Elixir space makes so much more sense and seems so much easier
Generally, you need to look into these often when you want to build some piece of infrastructure your application code will then use.
For example, if you want to build an elixir Stream that has an external API as a back-end, you could very well build it around GenServer.
Other examples where you may want to do it would be things like handling data imports/exports pipelines, or more general background jobs - but for these almost certainly you’re better off using higher level building blocks (Flow/Broadway/Oban etc.).
GenServers can be useful if you want to do some work on recurring basis, like, syncing something for accounts via API. At the same time, they’re useful if you want to rate limit / limit concurrency of things that happen within the same context. Sticking to the example of syncing something for accounts, you could end up with a single GenServer, meaning - single runtime process - that would be responsible of periodically starting a job to sync something for given user and then control the process, making sure there’s no more than 1 sync at the same time happening for given user. Again, this could be done this way, but also could be done with database as a locking mechanism to provide the same end result.
GenServers are also useful when you want to provide an abstraction to more complex tasks, for example I just wrote a GenServer that user interacts with, which breaks the job into smaller chunks, executes those (in own processes) and once all jobs are done responds to the initla call of the user without blocking the whole GenServer (the technique here used is delaying :reply to the user call).
In a CRUD app that just reads and writes stuff to database, there’s very rarely a need to write a GenServer.
Interesting, is there a blog post somewhere demonstrating this technique? I want to build something like that myself but could easily use your thing if you plan on showcasing it somewhere (here included).
I’d love to read the same post and don’t mean to discourage @hubertlepicki from writing it, but if I’m understanding correctly, the strategy essentially boils down to:
def handle_call(:event, from, state) do
state = kick_off_some_async_thing(state, from)
{:noreply, state}
end
# some time later...
GenServer.reply(from_that_was_saved_in_state, :the_response)
I might be misremembering but I thought calls must always return replies, no? Only casts don’t require them. I guess I’ve missed an important tidbit somewhere.
Calls do not need to reply immediately! The client, will continue to block until it’s timeout is reached or until the server calls GenServer.reply/2. This is why the from parameter is passed to handle_call – it can be stored in state and replied to later.