Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> In real life, we don’t need to write code with receive do loops. Instead, we use one of the behaviours created by people much smarter than us.

I make more than half of my income from Elixir. That said, the naive receive loop is much easier to understand than any of the GenServer examples. They pollute the module logic with all that handle_* boilerplate. I believe that neither Erlang nor Elixir got the right abstraction there. Method definition in any OO language is easier to understand. At least Elixir should have taken Agent and made it an invisible part of the language. All those calls to Agent in this example https://elixir-lang.org/getting-started/mix-otp/agent.html are still boilerplate.



What you said is rarely stated in public, but I've often felt it too: OTP obscures the underlying beauty of the Erlang platform.

OTP is well engineered of course, but the basic notion of the spawn -> receive -> loop cycle is so clean and illuminating that I wish newbies would hold out before learning OTP sometimes.

It's natural to think of case-specific abstractions around the primitives that are more germane to the domain at hand than OTP's.


OTOH every decent erlang book or tutorial I've ready, from Armstrong's book[1] to Learn You Some Erlang[2] to Erlang and OTP in Action[3] and others all start off by introducing primitives like spawn, message sending , and receive.

They then introduce OTP and explain all the cases it handles.

I've never read a book or tutorial on OTP that just starts with OTP. In fact (a bit tangetially) whenever I encounter abstractions where I don't already understand the primitives I have a very difficult time. Maybe that's just the way my brain is wired though. I have a terrible time with OO programming for that reason. I much prefer a separation of functions and data because it's much easier for me to reason about what is happening.

Anyway, I agree that programmers should start with the primitives, but I've never seen anyone really teach OTP any other way.

Edit: also there are tools such as Erlang.mk[4] by Loïc Hoguin[5] that will handle a lot of the boilerplate for OTP projects and building releases, though it's good to do it manually at least once while learning.

[1] https://pragprog.com/titles/jaerlang2/programming-erlang-2nd...

[2] https://learnyousomeerlang.com/

[3] https://www.manning.com/books/erlang-and-otp-in-action

[4] https://erlang.mk/guide/index.html

[5] https://ninenines.eu/


> every decent erlang book or tutorial I've ready, from Armstrong's book[1] to Learn You Some Erlang...

What a coincidence! I just tweeted about how you can get it and 12 other various programming books for just $8 now: https://twitter.com/AlchemistCamp/status/1311796404830892032


> spawn -> receive -> loop cycle is so clean and illuminating that I wish newbies would hold out before learning OTP sometimes

Yes it is neat and its fun to play with but it isn't usually something you want to use in production code. In production code it is important to have processes linked correctly so that errors propagate to callers. GenServer.call handles this for you, and also clarifies intent (blocking call to another process that must respond or fail).


I agree. The reason we stick with OTP, is that OTP behaviors like gen_server handle two system-level concerns that "raw" Erlang code doesn't:

1. OTP behaviors integrate with the OTP supervisor lifecycle management system (i.e. the OTP framework offers the supervisors standardized hooks to start up and shut down your process, guaranteeing that the errors generated during such steps will be in a format the supervisor can use);

2. Processes that implement OTP behaviors will react to `sys` messages; and so can be debugged, hibernated, code-upgraded, etc. on a framework level, "between" the times the process runs developer-defined code, without the developer having to write such handlers into their module.

This is all accomplished by passing control over the receive loop to a framework — `proc_lib` in this case — which in turn passes any messages it doesn't recognize back to your process. It's an inversion-of-control: proc_lib "is" your process; gen_server or whatever is a delegate of proc_lib; and your module is the delegate for gen_server. It's like an OOP class hierarchy in a GUI system, where the base class handles some events, subclasses handle others, and then your module only has to handle the few it's interested in.

But, if there was a way to do exactly that — to specialize one receive statement, defined in some other function, with your own additional clauses — then we wouldn't need the inversion-of-control framework of OTP! We could just write a regular receive statement that "points to" another receive statement as its "parent" or "fallback" (sort of like a chain of firewall rules, each pointing to the next as "what to check next if this one didn't handle it.") Ideally, the compiled result would be one fused receive statement that has all the clauses from all its parents.

And if you think about it, that's totally possible, even if Erlang itself doesn't offer a fancy chainable receive statement like that... because, in a language like Elixir tht has hygenic macros, you can use macros to generate such "fused receive statements"!

I'm honestly really surprised nobody has yet tried. I realized the possibility of this a couple years back, and have been waiting with baited breath for somebody to attempt it. If it worked out, this "tech" could be used to build a wholly-different-feeling language.


Well, the sys behavior is easy to implement.

