Help with if-like macro

Hi - yet another macro related yelp for help :slight_smile:

I’ve been trying everything I could think of but I just can’t get it right, help much appreciated.

The objective is to be able to write something in the line of:

@if bool_expr
  expr_1
  expr_2
  ...
  expr_n
end

which, if bool_expr is true, will return

[
  expr_1
  expr_2
  ...
  expr_n
]

You’ll have to start with:

@if bool_expr begin
  expr_1
  expr_2
  ...
  expr_n
end

Then, the statement " if bool_expr is true" seems a bit odd as it suggests to me that you’re trying to evaluate that expression at macro-expansion time. But at that time it will probably not be defined.

1 Like

Thanks - I’m happy to evaluate that anywhere, so the macro can return a whole if statement. But yes, in one of my attempts I was getting the error that I was trying to evaluate a non-bool (an expr).

What I’m trying to do is to overcome the default behaviour where the if statement returns the last executed expression - and instead wrap all the expressions inside the if block into an array and return the array.


I didn’t put the begin block as I’m open to any approach which yields the result. For ex passing a function with a do block is also fine.

Ok, that sounds better.

Aright, do a dump of your input expression and one of your desired output expression:

julia> dump(:(@retall if boolex
       a = 6
       b = 7
       c = a+b
       end))
Expr
  head: Symbol macrocall
  args: Array{Any}((3,))
    1: Symbol @all
    2: LineNumberNode
      line: Int64 1
      file: Symbol REPL[6]
    3: Expr
...

and

julia> dump(quote 
              a = 6
              b = 7
              c = a+b
              [a,b,c]
              end)
Expr
  head: Symbol block
  args: Array{Any}((8,))
    1: LineNumberNode
      line: Int64 2
      file: Symbol REPL[8]
    2: Expr
      head: Symbol =
      args: Array{Any}((2,))
...

Then figure out how to transform one nested datatype into the other. (Remember, meta-programming is nothing but nested data-structure gymnastics).

Also, you can enlist the help of MacroTools.jl. Although, there is probably nothing wrong with getting your hands dirty (i.e. not use MacroTools) until you’re a bit more experienced.

Thanks, I’ll give it a try.

What are you trying to do? What’s wrong with

if bool_expr
[
  expr_1
  expr_2
  ...
  expr_n
]
end

?

It’s for Genie’s templating engine which embeds Julia code into HTML. The template code looks like this:

<% if bool_expr %>
  <h1>Hello!</h1>
  <h2>Welcome to foo</h2>
  <p>Foo is the best</p>
<% end %>

This is converted by the templating engine into:

if bool_expr
  h1("Hello!")
  h2("Welcome to foo")
  p("Foo is the best")
end

This has the effect that only the last expression, p("Foo is the best") aka <p>Foo is the best</p> gets rendered.

Yes, now the only solution is to write:

<% if bool_expr 
[ %>
  <h1>Hello!</h1>
  <h2>Welcome to foo</h2>
  <p>Foo is the best</p>
<% ]end %>

However, this catches people off guard and I need to explain why this happens, especially as this is the only place where this brackets syntax is needed. It would be simpler to put in the docs that they need to use @if similar to how they need to use @foreach.

Something similar to what you want is

macro test(expr)
    expr.args[2] =  expr.args[2].args
    return expr
end

@test if 1==1
    "hi"
    3
end
#returns [:(#= none:2 =#),  "hi", :(#= none:3 =#), 3]

There is probably a more elegant way, but you can filter out the nones if you need

macro test(expr)
   expr.args[2] = :(filter(!isnothing, eval.($(expr.args[2].args))))
   return expr
end

@test if 1==1
    "hi"
    3
end
#returns ["hi", 3]

unfortunately I dont think you can use @if. I get a LoadError: syntax: invalid name "if"

Well, this should be especially unnecessary if you aren’t even writing the julia code. You should just make your template engine generate

if bool_expr
  [h1("Hello!")
  h2("Welcome to foo")
  p("Foo is the best")]
end

Instead.

1 Like

Thank you, that looks pretty promising, I’ll give it a try!

Yes, @if is off limits, I learned that a while ago but I didn’t want to complicate the discussion :slight_smile: I’d be happy with something like @ifthen.

That’s a great point. I would’ve preferred to rely on the compiler instead of me searching, matching, and massaging strings, which is more error prone. But it does have the advantage that it doesn’t introduce new DSL constructs. Thanks!


Edit 1
To be honest, my starting assumption was that it will be super easy to capture the body of the @if block, slap some square brackets around it, and return it. It wasn’t…

It isn’t that hard - its what my example does above. The body of the if statement is just the second argument of the if Expr (the first argument is the condition and the head is the “if”). My example just swaps out the body block with an array of its own arguments (which are the specific lines in the body block) so instead of just executing the body, it returns a list of what it was going to execute

If you try getting rid of the if part, things go off track, ie:

@if 1==1
  "hi"
  3
end

which is what I would like the users to work with.

Still, your proposed solution would be great combined with @yuyichao’s suggestion of simply modifying the template code before parsing, ie I could replace if cond with @test if cond which is less error prone than inserting the brackets into the template code.

1 Like

Yeee, combining the two solutions works perfectly!

Template:

<% if true %>
    <script src="/js/lite_cms/jquery.min.js"></script>
    <script src="/js/lite_cms/lite_cms.js"></script>
<% end %>

Parsed template:

Flax.@iif if true 
    Html.script(src="/js/lite_cms/jquery.min.js" )
    Html.script(src="/js/lite_cms/lite_cms.js" )
end

Rendered HTML:

<script src="/js/lite_cms/jquery.min.js"></script>
<script src="/js/lite_cms/lite_cms.js"></script>

Beautiful! Thank you @dstarerstor and @yuyichao!

What I meant is that you generated all the code anyway, so you won’t even need to parse anything…

You don’t need to go from

if ...
...
end

To

if ...
[...]
end

You knew where the begin and the end of the if block and even what expressions are there in it so you can go straight for the latter form.

Don’t use eval that’s wrong.

Also, the first version of the macro is just retuning the expressions as an array rather than evaluating anything.

If you really want to write the macro, which is really unnecessary when you are generating everything already, the macro should looks like

macro test(expr)
    expr.args[2] = esc(:([$([arg for arg in expr.args[2].args if !isa(arg, LineNumberNode)]...),]))
end
1 Like

Thank you, I will update the macro.

If I understand correctly, your point is:

  1. the workflow is: a) template (HTML with embeded Julia “snippets”) => b) parsed template (pure Julia code) => c) rendered template (the pure HTML output of executing the pure Julia code at #b)
  2. since the template engine generates the Julia code at #b), it can generate the if block wrapped in square brackets - is that right?

If I understand correctly per above, I would strongly prefer to avoid that, as parsing the block of code and isolating the body of the if expression is error prone. Now I use a pretty robust mechanism where I parse the DOM of the template (#a) and only work at element level. If I start parsing multiple elements as multi line strings I run into issues like nested blocks of Julia code within the if code and I have to keep track of the correct closing end - and this gets messy fast.


Macro updated, works like a charm, thank you!

You don’t need to parse multiple multiple ones at the same time?
I’m only saying that where you were emitting

if condition

you emit,

if condition
[

and when you emit the closing end for if, you emit ] end.

Maybe this shouldn’t be mentioned in polite company, but you actually can have a macro named @if if you’re sinister enough:

eval(Expr(:macro, Expr(:call, :if, :cond, :body), 
          :(nothing; 
            esc(Expr(:if, cond, 
                     Expr(:vect, map(x -> Expr(:block, x), 
                                     filter(x -> !(x isa LineNumberNode), body.args))...))))))
  

h1(x) = "<h1> $x " * raw"</h1>"
h2(x) = "<h2> $x " * raw"</h2>"
p(x)  = "<p> $x "  * raw"</p>"

Now at the repl:

julia> @if true begin
           h1("Hello!")
           h2("Welcome to foo")
           p("Foo is the best")
       end
3-element Array{String,1}:
 "<h1>Hello!</h1>"        
 "<h2>Welcome to foo</h2>"
 "<p>Foo is the best</p>" 

Of course, having the begin might offend your sensibilities, but I kinda like it.


Edit: As @yuyichao pointed out, the macro can be written in more julian form as

@eval macro $(Symbol("if"))(cond, body)
    out_body = [Expr(:block, arg) for arg in body.args if !(arg isa LineNumberNode)]
    quote
        if $cond 
            [$(out_body...)]
        end
    end |> esc
end

Yes you can. I’d do

@eval macro $(Symbol("if"))(...)
...
end

Instead. (Again, as I said in another thread, you almost never need to completely throw out expression literal and interpolation just because you can’t construct one particular level)

1 Like