Julia 制作宏以将函数标记为已弃用

Julia 制作宏以将函数标记为已弃用,julia,metaprogramming,deprecated,Julia,Metaprogramming,Deprecated,在我之前的文章中,我发现标准库(Julia v1.5)宏@deprecate用于替换其他函数 我想制作一个宏mark_deprecated,它在应用于函数时具有以下效果: 在调用目标函数时打印可自定义的弃用警告(如果可能,仅在第一次调用时) 修改函数的文档(可查看为julia>?function_name),以包括弃用警告 当然,以后可能会包括许多其他方便的选项,例如指定替换函数的功能、生成错误而不是警告的选项等 我主要是在Julia metaprogramming中做这项练习,目前为止我没有任

在我之前的文章中,我发现标准库(Julia v1.5)宏
@deprecate
用于替换其他函数

我想制作一个宏
mark_deprecated
,它在应用于函数时具有以下效果:

  • 在调用目标函数时打印可自定义的弃用警告(如果可能,仅在第一次调用时)
  • 修改函数的文档(可查看为
    julia>?function_name
    ),以包括弃用警告
  • 当然,以后可能会包括许多其他方便的选项,例如指定替换函数的功能、生成错误而不是警告的选项等

    我主要是在Julia metaprogramming中做这项练习,目前为止我没有任何经验(有点担心这作为第一项任务可能太难了)

    试图理解@deprecate 作为第一步,我查看了当前的标准库@deprecate宏。内容如下:

    # julia v1.5
    # quoted from deprecated.jl, included by Base.jl
    
    macro deprecate(old, new, ex=true)
        meta = Expr(:meta, :noinline)
        if isa(old, Symbol)
            oldname = Expr(:quote, old)
            newname = Expr(:quote, new)
            Expr(:toplevel,
                ex ? Expr(:export, esc(old)) : nothing,
                :(function $(esc(old))(args...)
                      $meta
                      depwarn($"`$old` is deprecated, use `$new` instead.", Core.Typeof($(esc(old))).name.mt.name)
                      $(esc(new))(args...)
                  end))
        elseif isa(old, Expr) && (old.head === :call || old.head === :where)
            remove_linenums!(new)
            oldcall = sprint(show_unquoted, old)
            newcall = sprint(show_unquoted, new)
            # if old.head is a :where, step down one level to the :call to avoid code duplication below
            callexpr = old.head === :call ? old : old.args[1]
            if callexpr.head === :call
                if isa(callexpr.args[1], Symbol)
                    oldsym = callexpr.args[1]::Symbol
                elseif isa(callexpr.args[1], Expr) && callexpr.args[1].head === :curly
                    oldsym = callexpr.args[1].args[1]::Symbol
                else
                    error("invalid usage of @deprecate")
                end
            else
                error("invalid usage of @deprecate")
            end
            Expr(:toplevel,
                ex ? Expr(:export, esc(oldsym)) : nothing,
                :($(esc(old)) = begin
                      $meta
                      depwarn($"`$oldcall` is deprecated, use `$newcall` instead.", Core.Typeof($(esc(oldsym))).name.mt.name)
                      $(esc(new))
                  end))
        else
            error("invalid usage of @deprecate")
        end
    end
    
    我试图理解这一点(如果您理解宏,则无需阅读):

    • :meta
      的内容将在中解释
    • 宏中的变量
      oldname
      newname
      从不使用。我假设这是由于开发人员的疏忽造成的(尽管没有使用变量,但声明具有一些不明显的效果)。我移除它们
    • 我不知道如何处理
      a(…),其中B
      表达式(这样的表达式进入顶级elseif块)。现在不用担心那部分了。看起来像是
      中的
      表达式被简单地剥离了。与表达式中的
      :花括号相同。似乎在任何情况下,函数符号(oldsym)都是从表达式(第一个参数)中提取出来的
    • 我不明白Base.show\u unquoted到底做了什么。似乎它只是为了输出而将表达式“打印”成字符串,所以我不必担心细节
    • 宏的主要内容当然是返回的
      Expr
      。它声称它是在顶层进行评估的。我不关心的出口问题
    • 我不知道什么是
      Core.Typeof($(esc(oldsym))).name.mt.name
      。它似乎是函数的实际符号(与包含符号的字符串相反)
      Core.Typeof
      似乎与
      Typeof
      相同。您可以执行
      typeof(some_function).name.mt.name
      并从
      mt::Core.MethodTable
      中取出符号。有趣的是,对于这些低级数据结构及其字段,标签补全似乎不起作用
    朝向我的宏 试图剽窃上述内容:

    # julia v1.5
    module MarkDeprecated
    
    using Markdown
    import Base.show_unquoted, Base.remove_linenums!
    
    
    """
        @mark_deprecated old msg 
    Mark method `old` as deprecated. 
    Print given `msg` on method call and prepend `msg` to the method's documentation.
            MACRO IS UNFINISHED AND NOT WORKING!!!!!
    """
    macro mark_deprecated(old, msg="Default deprecation warning.", new=:())
        meta = Expr(:meta, :noinline)
        if isa(old, Symbol)
            # if called with only function symbol, e.g. f, declare method f(args...)
            Expr(:toplevel,
                :(
                    @doc(  # This syntax is riddiculous, right?!?
                        "$(Markdown.MD($"`$old` is deprecated, use `$new` instead.", 
                                    @doc($(esc(old)))))",
                        function $(esc(old))(args...)
                        $meta
                        warn_deprecated($"`$old` is deprecated, use `$new` instead.", 
                                Core.Typeof($(esc(old))).name.mt.name)
                        $(esc(new))(args...)
                        end
                    )
                )
            )
        elseif isa(old, Expr) && (old.head === :call || old.head === :where)
            # if called with a "call", e.g. f(a::Int), or with where, e.g. f(a:A) where A <: Int,
            # try to redeclare that method
            error("not implemented yet.")
            remove_linenums!(new)
            # if old.head is a :where, step down one level to the :call to avoid code duplication below
            callexpr = old.head === :call ? old : old.args[1]
            if callexpr.head === :call
                if isa(callexpr.args[1], Symbol)
                    oldsym = callexpr.args[1]::Symbol
                elseif isa(callexpr.args[1], Expr) && callexpr.args[1].head === :curly
                    oldsym = callexpr.args[1].args[1]::Symbol
                else
                    error("invalid usage of @mark_deprecated")
                end
            else
                error("invalid usage of @mark_deprecated")
            end
            Expr(:toplevel,
                :($(esc(old)) = begin
                $meta
                warn_deprecated($"`$oldcall` is deprecated, use `$newcall` instead.", 
                        Core.Typeof($(esc(oldsym))).name.mt.name)
                $(esc(old)) # TODO: this replaces the deprecated function!!!
            end))
        else
            error("invalid usage of @mark_deprecated")
        end
    end
    
    
    function warn_deprecated(msg, funcsym)
        @warn """
                Warning! Using deprecated symbol $funcsym.
                $msg
                """
    end
    
    end # Module MarkDeprecated
    
    问题 到目前为止,我没有做到我想要的两件事中的任何一件:

  • 调用方没有导入
    标记
    ,我如何处理这种情况,我用它连接docstring?(编辑:显然这不是问题?出于某种原因,尽管模块
    标记
    未导入到
    测试
    模块中,但修改似乎仍然有效。但我不完全理解原因。很难了解宏生成代码的每个部分在何处执行…)
  • 如何实际避免替换该函数?从内部调用它会创建一个无限循环。我基本上需要一个Python风格的装饰器?也许这样做的方法是只允许将
    @mark\u deprecated
    添加到实际的函数定义中?(这样一个宏实际上是我在标准库中期望找到的,并且在我掉进这个兔子洞之前使用)
  • 在我的示例中,宏(对于
    @deprecate
    )不会影响方法
    a(x)
    ,因为它只创建一个带有签名
    a(args…
    )的方法,当仅对函数符号调用宏时,该方法对一个参数调用的优先级较低。虽然对我来说并不明显,但这似乎是
    @deprecate
    想要的行为。但是,是否可以将宏对裸函数符号的默认应用程序设置为弃用所有方法

  • 我认为您想要实现的目标与
    Base@deprecate
    的目标不同。如果我理解正确:

    • 您不希望宏为不推荐使用的方法创建定义;您更希望为手写定义添加注释
    • 您想要修改docstring,而
      @deprecate
      不需要修改
    既然您这样做是为了学习元编程,也许您可以尝试一步一步地编写自己的宏,而不是理解
    Base.@deprecate
    如何工作并尝试调整它

    关于你的具体问题:

    1。当调用者不导入降价时,我如何处理这种情况?

    下面的示例可能有助于解释事物是如何工作的:

    module MyModule
    
    # Markdown.MD, Markdown.Paragraph and msg are only available from this module
    import Markdown
    msg(name) = "Hello $name"
    
    macro greet(name)
        quote
            # function names (e.g. Markdown.MD or msg) are interpolated
            # => evaluated at macro expansion time in the scope of the macro itself
            # => refer to functions available from within the module
            $(Markdown.MD)($(Markdown.Paragraph)($msg($name)))
    
            # (But these functions are not called at macro expansion time)
        end
    end
    end
    
    具体请参见
    msg
    如何正确引用
    Main.MyModule.msg
    ,这是您必须从“外部”上下文调用它的方式:

    2。也许这样做的方法是只允许将@mark\u不推荐添加到实际函数定义中?

    是的,这就是我要做的

    3。是否可以将宏对裸函数符号的默认应用程序设置为不推荐所有方法?

    我想从技术上讲,不推荐给定函数的所有方法是可能的。。。或者至少是在弃用代码运行时存在的所有方法。但是,之后将定义的方法呢? 我个人不会这样做,只标记方法定义



    也许类似的东西可以是一个存根,用作更复杂宏的起点,以精确地执行您想要的操作:

    module MarkDeprecate
    using Markdown
    using MacroTools
    
    function mark_docstring(docstring, message)
        push!(docstring,
              Markdown.Paragraph("Warning: this method is deprecated! $message"))
        docstring
    end
    
    function warn_if_necessary(message)
        @warn "This method is deprecated! $message"
    end
    
    macro mark_deprecate(msg, expr)
        fundef = splitdef(expr)
        prototype = :($(fundef[:name])($(fundef[:args]...);
                                       $(fundef[:kwargs]...)) where {$(fundef[:whereparams]...)})
    
        fundef[:body] = quote
            $warn_if_necessary($msg)
            $(fundef[:body])
        end
    
        quote
            Base.@__doc__ $(esc(MacroTools.combinedef(fundef)))
            Base.@doc $mark_docstring(@doc($prototype), $msg) $prototype
        end
    end
    end
    
    julia> @macroexpand MyModule.@greet "John"
    quote
        #= REPL[8]:8 =#
        (Markdown.MD)((Markdown.Paragraph)((Main.MyModule.msg)("John")))
    end
    
    julia> MyModule.@greet "John"
      Hello John
    
    module MarkDeprecate
    using Markdown
    using MacroTools
    
    function mark_docstring(docstring, message)
        push!(docstring,
              Markdown.Paragraph("Warning: this method is deprecated! $message"))
        docstring
    end
    
    function warn_if_necessary(message)
        @warn "This method is deprecated! $message"
    end
    
    macro mark_deprecate(msg, expr)
        fundef = splitdef(expr)
        prototype = :($(fundef[:name])($(fundef[:args]...);
                                       $(fundef[:kwargs]...)) where {$(fundef[:whereparams]...)})
    
        fundef[:body] = quote
            $warn_if_necessary($msg)
            $(fundef[:body])
        end
    
        quote
            Base.@__doc__ $(esc(MacroTools.combinedef(fundef)))
            Base.@doc $mark_docstring(@doc($prototype), $msg) $prototype
        end
    end
    end
    
    julia> """
               bar(x::Number)
           
           some help
           """
           MarkDeprecate.@mark_deprecate "Use foo instead" function bar(x::Number)
               42
           end
    bar
    
    julia> """
               bar(s::String)
           
           This one is not deprecated
           """
           bar(s::String) = "not deprecated"
    bar
    
    julia> methods(bar)
    # 2 methods for generic function "bar":
    [1] bar(s::String) in Main at REPL[4]:6
    [2] bar(x::Number) in Main at REPL[1]:23
    
    julia> @doc(bar)
      bar(x::Number)
    
      some help
    
      Warning: this method is deprecated! Use foo instead
    
      bar(s::String)
    
      This one is not deprecated
    
    julia> bar("hello")
    "not deprecated"
    
    julia> bar(5)
    ┌ Warning: This method is deprecated! Use foo instead
    └ @ Main.MarkDeprecate REPL[1]:12
    42