I also am not a fan of the complexity and overhead of gen_server so instead I use metal (http://github.com/lpgauth/metal). It's a simple receive loop with an optional init and terminate callback. It implement sys and can be supervised.


Does gen_server have a measurable overhead compared to this? The main event loop of gen_server looks remarkably similar to what you are doing but handles calls, casts, hibernation, debugging, provides stacktraces and doesn't "force" exit trapping. In the "happy-path", I don't see any additional overhead to what you are doing.

I would also like to see "handle_call" and "handle_cast" (maybe even "handle_info" with a blanket noreply-implementation) optional, but until then I can deal with the additional two lines to exit on cast or call if I don't require those.


I don't think this approach would result in lower overhead, since `metal` here is still a separate module.

Maybe if it were instead a macro/parse-transform, injecting all the common "framework" code into the module implementing the server callbacks, you could then at least get the optimizations that result from all the calls into and out of the "framework" being local rather than remote calls. Private functions could be inlined/fused, etc.

Further, if HiPE was made to work again on Erlang 25, such a "statically-compiled" server module could also "stay native", with no context-switches to interpreted code, in a way that HiPE can't presently manage for gen_servers due to the code in `gen_server` and `proc_lib` themselves not executing natively.


Yeah but that's like saying the awkwardness of stl templates obscures the beauty of C++ or the Android app api obscures the beauty of the jvm. The genserver is messy because it deals with a good chunk of messiness around safely managing distributed systems for you. It probably could be improved (Dave thomas critiques comes to mind) but elixir had to be conservative because if it weren't it wouldn't have had buy in from the beam community and wouldn't have become the first truly successful non-erlang language targeting the BEAM.


I finished reading Elixir in Action a few days ago. This was probably the best part of that book. It takes you through building a primitive GenServer with basic processes before moving onto actually using GenServer. It specifically goes over tail recursion optimization before it introduces the receive do loop, which seems like a very important part of the whole thing.


Elixir in Action is fantastic. Its approach is so different from the other introductory books, but the payoff is real.


Would you recommend the book?


Also not the OP, but unequivocally yes (though for full disclosure, I am biased as was one of the technical reviewers for the second edition). It was hugely important to me personally learning the language; the approach is excellent and it's very well written. You take a simple to-do application and write a number of versions of it, each one progressively more complex and featureful, and each one using progressively higher language/OTP abstractions.


Not the OP, but it is a great book, I'd highly recommend it.


> the basic notion of the spawn -> receive -> loop cycle is so clean and illuminating that I wish newbies would hold out before learning OTP sometimes

The frustrating thing is that when I made exactly such a tutorial two years ago, I was immediately chastised (on my old hosted commenting system) for showing something so low-level to start with:

https://alchemist.camp/episodes/simple-process-example


> the basic notion of the spawn -> receive -> loop cycle

I'm working on a video series about these concurrency patterns; even recorded the first one but the audio is bad so I'm going to re-record it before I publish it when some "real" audio equipment comes in (hopefully this weekend) - let me know what you think:

https://www.youtube.com/watch?v=eoEcpcLVumc


Pretty good!

I knew most of that stuff already so I can't speak to the pure education value, but it made sense, and I think you hit the key points.

I bet some visuals would help it make sense for total nooberz.

AND! I didn't know that about "v" - thanks! :)


another protip: you can also call v(n) and it will give you the item at iex(n)>


There are libraries that do this, like https://github.com/sasa1977/exactor but the Elixir community nowadays seems more conservative about macro "magic" than it was a few years ago. Sasa has written an entire book about OTP and if he doesn't recommend using his own library I wouldn't argue with him, but I haven't written a Genserver myself in years despite writing Elixir for a living the past 3.5 years. If you are going to learn how they work though, it makes sense to stick to how Erlang actually implements them.


> There are libraries that do this, like https://github.com/sasa1977/exactor

At the very top of the Extractor readme:

> I don't maintain this project anymore. In hindsight, I don't think it was a good idea in the first place. I haven't been using ExActor myself for years, and I recommend sticking with regular GenServer instead :-)

This both confirms what you said about the Elixir community shying away from macro magic, and makes this library a very bad idea to use in new projects, imo.


Right, thats why I said he doesn't recommend using his own library. My point is people have been down this road, and moved away from it, not that anyone should use it.


Of course it's easier to understand. It's also much easier to get wrong. Just yesterday I messed around with running a small process on to ship subscribed messages across node boundaries and forgot the recursive call in one of 4 cases, leading to mysterious stopping.

In Elixir you don't even have to define all of this boilerplate, just the functions you actually need. Also, the callbacks are not classical methods, a handle_call doesn't have to reply immediately, for example. Distinguishing info, call and cast is also very relevant.

If you wanted something like

    {:ok, agent} = Agent.start_link ...

    agent.get()
I think you would need a change in the BEAM to recognise that agent is a "particular kind of PID".


That's what I was thinking about, client side. Probably the Elixir compiler could detect it and generate the code to bridge the gap. It feels OO-ish but we're dealing with mutable state anyway. I prefer "easy" than "pure".


Seems like you're losing something here -- ISTM that the goodness of a reified massage is now lost. A message, as a piece of data, can be tested, stored, passed on, etc., while a "call" (the .get()) cannot.


Do you do that with proc_lib or is it just a completely otp-unaware process like Joe would do?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: