Reputation: 1620
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:
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).
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):
:meta
thing is explained in the documentation.oldname
and newname
inside 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.a(...) where B
expressions (such an expression enters the toplevel elseif block). Not going to worry about that part for now. It seems like the where
expression is simply stripped off anyway. Same with :curly
brackets in the expression. It seems like in any case the function symbol (oldsym) is extracted from the expression (first argument).Base.show_unquoted
does exactly. Seems like it "prints" Expressions into strings just for output so I won't worry about the details.Expr
. It asserts that it is evaluated at top level. The export thing I don't care about.Core.Typeof($(esc(oldsym))).name.mt.name
is. It seems to be the actual Symbol
of the function (as opposed to a string containing the symbol). Core.Typeof
seems to be the same as typeof
. You can do typeof(some_function).name.mt.name
and get the symbol out from the mt::Core.MethodTable
. Interestingly, the Tab-Completion doesn't seem to work for these low level data-structures and their fields.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
module Testing
import ..MarkDeprecated # (if in the same file)
a(x) = "Old behavior"
MarkDeprecated.@mark_deprecated a "Message" print
a("New behavior?")
end
I so far failed to do any of the two things I wanted:
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 module Markdown
not being imported in the Testing
module. I don't fully understand why though. It's hard to follow where each part of the macro generated code is executed...)@mark_deprecated
to 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)@deprecate
) does not affect the method a(x)
in my example since it only creates a method with signature a(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?Upvotes: 2
Views: 301
Reputation: 20298
I think what you want to achieve is not the same thing as what Base.@deprecate
does. If I understand correctly:
@deprecate
does notAnd since you're doing this as an exercise to learn metaprogramming, maybe you could try writing your own macro step by step, rather than understanding how Base.@deprecate
works and trying to adapt it.
As for your specific questions:
1. How do I deal with a situation when the caller doesn't import Markdown?
Maybe the following example helps explaining how things work:
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
See in particular how msg
correctly refers to Main.MyModule.msg
, which is how you have to call it from the "outside" context:
julia> @macroexpand MyModule.@greet "John"
quote
#= REPL[8]:8 =#
(Markdown.MD)((Markdown.Paragraph)((Main.MyModule.msg)("John")))
end
julia> MyModule.@greet "John"
Hello John
2. Perhaps the way to do it is to only allow adding the @mark_deprecated to the actual function definition?
Yes, that is what I would do.
3. Is it possible to default application of the macro to the bare function symbol to deprecating all methods?
I guess it would technically be possible to deprecate all methods of a given function... or at least all methods that exist at the time when your deprecation code runs. But what about methods that would be defined afterwards? I personally would not go that way, marking only method definitions.
Maybe something like this could be a stub to be used as a starting point for a more complex macro doing precisely what you want:
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
Upvotes: 2