How do I create a function from an expression

question

#1

Suppose I have

expr = :(sin(x)+cos(x))

How can I get a function looking like

x->sin(x)+cos(x)

I have tried to write such a macro

macro expr2fn(expr, arg)
    return :($arg -> $(expr))
end

but it does not work

julia> f = @expr2fn expr x
(::#63) (generic function with 1 method)

julia> f(1.0)
:(sin(x) + cos(x))

Not sure what is happening here.


Generate functions inside a function from string
#2

You can call your macro as f = @expr2fn sin(x) + cos(x) x.

Taking the view of macros as functions that map expressions to expressions (https://docs.julialang.org/en/latest/manual/metaprogramming/#Hold-up:-why-macros?-1), with @expr2fn expr x you’re basically passing the arguments :(expr) and :(x) to a function with a signature expr2fn(::Expr, ::Expr)::Expr; that’s not what you want.

Instead you could do f = eval(:(x -> $expr)).


#3

Sadly that’s not I want.

  1. expr=:(sin(x)+cos(x)) is not constructed by hand. It’s returned symbolic calculation.
  2. I don’t want eval since eval always acts on global scope, which limits the possibility of using it inside a function.

#4

Could you post a concrete example where the eval construction doesn’t do what you want?


#5

Copied from Generate functions inside a function from string

function foo()
    return eval(parse("x->x"))
end
foo()(1)  
# ERROR: MethodError: no method matching (::##9#10)(::Int64)
# The applicable method may be too new: 
# running in world age 21870, while current world is 21871.

#6

But I think I found a fix

macro expr2fn(fname, expr, args...)
    fn = quote
        function $(esc(fname))()
            $(esc(expr.args[1]))
        end
    end
    for arg in args
        push!(fn.args[2].args[1].args, esc(arg))
    end
    return fn
end

@expr2fn(f, :(sin(x+y)), x, y)

julia> f(1.0, 1.0)
0.9092974268256817

I guess it’s just the documentation is never clear about what I can get if I pass a quoted expression into a macro.


#7

FWIW, eval works fine on 0.7:

   _       _ _(_)_     |  A fresh approach to technical computing
  (_)     | (_) (_)    |  Documentation: https://docs.julialang.org
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 0.7.0-beta.201 (2018-07-07 22:13 UTC)
 _/ |\__'_|_|_|\__'_|  |  Commit cdd4e84ac9 (5 days old master)
|__/                   |  x86_64-apple-darwin14.5.0

julia> function foo()
           return eval(Meta.parse("x->x"))
       end
foo (generic function with 1 method)

julia> foo()(1)
1

#8

Also, assigning the function to a variable circumvents the world age issue on 0.6:

julia> function foo()
           return eval(parse("x->x"))
       end
foo (generic function with 1 method)

julia> f = foo()
(::#3) (generic function with 1 method)

julia> f(1)
1

#9

That helps you at the REPL but not so much in a function.

julia> function g()
           f = foo()
           println(f(1))
       end
g (generic function with 1 method)

julia> g()
ERROR: MethodError: no method matching (::##1#2)(::Int64)
The applicable method may be too new: running in world age 21838, while current world is 21839.
Closest candidates are:
  #1(::Any) at none:1 (method too new to be called from this world context.)
Stacktrace:
 [1] g() at ./REPL[13]:3

#10

Yep, you’re right. And that one doesn’t work on 0.7 either.


#11

This only works because you’re sending in the literal :(sin(x+y)) to your macro.

julia> expr = :(sin(x+y))
:(sin(x + y))

julia> @expr2fn(f, expr, x, y)
ERROR: type Symbol has no field args

Fundamentally macros work on syntax, not on values, and since you want to be able to generate functions from values, macros are not the appropriate tool. As far as I know only eval in some form can get you there.


#12

Yeah, I think invokelatest is the only way out then if you really want this to work in local scope. I’ve personally never needed anything like this by the way; maybe you could talk a little about your intended application?


#13

This may be illustrative of the problem and two possible ways to circumvent the world age issue.

julia> expr = :(sin(x + y))
:(sin(x + y))

julia> function f1(expr, x, y)
           f = eval(:((x, y) -> $expr))
           return f(x, y)
       end
f1 (generic function with 1 method)

julia> function f2(expr, x, y)
           f = eval(:((x, y) -> $expr))
           return @eval $f($x, $y)
       end
f2 (generic function with 1 method)

julia> function f3(expr, x, y)
           f = eval(:((x, y) -> $expr))
           return Base.invokelatest(f, x, y)
       end
f3 (generic function with 1 method)

julia> f1(expr, 1, 2)
ERROR: MethodError: no method matching (::##1#2)(::Int64, ::Int64)
The applicable method may be too new: running in world age 21836, while current world is 21837.
Closest candidates are:
  #1(::Any, ::Any) at REPL[2]:2 (method too new to be called from this world context.)
Stacktrace:
 [1] f1(::Expr, ::Int64, ::Int64) at ./REPL[2]:3

julia> f2(expr, 1, 2)
0.1411200080598672

julia> f3(expr, 1, 2)
0.1411200080598672

#14

Thank you for all the comments. What do you think of my following implementation?

module Expr2Fn

export @expr2fn, setexpr

expr = :(sin(x+y))

macro expr2fn(fname, args...)
    fn = quote
        function $(esc(fname))()
            $expr
        end
    end
    for arg in args
        push!(fn.args[2].args[1].args, esc(arg))
    end
    return fn
end

function setexpr(extexpr)
    global expr = extexpr
end

end

using Expr2Fn

setexpr(:(cos(x+y)))

@expr2fn(f, x, y)

julia> f(1.0, 1.0)
-0.4161468365471424

A little bit abuse of Hygiene but seems to work.


#15

Please see the post for intention (I am her colleague):

I am starting a new thread because I think eval is not the way out since it’s too restrictive to support local scope.


#16

I think you will find, when you try to run it in a local scope, that it’s less local than you hope and/or runs into world age issues.


#17

You can make that macro statement a lot simpler like this

macro expr2fn(expr, fname, args...)
    return Expr(:function, Expr(:call, esc(fname), args...), expr)
end

although unfortunately the function that is defined by a macro like this is not accessible in the calling scope.


#18

Not sure if this is the “right” way to do it, but it works:

instead of
julia> f = @expr2fn expr x

try:

julia> expr2 = :(f = @expr2fn $expr x)
:(f = @expr2fn(sin(x) + cos(x), x))

julia> eval(expr2)
(::#21) (generic function with 1 method)

julia> f(1.0)
1.3817732906760363


#19

Can I back this up a level and ask what you are trying to accomplish? I feel like this may be a XY problem situation.


#20

That’s impossible.

No, it’s the global scope that does.

If the input is not static (if it’s static you should/could use a macro to do the symbolic calculation) then if you want to create the function you must use eval. After you’ve done with all the function/expression construction, you can use eval or invokelatest to call the code that uses those functions.