[HN Gopher] Under the Hood of Macros in Elixir
___________________________________________________________________
Under the Hood of Macros in Elixir
Author : unripe_syntax
Score : 137 points
Date : 2021-10-05 13:09 UTC (9 hours ago)
(HTM) web link (blog.appsignal.com)
(TXT) w3m dump (blog.appsignal.com)
| yurishimo wrote:
| Really cool article!
|
| I've been learning Elixir for a couple of weeks now and
| metaprogramming is one thing that trips me up. Especially just
| browsing the source of Phoenix for the first time, it was/is
| really hard to follow what's happening.
|
| I come from a PHP background, using Laravel, which is regarded as
| a "magical" framework, but I think a stark difference is the lack
| of robust metaprogramming in PHP. Almost everything can be traced
| back to a parent object or a trait. The few magic methods you may
| see floating around are usually implemented in `__get()` and off
| the top of my head, I think they usually are only magic
| getters/setters on models.
|
| Laravel has a concept of macros that you can use to extend
| objects injected from the DI container, but each must be
| explicitly defined, so grepping for them is easy.
|
| In Phoenix, I could end up with a generated function at compile
| time that is nonexistent in my git repo. Not being able to click
| through to the definitions of these has tripped me up. A function
| called `{my-domain}_path()` feels like it should have an explicit
| definition.
|
| Still enjoying Elixir and hopefully I can work with it
| professionally at some point! I just wish it was easer to trace
| back where some of these things functions come from when you're
| using them far away from the definition.
| sidkshatriya wrote:
| You are correct about a lot of meta programming in Phoenix. As
| there is no inheritance in Elixir, macros are used a lot to
| inject code from another module. So as an example if Phoenix
| has an Endpoint module and your module is an endpoint the
| macros (especially "use") injects endpoint related code. It's
| impressive but ultimately all this magic gets too much. It gets
| very confusing when all these macros run because you have
| complications like bind_quoted, accumulating in module @ tags
| etc. And because there are no true static types you need to be
| very careful. Phoenix is slick and I think it's doing its best
| given the underlying platform.
| weatherlight wrote:
| Macros are nice when you are designing a DSL for a framework or
| a library. otherwise, they should probably be avoided.
| dqv wrote:
| I don't really like the dogma of "don't use x". It's much
| better to just show examples of when macros are useful and
| when they're not. I got burnt trying to do tenancy. At one
| point I was like "I'll use the process dictionary", but had
| it hammered into my brain from everywhere "don't use the
| process dictionary!" It turns out the process dictionary was
| exactly correct for my use case. This is just an example of
| one of those dogmas that needs to be more like "x is a bad
| use case if you're trying to do y, but not if you're trying
| to do z".
|
| Here's an example of when macros are very useful:
| months = Enum.map(~w(january february march april may june
| july august september october november december)a, fn month
| -> {month, to_string(month)} end)
| def to_month(arg) for {atom, _string} <- months
| do def to_month(unquote(atom)) do {:ok,
| unquote(atom)} end end for
| {atom, string} <- months do def
| to_month(unquote(string)) do {:ok, unquote(atom)}
| end end def to_month(_), do: :error
|
| I can already hear someone saying "well you can do the same
| with functions" but it starts to get ugly and you're now
| doing these conversions at run time. A function-oriented
| approach would need guard clauses and run-time checks of an
| element being in a list of atoms OR strings then possibly
| run-time conversion of the strings to atoms. Or you can just
| use macros.
| dnautics wrote:
| Yeah. It's super tempting to use macros for inheritance (and
| there are some anointed frameworks that even encourage that
| -- I'm looking at you ExUnit), but I think that is the wrong
| way to think of macros.
|
| The codebase I work on at work is littered with this
| antipattern, but I grin and bear it because elixir is such a
| joy to work with otherwise.
| waynesonfire wrote:
| everything is a DSL / framework / library. I'm starting to
| dislike this idea. The claim is a general programmer should
| avoid macros, but, some blessed developers are granted this
| privilege and thus have the tools to create useful -DSL /
| framework / library- code because they used this advanced
| capability. And useful code tends to become popular.
|
| It's a difficult pill to swallow when a an engineer comes in
| guns blazing trying to enhance your baby project with macros
| and thereby introducing a ton of new complexity. This is the
| issue.
|
| Macros are like a developer-ux enhancement. You better have
| your core library figured out pretty well before investing in
| this enhancement and most projects never reach that level of
| maturity. Especially, internal ones.
|
| It's not that macros should be avoided, they just need to be
| introduced at the right time.
| klibertp wrote:
| Macros in Elixir are a code-generation facility that
| happens to be Turing-complete, nothing more. You should use
| it whenever code generation makes sense. Code generation
| comes with a set of well-known problems, for example the
| error messages and stacktraces need special attention. If
| you feel that the advantages of code generation outweight
| the disadvantages in your specific situation, you should
| use it, no matter if you're designing a framework or
| writing a one-off script.
|
| One of the advantages of code generation is hiding the
| details of implementation, so that they can be _changed_
| more easily (because the description of the whole is
| shorter, and kept in one place). While you get the same
| benefits in Object Oriented languages via other means
| (interfaces and abstract classes, Template Class pattern),
| in Elixir you don 't have much choice: you either implement
| a callback module (ie. behaviour; but then there are cross-
| module calls at runtime) or generate the code via macros
| (but then you lose the 1-to-1 correspondence between what's
| in the file and what's actually defined in the module). Of
| course, both can be (and frequently are, like in GenServer,
| Application, Supervisor, etc.) used at the same time.
|
| Not using macros until some point in time in the
| development (after it "reaches that level of maturity")
| risks getting that point in time wrong. It almost always
| _will_ be wrong, actually, and more often than not you 'll
| find that it's too late: introducing macros after first
| spending time on optimizing the functional interface makes
| you just do the same work for the second time, at which
| point the improvement in "developer ux" may not be worth it
| or even noticeable. Erlang doesn't have (well, it does, but
| not as easily accessed) macros, and the libraries still get
| written in it - frameworks too - and there's little point
| in adding macro-based wrappers (beyond some simple default
| implementation injections) to them, because their
| functional interfaces are already optimized from the dev UX
| angle. So, to make the most out of macros, you probably
| shouldn't wait until "after you're done" with something,
| but you should introduce them as soon as you see a good use
| for them, while considering all the potential downsides and
| pitfals that come with their use.
|
| Also of note, while you can compare macros to eg. Python
| (or Ruby, or Smalltalk) Metaclasses, there is a difference:
| Python offers _a lot_ of mechanisms that you should prefer
| using over metaclasses if they would suffice. On the other
| hand, there 's not really much you can use in Elixir, as
| it's a lot smaller and simpler language. So, while I don't
| think many Python programmers should be writing metaclasses
| (though all should know what they are!), I think almost all
| Elixir programmers should be able to use macros. The macro
| system in Elixir, BTW, is actually on a simpler side of
| Turing-complete macro systems, both defmacro from Lisp,
| syntax-case from Scheme, and imperative macros in Nim (and
| many others) are all much more complex and harder to use.
| So I think the Elixir designers _wanted_ people to use
| macros and made an effort to make it safer and simpler.
| That 's just my impression though, don't quote me on this
| :)
| dnautics wrote:
| > A function called `{my-domain}_path()`
|
| 100% agree. I think it's generally good practice for macros
| which generate non-hidden functions to surface them as
| parameters for the user to include in their code so you can
| easily chase where it's coming from (e.g. EEx.function_from*).
|
| On the other hand, at least it's easy to find the source code
| for any given macro.
|
| If you want a gentler, less magickey introduction to elixir for
| web, I recommend starting with Plug. The good thing is that
| Phoenix builds on top of plug so everything you will learn is
| still applicable to Phoenix. The big danger is that you might
| be dragged kicking and screaming into Phoenix, as I was (but
| trust that most of the macros is phoenix are there for a reason
| and they do save time in most use cases at the expense of
| having to memorize a whole lot of shortcuts)
| klibertp wrote:
| Plug is actually very hard to learn, because its design is
| very simple, but knowing how to use this design to produce
| the effect you need requires a whole lot of mental
| gymnastics. A less generic design would account for, for
| example, "initialize this state on first time this plug is
| called" (I think something changed here, but don't remember
| if for the better) and similar things, but then it would be
| less generic and possibly not flexible enough to support all
| the things Phoenix and co. need from it.
|
| That being said, Plug is still better than Phoenix, so if
| you're not in rush to build something and can experiment,
| then learning Plug first will give you better understanding
| of Phoenix later on.
| nelsonic wrote:
| If anyone else is stuck learning Plug, the Elixir School
| tutorial is a good starting point:
| https://github.com/dwyl/elixir-plug-tutorial
| nesarkvechnep wrote:
| It's too early for you to try metaprogramming. It has very
| simple rules but proficiency with the language is advised when
| diving into macros. You might want to read Chris McCord's
| "Metaprogramming Elixir". The author is also one of Phoenix's
| authors.
| dugmartin wrote:
| I agree. I've been using Elixir for almost five years and I
| wrote my first macro this morning. I needed _slightly_
| customized code in each instance of set of GenServers and it
| turned out a macro was the cleanest way to do it.
|
| That said, understanding macros will help you sprinkle in
| your own "magic" in Phoenix though. I normally import a
| custom view helpers module inside the lib/<project>_web.ex
| file in this macro like this: defp
| view_helpers do quote do ...
| import <project>Web.ViewHelpers end end
|
| Then you can add methods in <project>Web.ViewHelpers and they
| are automagically imported into all your templates, eg.
| > lib/<project>_web/view_helpers.ex: defmodule
| <project>Web.ViewHelpers do use Phoenix.HTML #
| this allows you to use link(), ~E, etc def
| tagline, do: "Running light without overbyte" end
| > lib/<project>_web/templates/foo.html.heex ...
| <div class="tagline">{tagline()}</div>
| dqv wrote:
| And actually the <project>Web macros are a great way to
| show people when macros are useful. A fun exercise for
| beginners might be to have them manually import those
| modules. And it illuminates some aspects of Phoenix in
| general e.g. "oh I'm importing Phoenix.Controller in all my
| views." In other words, the fact that you're rendering a
| view from a controller is not the reason you have
| controller functions available in your views, it's because
| your importing Phoenix.Controller when you "use ProjectWeb,
| :view"
| dugmartin wrote:
| Yeah, the "magic" dispatcher code in <project>_web.ex is
| pretty powerful but I'm not sure it would be easy for
| beginners to grok it: @doc """
| When used, dispatch to the appropriate
| controller/view/etc. """ defmacro
| __using__(which) when is_atom(which) do
| apply(__MODULE__, which, []) end
|
| That said I much prefer how Phoenix puts this code
| explicitly in each project and doesn't hide it in a gem
| like Rails.
| eric4smith wrote:
| I still don't get it. Then again I'm not too much of a bright
| programmer.
|
| However Elixir has been paying the bills for 5 years now, so...
| meh.
| atonse wrote:
| I literally run a company that builds products on elixir and my
| elixir code has touched millions during the pandemic, and I'm
| about 80% sure I'd fail an interview for an elixir developer
| job if it asked anything about macros, protocols, etc.
|
| Because I simply don't understand them (and haven't had the
| time to play around with them at a "Learn this language" level,
| as opposed to "build this feature we needed yesterday.") But
| I've also preferred not to do magical things and instead be
| explicit. But then again, I'm not building something like Ecto
| or Phoenix.
| davidw wrote:
| I've used Erlang for years, and this is more the mentality
| with that language. Just do the explicit thing even if it's a
| bit more code.
___________________________________________________________________
(page generated 2021-10-05 23:01 UTC)