In my earlier question I found out that the standard library (Julia v1.5) macro @deprecate is used to replace functions by others.
I want to make a macro mark_deprecated that has the following effect when applied to a function:
- Print a customizable deprecation warning when the target function is called (if possible, only for the first time it is called).
- Modify the function's documentation (viewable as
julia>? function_name) to also include the deprecation warning.
Of course many other convenient options may be included later, such as the ability to specify a replacement function, an option to produce errors instead of warnings, etc.
I am mainly doing this as an exercise in Julia metaprogramming, in which I have zero experience so far (slightly worried this could be too hard as a first task).
Trying to understand @deprecate
As a first step, I looked at the current Standard Library @deprecate macro. It goes as follows:
# 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
My attempts at understanding this thing (NO NEED TO READ IF YOU UNDERSTAND THE MACRO):
- The
:metathing is explained in the documentation. - The variables
oldnameandnewnameinside the macro are never used. I assume that this is due to negligence of the developers (as opposed to the declarations having some non-obvious effect despite the variables not being used). I remove them. - I'm not sure how to handle
a(...) where Bexpressions (such an expression enters the toplevel elseif block). Not going to worry about that part for now. It seems like thewhereexpression is simply stripped off anyway. Same with:curlybrackets in the expression. It seems like in any case the function symbol (oldsym) is extracted from the expression (first argument). - I don't understand what
Base.show_unquoteddoes exactly. Seems like it "prints" Expressions into strings just for output so I won't worry about the details. - The main meat of the macro is of course the returned
Expr. It asserts that it is evaluated at top level. The export thing I don't care about. - I have no idea what
Core.Typeof($(esc(oldsym))).name.mt.nameis. It seems to be the actualSymbolof the function (as opposed to a string containing the symbol).Core.Typeofseems to be the same astypeof. You can dotypeof(some_function).name.mt.nameand get the symbol out from themt::Core.MethodTable. Interestingly, the Tab-Completion doesn't seem to work for these low level data-structures and their fields.
Towards my macro
Trying to plagiarize the above:
# 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
For testing:
module Testing
import ..MarkDeprecated # (if in the same file)
a(x) = "Old behavior"
MarkDeprecated.@mark_deprecated a "Message" print
a("New behavior?")
end
Problems
I so far failed to do any of the two things I wanted:
- How do I deal with a situation when the caller doesn't import
Markdown, which I use to concatenate the docstrings? (EDIT: Apparantly this is not a problem? For some reason the modification seems to work despite the moduleMarkdownnot being imported in theTestingmodule. I don't fully understand why though. It's hard to follow where each part of the macro generated code is executed...) - How do I actually avoid replacing the function? Calling it from inside itself creates an infinite loop. I basically need a Python-style decorator? Perhaps the way to do it is to only allow adding the
@mark_deprecatedto the actual function definition? (such a macro would actually be what I was expecting to find in the standard library and just use before I fell down this rabbithole) - The macro (which is also true for
@deprecate) does not affect the methoda(x)in my example since it only creates a method with signaturea(args...), which has lower priority for one argument calls, when the macro is called on the function symbol alone. While not obvious to me, this seems to be desired behaviour for@deprecate. However, is it possible to default application of the macro to the bare function symbol to deprecating all methods?