函数体内调eval,怎样将符号解析为局部变量?


#1

例如:
julia> function goodfunc(x, s=:s1)
s1(y) = y + x;
s2(y) = y*x;
return eval(s);
end
goodfunc (generic function with 2 methods)

尝试: goodfunc( 22 )
输出错误:ERROR: UndefVarError: s1 not defined
明明s1是函数体内定义的符号,而eval函数的调用也是在函数体内,为何不能将符号解析成局部所定义的函数句柄?如果希望将符号解析为当前的局部变量应该怎么做?


#2

支持这个feature的话,函数就变的过于动态,不利于编译器优化,所以不支持。Julia的解决方案是(trait-based)multiple dispatch


#3

所以为啥不能是 s==:s1 ? s1 : s2 之类的写法呢?你在Julia里会发现性能差的写法都很别扭(或者压根不能通过)

Julia只允许生成代码,而不能修改已经生成的代码。函数会在被调用前编译(eval),你在函数内部eval就意味着要运行时修改编译出来的代码。所以这个和设计初衷就是违背的。


#4

最终我把函数写成了类似这种形式:
function goodfunc(x, s=:s1)
exported = Dict()
s1(y) = y + x;
exported[:s1] = s1;
s2(y) = y*x;
exported[:s2] = s2;
return exported[s]
end

因为函数的目的就是通过设置参数选择函数句柄,所以只需要调用一次,性能上的差别无关紧要.虽然用字典把函数名符号映射为函数指针的办法可能比从eval解析的办法性能好,但是这种写法貌似比较麻烦.所以,最好有更自动化的写法,比如自动生成局部变量句柄字典之类的.


#5

性能无关紧要直接用 value type Val 做 dispatch 就好了:

julia> _goodfunc(::Val{:s1}, x) = s1 = y -> y + x
_goodfunc (generic function with 2 methods)

julia> _goodfunc(::Val{:s2}, x) = s2 = y -> y * x
_goodfunc (generic function with 2 methods)

julia> goodfunc(x, s=:s1) = _goodfunc(Val(s), x)
goodfunc (generic function with 2 methods)

julia> goodfunc(1)
#7 (generic function with 1 method)

julia> goodfunc(1)(2)
3

julia> goodfunc(1, :s2)(2)
2

#6

谢谢提醒,Val的用法第一次见到.不过,如果有多个输入参数值就不好匹配了,每一种排列组合的可能结果都得列出一种匹配.
如果像这样写一个大的闭包函数,直接用所有的超参数定制出一批底层函数,一次性以字典输出,使用时调用省事,而且代码也容易改写和扩展.


#7

我觉得你的设计有问题。这个情况应当使用多重派发,用类型来派发函数。不是性能的问题,是更容易阅读。因为编译器会给你解决不同变量组合的问题,你可以有一堆叫s的函数,输入不同类型的值。而不是根据keyword选择不同的s。


#8

你的情况不是普通函数,而是一个代码生成的子领域,叫partial evaluation。

其中这种specify部分参数,得到更优算法的做法,叫staging。

julia有一种方便的,很接近staging的方法, 叫generated function

function goodfunc(x, s = :s1)
     gengoodfunc(x, Val(s))
end

@generated function gengoodfunc(x, ::Val{S}) where S
         Dict(
             :s1 => :(y -> y + x),
             :s2 => :(y -> y * x)
         )[S]
 end

generated形式上并不处理任意staging,只针对类型的staging(但可以处理不可变值的staging, 见后)

更general的staging可以通过eval来做,比如下列做法:

function goodfunc(x, s = :s1)
     gengoodfunc(x, s)
end

function gengoodfunc(x, s)
         Dict(
             :s1 => () -> :(y -> y + $x),
             :s2 => () -> :(y -> y * $x)
         )[s]() |> @__MODULE__.eval
 end

值得注意的是,generated function比起general purpose的staging有一个好处,那就是不需要人工手工生成staged的函数。

例如上面两段代码,使用generated func时,无数次调用goodfunc只生成一次代码,而使用后者的general staging时,每次调用goodfunc均生成一次代码,这也意味着真正使用general staging函数时,需要先手动生成staged函数,然后多次调用staged结果。

而为什么generared func有这个好处,是因为它可以通过multiple dispatch对staging结果进行cache( 同时由于是multi dispatch, 还可以尽可能的静态编译 )。general staging不保证能cache,因为参数类型不一定hashable。

multiple dispatch支持分派不可变值(在trait角度讲,immutable <: hashable),所以generated func的表达力上界是staging时要specify的参数是immutable的任何情况。

This is the whole story.

最后。
动态修改作用域的语言确实存在,例如python的locals(),你可以按名(一个运行时字符串)访问变量,但不能添加新的变量符号,且开销巨大。

julia中如果你想这么做也是有办法的,不要eval(s), 类似py, 使用 (@locals)[s],需要julia1.1+。