I personally don't have any problem with heavy macro-use in libraries, I feel like that is where they shine. So long as the documentation is good, I don't really care how the library itself is implemented. I would much rather have the cleaner API in many cases. I know it can be taken too far, though. Some funky stuff can happen in Phoenix's router, though a lot of it is actually to reduce cyclic compile-time dependencies as the router is a place where these necessarily happen.
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.
I never use `resources` though have no beef with `pipeline` or `scope`, though I haven't thought too much about it. It is a little weird getting used to which modules are getting auto-qualified and which aren't, but I'm long passed that. And that is actually what is "fixing" the cyclical dependency issue.
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.
You can do that if you want. Just remove `def router` from `MyAppWeb` move its contents over to a custom module:
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.
Oh! Well I don't know your setup but you could always add a top level `PhoenixHelpers` namespace (or a better name) that holds these generic types of things that can be shared between apps. Though again I really don't know your set up. Like maybe you have an app whose router doesn't need `Phoenix.Controller` so then this wouldn't make sense.
> I wish `pipeline`, `scope` and `resources` in Phoenix router and also all the `use MyApp.Web, :x`, would just die in a fire.
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?
> This is still a different semantic if you do the deadview equivalent, which will try to do a path resolution in /templates.
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...
> So long as the documentation is good, I don't really care how the library itself is implemented.
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.
Your intuition as a nom-elixir user is absolutely correct. However it's worth saying that as a working dev, there's almost never a problem in the core++ libraries.
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!!
Those are fair points. In terms of PRing, though, I do think that people are a little too "afraid" of macros. It sometimes feels like people won't learn them out of of principle which is a shame as they aren't that complicated. Though anyone trying to navigate nested macros definitely has my sympathy.
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.
EEx does macros well. Note that EEx.function_from_x makes you declare the name of the function it instantiates, so it's searchable.
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.
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.
Last I checked you need to do init/1 by hand. Honestly being more transparent about creating child_spec like `GenServer.create_default_child_spec` would be great. The rest of use GenServer is pretty good, IMO (this is why I wrote 'kind of a bad' macro). I normally hate default implementation of functions but defaulting with an error saying "you forgot this" is reasonable.
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
> 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.
The only thing I can think of is when developers overuse genserver not realizing it's involving another process and then serializing your requests (behind a queue even) into its mailbox. That may be desirable for some but in-process handling can sometimes be the better choice.
I've never written a genserver in any professional project I've worked on.... Well, except maybe for a trivial 5 line wrapper enables a global ets.
> 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.
> I've never written a genserver in any professional project I've worked on
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.
I use other people's GenServers. Never agents. Tasks for concurrency (but almost never those either, just Oban). Ets for stateful datastore.
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)
Ah I see. Yeah, Phoenix definitely gives you a lot out of the box. I think for webdev it's probably enough. This is a pretty straightforward Phoenix app for example, only uses a couple GenServers: https://github.com/mbta/arrow
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.
I see after I posted you edited to mention Agents, Tasks, Oban, and gen_statem.
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 don't use gen_statem, its clunky as hell, does a weird thing with response aliasing, and it's even harder to read than Genserver. Plus I don't need it.
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 actively trying not to use GenServers in your application code or is it just that you have a set of 3rd party dependencies that remove the need?
There's no reason not to write GenServers if you need them. Just don't use them as a class replacement :) But if you need to model some simple runtime state or concurrency, that's what they're there for.
> 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.
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.