A tiny macro to clean LiveView code

Sup ppl, I just wanted to share a little macro I created to simplify LiveView code. It allows you to convert

socket.assings.variable

into

~a|variable|

For instance here’s a function from Chris McCord’s live_trek

  def handle_event("delete", %{"id" => id}, socket) do
    todo = Todos.get_todo!(socket.assigns.scope, id)
    {:ok, _} = Todos.delete_todo(socket.assigns.scope, todo)

    {:noreply, socket}
  end

Would be

  def handle_event("delete", %{"id" => id}, socket) do
    todo = Todos.get_todo!(~a|scope|, id)
    {:ok, _} = Todos.delete_todo(~a|scope|, todo)

    {:noreply, socket}
  end

Necessary? Not really.

But I thought it looks much better, specially when you have lot’s of socket.assigns and want to reduce verbosit, avoid unpacking assigns and reduce formatter line breaks.

Here’s v0.1

  defmacro sigil_a(expr, _modifiers) do
    socket = Macro.var(:socket, nil)

    quote do
      Map.get(unquote(socket).assigns, String.to_existing_atom(unquote(expr)))
    end
  end

And here’s a vim command to search and replace:

:%s/socket\.assigns\.\([a-zA-Z0-9_]*\)/\~a|\1|
8 Likes

Nice idea. Curious, why Map.get rather than fetch? I think the behavior being replaced raises if the expected key is missing, whereas this will return nil I think

3 Likes

No reason, probably a bug :stuck_out_tongue:

  1. You can also move String.to_existing_atom to just String.to_atom outside of the quoted block, this saves you a function call at runtime.
  2. might want to try var!(socket) inside the quote instead of Macro.var outside the quote. I think they are equivalent but var!(…) Is slightly more legible and idiomatic IMO
2 Likes

If you wanted to be really evil, you could overload the @ operator if the caller environment contains socket :smiling_imp:

defmodule Evil do
  defmacro __using__(_) do
    quote do
      import Kernel, except: [@: 1]
      import Evil
    end
  end

  import Kernel, except: [@: 1]

  defmacro @name do
    var = Macro.var(:socket, nil)

    if assigns_call?(__CALLER__, name) do
      {name, _, _} = name
      quote(do: Map.fetch!(unquote(var).assigns, unquote(name)))
    else
      quote(do: Kernel.@(unquote(name)))
    end
  end

  defp assigns_call?(env, {name, _, nil}) when is_atom(name) do
    Macro.Env.has_var?(env, {:socket, nil})
  end

  defp assigns_call?(_, _), do: false
end

defmodule Hmm do
  use Evil

  @okay :foo

  def maybe(socket) do
    @okay
  end

  def wow do
    @okay
  end
end

dbg(Hmm.maybe(%{assigns: %{okay: :NEAT!}}))
dbg(Hmm.wow())

(don’t do this)

7 Likes

This would have a lot of potential if there were good overridable unary operators in Elixir.

Unfortunately, +, -, !, ^, not, @, & are too useful and meaningful to be overriden. There is ~~~ but its ugly and too hard to write :stuck_out_tongue: .

Makes me think Elixir maybe could support an unary $ operator.

1 Like

I mean, it’s what happens in EEx templates, though!

$ is being used for the potential new type system

Pretty cool!

Innnnnteresting. I was trying to figure out how to do something like this and couldn’t figure it out. I was trying to use var!/2 for this to no avail. I never understood that you can check the caller env.

I was looking to create a single guard that would work on both socket and assigns. I have a lot of single module CRUD LiveViews so I wanted something like:

def handle_event("update", %{"user" => user_params}, socket) when in_action([:edit]) do
  #...
end

def render(assigns) when in_action([:edit]) do
  ~H"""
  """
end

This would check whichever existed of socket.assigns.live_action or assigns.live_action was in [:edit].

Even though I don’t think that is particularly evil (that is, what I wanted to do—what you’re suggesting is absolutely pure evil :see_no_evil: :sweat_smile:) , I ultimately decided I’d rather not do anything out the ordinary like this so I never bothered to seek help figuring it out, but it’s nice to know how it could be done, so thanks!

(that is, if that would even solve the problem… I haven’t actually tried, just responded immediately, lol, but still informative for me)

3 Likes

Glad it was informative!

It’s definitely worth digging into the environment info you have available in macros. Even in the OP’s example, you could check the caller env for a socket var and raise an informative error if it doesn’t exist (as opposed to the underlying Map.fetch! error or whatever you end up using).

1 Like

That;s pretty cool :wink: I’ll have to play with it a little bit to completely grasp it, thanks!

If any1 is actually using it, here’s an update. Using @tfwright & @ityonemo suggestions, I’ve got:

  defmacro sigil_a(expr, _modifiers) do
    {_, _, [key]} = expr
    key = String.to_atom(key)

    quote do
      Map.fetch!(var!(socket).assigns, unquote(key))
    end
  end

1 Like

This operator might be used for the type system

Like be the paper “https://www.irif.fr/_media/users/gduboc/elixir-types.pdf

1 Like

yeah, so? :slight_smile: