Function that creates generated function using interpolation?

metaprogramming

#1

Hi, so I’d like to to create a function that allows me to automate the creation of generated functions.

Here is an example of the type of generated function I’d like to create:

using Reduce
expr = :(x^2)
@generated function fun(x,::Val{p}) where p
    :(@fastmath $(horner(df(expr,:x,p))))
end

However, I’d like to create a function that creates this function for me, so I want to be able to do

genfun(:fun,expr)

where :fun symbol will be the name of the generated function and expr is substituted into it.

An approximation of what I want looks like this:

function genfun(fun::Symbol,expr)
    @eval begin
        @generated function $fun(x,::Val{p}) where p
            :(@fastmath $(horner(df($$(QuoteNode(expr)),:x,p))))
        end
    end
end

However, I’m not exactly sure how to properly interpolate the expressions for the desired result.

Does anyone know how to do something like this?

NOTE this requires the master branch of Reduce, so Pkg.checkout


#2

You should use a macro for that.


#3

As I understand it, if I use a macro then the generated function will not be in the desired scope.

I need the function to be defined in my module, if I use a macro, it will not be available to call, right?

Either way, the main hurdle is the interpolation, which I would need to do whether it’s a macro or not.


#4

Assuming that you mean Reduce with “my module”:
If you add a method to a function defined in your module then it will be available there too.

Then, it it not just:

function genfun(fun::Symbol,expr)
    @eval begin
        @generated function $fun(x,::Val{p}) where p
            :(@fastmath $(horner(df(expr),:x,p))))
        end
    end
end

#7

Hm… it is closer to solution, but does not work completely yet. Since I had expr = :(x^2) defined in REPL, it seemed to work, but if I restart Julia, then expr is not found:

function genfun(fun::Symbol,expr)
    @eval begin
        @generated function $fun(x,::Val{p}) where p
            :(@fastmath $(horner(df(expr,:x,p))))
        end
        $fun(x,p::Int) = $fun(x,Val(p))
    end
end

example

julia> genfun(:f1,:((4x^4-44x^3+61x^2+270x-525)/100))
f1 (generic function with 3 methods)

julia> f1(2,0)
ERROR: UndefVarError: expr not defined
Stacktrace:
 [1] f1(...) at ./REPL[17]:4
 [2] f1(::Int64, ::Int64) at ./REPL[17]:6
 [3] macro expansion at ./REPL.jl:97 [inlined]
 [4] (::Base.REPL.##1#2{Base.REPL.REPLBackend})() at ./event.jl:73

So expr should be inserted into the generated function when genfun is called, but it is not inserted.


#8

Does nobody know how to get expr to insert into the generated function?

There must be a way to quote it…


#9

Not exactly what you asked for, but this is what it looks like now that I finally got it to work:

function genfun(fun::Symbol,expr)
    quote_expr = Expr(:quote, expr)
    fun_expr_val = quote
        @generated function $fun(x,::Val{p}) where p
            expr = $quote_expr
            dfexpr = df(expr,:x,p)
            hdfe = horner(dfexpr)
            :(@fastmath $hdfe)
        end
    end
    fun_expr = quote
        function $fun(x, p)
            Base.Cartesian.@nif 10 i -> (i-1 == p) i -> ($fun(x, Val{i-1}())) i -> throw("Unsupported!")
        end
    end
    @eval begin
        $fun_expr_val
        $fun_expr
    end
end
genfun(:f8,:((4x^4-44x^3+61x^2+270x-525)/100))
f8(2,0)#-0.29
f8(2,Val(0))#-0.29
f8(2,1)#1.14

To more directly answer your question, quote the expr: quote_expr = Expr(:quote, expr), so now you have two layers of quotes. Inserting deletes one layer, so you still have another left.

What I have above can probably be condensed. I just broke things into pieces while playing with it, to try and figure out what was going on.

EDIT:
A cool upgrade on the Base.Cartesian.@nif version would be to run inference on the return type of $fun for some Val(i), and then instead of throwing an error if p>10-1 (or whatever the cutoff for number of if statements), have it do a dynamic dispatch with a type assertion thrown on so the whole thing remains type stable.
You can’t do the dynamic dispatch without an assertion and have it still be type stable.


#10

Here is another challenge:

Thanks for your help. You actually somewhat predicted my next goal, but I wanted something different:

hdf(expr,p::Int) = horner(df(expr,:x,p))
function genfun(fun::Symbol,expr;depth=-1)
    if depth < 0
        @eval begin 
            @generated function $fun(x,::Val{p}) where p
                expr = $(Expr(:quote,expr))
                :(@fastmath $(horner(df(expr,:x,p))))
            end
            $fun(x,p::Int) = $fun(x,Val(p))
        end
    else
        lexpr = [Expr(:quote,hdf(expr,i)) for i ∈ 0:depth-1]
        @eval begin
            N = 1+$depth
            expr = $lexpr
            function $fun(x,p)
                macroexpand(quote
                    Base.Cartesian.@nif($N,
                        i->(i-1 == p),
                        i->($(:(expr[i]))),
                        i->throw(error("$(p)-th derivative not supported")))
                end)
            end
        end
    end
end

So the purpose of this function is that you can either make a generated function that evaluates derivatives up to arbitrary depth by default (with slower performance), but one should have the option of specifying a depth limit, which should then produce a plain function that uses the if-then-else structure.

julia> genfun(:fa,:(x^2))
fa (generic function with 2 methods)

julia> fa(2,0)
4

So the generated function now works properly. However, producing the right kind of if-then-else plain function for faster performance requires the expr[i] derivatives to be inserted in each conditional.

In order to help debug this, the produced function outputs the if-then-else code instead of evaluating:

julia> genfun(:fb,:(x^2);depth=2)
fb (generic function with 1 method)

julia> fb(2,0)
quote  # REPL[211], line 17:
    if 0 == p
        expr[1]
    else 
        if 1 == p
            expr[2]
        else 
            throw(error("$(p)-th derivative not supported"))
        end
    end
end

If someone could figure out how to insert / interpolate expr[i] into that, I would be very thankful.

Otherwise, instead of using Base.Cartesian.@nif, a new function needs to take care of it.


Interpolate array elements into `Base.Cartesian.@nif`