Julia — получение параметров во время выполнения внутри созданной во время выполнения функции

#julia #metaprogramming

#джулия #метапрограммирование

Вопрос:

Я пытаюсь создать макрос, который заменяет тело функции таким образом, чтобы:

 @foo function bar(a,b,c)
   *my code here*
end
  

и я продвинулся довольно далеко, но мой код здесь должен знать параметры bar(...) , которые можно выполнить
с помощью Base.@locals . Но проблема в том, что @foo ее нужно будет вставить, и я думаю, что она использует неправильную «локальную область» или вычисляется до появления параметров.

foo выглядит примерно так: ( expr.args[2] является телом функции)

 macro foo(expr::Expr)
    ...
    expr.args[2] = quote
        locals = Base.@locals
        println(locals)
        ...
    end
    
    :($expr)
end
  

Но при вызове этой созданной позже функции выводится только пустой dict, тогда как созданная вручную функция выводит параметры при использовании того же кода:

 function test(a,b,c)
    println(Base.@locals)
end
test(1,2,3) # returns: Dict{Symbol,Any}(:a => 1,:b => 2,:c => 3)
  

Итак, мой вопрос в том, как мне обойти это, чтобы Base.@locals (или аналогичный метод) дал мне параметры?

Ответ №1:

Вместо того, чтобы полагаться на Base.@locals , я бы получал аргументы функции непосредственно из определения и делал что-то вроде этого:

 using MacroTools

macro foo(expr)
    def = MacroTools.splitdef(expr)

    make_pair(arg) = :($(Meta.quot(arg)) => $arg)
    def[:body] = quote
        d = Dict($(map(make_pair, def[:args])...))
        println(d)
        $(def[:body])
    end

    MacroTools.combinedef(def)
end
  

Несколько замечаний и пояснений к этому коду:

  • Макросы, пытающиеся переписать / изменить определения функций, могут значительно выиграть от использования функций splitdef и combinedef MacroTools . Глобально предлагаемый макрос состоит из 3 этапов:
    1. разделите определение функции на отдельные части;
    2. вставьте нужный код в начало тела функции;
    3. повторно объедините все части (включая измененное тело) в законное определение функции.
  • Словарь, содержащий аргументы функции, состоит из следующих ключевых компонентов:
    • make_pair принимает заданное имя переменной (скажем, в виде символа :a ) и возвращает выражение, вычисляющее пару :a => a ;
    • затем эта make_pair функция map передается всем аргументам, исходящим из (разложенного) определения функции, для построения массива пар аргументов;
    • наконец, этот массив помещается в Dict конструктор.

Мы можем проверить, что этот пример определения функции расширяется до ожидаемого кода:

 julia> Base.remove_linenums!(@macroexpand @foo function bar(a,b,c)
           a   b   c
       end)
:(function Main.bar(var"#26#a", var"#27#b", var"#28#c"; )
      var"#25#d" = Main.Dict(:a => var"#26#a", :b => var"#27#b", :c => var"#28#c")
      Main.println(var"#25#d")
      begin
          var"#26#a"   var"#27#b"   var"#28#c"
      end
  end)
  

… и в более общем плане все работает так, как ожидалось, и во время выполнения:

 julia> @foo function bar(a, b, c)
           a   b   b
       end
bar (generic function with 1 method)

julia> bar(1, 2, 3)
Dict(:a => 1,:b => 2,:c => 3)
5