54) ElixirConf US 2018 – Closing Keynote – Chris McCord

Not yet, AFAIK, but today you may use Texas or Drab to archive the same effect.

3 Likes

with texas, nothing is a response (unless your websocket disconnects, then you fall back to http) - you have to broadcast a message whenever you want a view to be updated, for a long running process this could be as simple as broadcasting out “progress_bar” once every second or so and all the clients subscribed to that topic would update themselves with a function def progress_bar(conn), do: ... inside that function you would just call on some state somewhere that knows what percentile is complete, so you’d just broadcast that message out over and over until you’re done processing

2 Likes

Finally some progress! I haven’t tested the code as thoroughly as I should, but I’m pretty happy with these preliminary results (code here: https://github.com/tmbb/phoenix_undead_view).

The way it works is very simple:

  • Instead of nesting blocks (like the default implementation of phoenix templates), we generate a proper list (not even a normal iolist).

  • We merge the binaries in the list so that it’s always: [static_binary, dynamic_expression, static_binary, dynamic_expression, ..., dynamic_expression, static_binary]

  • Variables defined in one of the elements of the list aren’t available in the following elements, which is a problem. For example, this raises an error: [x = 1, y = x + 1].

  • To go around this limitation, we assign the elements of the list to named variables and put them all in a block. This way, all variables are injected into the current scope as the user expects from a normal EEx template.

  • Finally, in the last expression of the block, we return the list of variables we care about:

    1. If we want to render the full template, return both the static__ and the dynamic__ variables.
    2. If we want to render the static part of the template, return only the satic__ variables (which we can statically replace by their value, as I show above)
    3. If we want to render the dynamic part of the template, return only the dynamic__ variables

I can finally separate the dynamic from the static parts of the template, and I didn’t even have to do significant AST rewriting! Actually, there is a place where I pattern match on the AST but that’s just a shortcut I’ll replace later. I can now take this template:

<% a = 1 %>
Blah blah blah
<%= a %>
Blah blah
<%= if a > 1 do %>
  <%= a + 1 %>
<% else %>
  <%= a - 1 %>
<% end %>

Compile it into the following (full template):

{:safe,
 (
   static__1 = ""

   dynamic__1 =
     (
       a = 1
       nil
     )

   static__2 = "\nBlah blah blah\n"

   dynamic__2 =
     case(a) do
       {:safe, data} ->
         data

       bin when is_binary(bin) ->
         Plug.HTML.html_escape_to_iodata(bin)

       other ->
         Phoenix.HTML.Safe.to_iodata(other)
     end

   static__3 = "\nBlah blah\n"

   dynamic__3 =
     case(
       if(a > 1) do
         [
           "\n  ",
           case(a + 1) do
             {:safe, data} ->
               data

             bin when is_binary(bin) ->
               Plug.HTML.html_escape_to_iodata(bin)

             other ->
               Phoenix.HTML.Safe.to_iodata(other)
           end,
           "\n"
         ]
       else
         [
           "\n  ",
           case(a - 1) do
             {:safe, data} ->
               data

             bin when is_binary(bin) ->
               Plug.HTML.html_escape_to_iodata(bin)

             other ->
               Phoenix.HTML.Safe.to_iodata(other)
           end,
           "\n"
         ]
       end
     ) do
       {:safe, data} ->
         data

       bin when is_binary(bin) ->
         Plug.HTML.html_escape_to_iodata(bin)

       other ->
         Phoenix.HTML.Safe.to_iodata(other)
     end

   static__4 = ""
   [static__1, dynamic__1, static__2, dynamic__2, static__3, dynamic__3, static__4]
 )}

And separate the static parts…

{:safe, ["", "\nBlah blah blah\n", "\nBlah blah\n", ""]}

From the dynamic parts:

{:safe,
 (
   dynamic__1 =
     (
       a = 1
       nil
     )

   dynamic__2 =
     case(a) do
       {:safe, data} ->
         data

       bin when is_binary(bin) ->
         Plug.HTML.html_escape_to_iodata(bin)

       other ->
         Phoenix.HTML.Safe.to_iodata(other)
     end

   dynamic__3 =
     case(
       if(a > 1) do
         [
           "\n  ",
           case(a + 1) do
             {:safe, data} ->
               data

             bin when is_binary(bin) ->
               Plug.HTML.html_escape_to_iodata(bin)

             other ->
               Phoenix.HTML.Safe.to_iodata(other)
           end,
           "\n"
         ]
       else
         [
           "\n  ",
           case(a - 1) do
             {:safe, data} ->
               data

             bin when is_binary(bin) ->
               Plug.HTML.html_escape_to_iodata(bin)

             other ->
               Phoenix.HTML.Safe.to_iodata(other)
           end,
           "\n"
         ]
       end
     ) do
       {:safe, data} ->
         data

       bin when is_binary(bin) ->
         Plug.HTML.html_escape_to_iodata(bin)

       other ->
         Phoenix.HTML.Safe.to_iodata(other)
     end

   [dynamic__1, dynamic__2, dynamic__3]
 )}

The static template could be optimized further (by removing the empty binaries), but the goal is to send these things over the network and interact with Javascript, so it’s useful to make sure that the static parts start and end with a binary. The static parts will only be transmitted once, which means they optimizing them is not as rewarding as optimizing the dynamic parts.

The main limitation is that I can’t yet expand macros and optimize the macro-expanded result (it might raise some delicate questions regarding scope, but it’s probably not such a big deal. Without the ability to optimize macroexpanded results this code is rather useless for dynamic forms, as we’ve discussed above.

Code here: https://github.com/tmbb/phoenix_undead_view. The code is not yet optimized, and it uses a quadratic algorithm in some places to build quoted expressions, but it’s good enough for a proof of concept, and it does prove that you don’t need hardcore AST rewriting to separate the static from the dynamic parts.

3 Likes

A (slightly) better possibility might be:

<%= f = open_form_for @changeset, Routes.some_path(...) %>
  <label>...</label>
  <%= input_for f, :name %>
  <%= error_tag f, :name %>
<%= close_form %>

which at least doesn’t leak as many implementation details of what form_for does… I still don’t like it that much, though.

2 Likes

NOTE: the form above won’t work for more complex templates because the inner lists must be recursively converted into blocks as well (because of th scoping issue mentioned above).

I’d like to treat top-level expressions differently in the EEx engine itself, which would avoid the recursive step. Unfortuntely, I still don’t understand the EEx callbacks as well as I should. The key is probably in the handle_init() and the handle_begin() callbacks.

None of this helps me with the task of expanding macros, and optimize their expansion, of course. Expanding macros will be trickier.

If I expand the macros “inline” as I compile the template (this might be possible if I carry the expansion env around), then things should work. I’ll probably have to perform some AST rewriting in that case, but I’ll be rewriting an AST that I’ve built myself (not an arbitrary AST), so it seems simple enough.

Yes, that’s where I would start exploring.

Yeah, I am also concerned with the slippery slope this may open. For example, should we convert content_tag to macros and try to expand something such as content_tag :a, dynamic? For things like form_for, the only way a macro would work is by also changing the semantics of the code (i.e. f would leak to outside of the anonymous function). So we are looking at a growing complexity here.

I bet you can compile

<%= form_for changeset, ..., fn f -> %>
  # code that uses f
<% end %>

into

 <form ...>
  # static stuff
  <% very_higyenic_var = FormData.to_form(changeset) %>
  # code is transformed so that f becomes very_hygienic_var
  # the variable leaks but because it's hygienic it will have no effect on the global scope
  # (it's never used anywhere)
</form>

In the case of a form, it would be useful to reuse the csrf token and mark it as static, but that’s really easy to handle. We just need to tag it with something like {:static, expr} and move the expr into the static part of the template. That way it will only be rendered once and can be reused.

EDIT: just because we can it doesn’t mean we should, but it’s an avenue worth pursuing, and even if we fail it’s worth finding out why and documenting it, so that others won’t have to discover it by

EDIT2: for even more safety, the “hygienic variable” could have the name of the function argument (in this case f) with a different context so that if anything depends on the variable name things will still work. Of course it’s never hoing to be 100% referentiay transparent but it’s pretty close.