Macro for enabling/disabling parts of code

I’ve often desired to have “optional” execution paths in my code that I could activate from outside of the function. This would be helpful in situations where I have a complex function that does several data processing steps, resulting in some scalar result. However, sometimes there is something wrong with combination of input parameters and input data and I’d like to get access to some results of several processing sub-steps, so that I could plot them, or directly get their plots. On the other hand, having this “visual debugging” data is too expensive for real heavy calculation (think of having tens of 100MB intermediate arrays instead of doing operations inplace).
An idea:

using Plots
global p = plot()

macro debug_step(ex)
    return quote
        local val = $(esc(ex))
        val
    end
end

macro with_debug_step(ex)    
    return quote
        local val = $(esc(ex))
        val
    end
end

macro sayhello(name)
           return :( println("Hello, ", $name) )
       end

function heavy_calculation(arr, par)
    @debug_step plot!(p, deepcopy(arr), label="Orig arr")
    arr .-= par #heavy calculation depending on "tuning" parameter
    @debug_step plot!(p,deepcopy(arr), label="arr step1")
    arr .+= randn(length(arr)) #another heavy calculation
    @debug_step plot!(p,deepcopy(arr), label="arr step2")
    s =  sum(arr)
    if s >100
        @error("bad par value")
    end
    s
end

a = [randn(10).+44 for i=1:50]
#test run that with parameter 42 the heavy_calculation is well performing
@with_debug_step heavy_calculation(a[1], 42)
# check that all plots show correct behavior
plot(p)

#and now run again but the heavy_calculation function does not execute the expressions guarded by @debug_step  
for aa in a
    b = heavy_calculation(a[1], 42)
    println(b)
end

here I’ve used stub macro implementation that does nothing. The desired behavior would let @with_debug_step introduce some variable in the macro domain and it that is set the @debug_step macro would either execute or do no-op.

What is the right approach to write such cooperating macro pair? Could you give me an example of similar behavior elsewhere?

You could try a custom logger.

AFAICT log messages become nops when the log level is below the given number.

See
https://docs.julialang.org/en/v1/stdlib/Logging/#Using-Loggers-1

2 Likes

Something similar was discussed for disabling assertations, and I think the solution here might be able to be adapted to your case: @assert alternatives?

Thank you very much for the both recomendations.
I’ve flagged @JonasIsensee answer as this is the direction I’m proceeding. So now my workflow is using my custom VisualLogger type that encapsulates a ConsoleLogger and accumulates all the data and plots into a MultiDict instead of writing them into stderr or file. The reason for it is obvious. I want to see my debug plots immediately after running the calculation on a sample of data. the logger looks like this:

using Logging, DataStructures

struct VisualLogger <: AbstractLogger
    consolelogger::ConsoleLogger
    val::DataStructures.MultiDict
end
function VisualLogger(min_level = Logging.Debug)
    VisualLogger(ConsoleLogger(stderr, min_level, Logging.default_metafmt,
                  true, 0, Dict{Any,Int}()), DataStructures.MultiDict())
end

import Logging: shouldlog, min_enabled_level, handle_message

shouldlog(logger::VisualLogger, level, _module, group, id) =
    get(logger.consolelogger.message_limits, id, 1) > 0

min_enabled_level(logger::VisualLogger) = logger.consolelogger.min_level

function handle_message(logger::VisualLogger, level, message, _module, group, id,
                        filepath, line; maxlog = nothing, kwargs...)
    if maxlog != nothing && maxlog isa Integer
        remaining = get!(logger.consolelogger.message_limits, id, maxlog)
        logger.consolelogger.message_limits[id] = remaining - 1
        remaining > 0 || return
    end
    if !isempty(kwargs)
        for (key, val) in pairs(kwargs)            
            insert!(logger.val, key, val)
        end
    end
    handle_message(logger.consolelogger, level, message, _module, group, id,
                            filepath, line; maxlog = maxlog, kwargs...)
    nothing
end
import Base: getproperty, propertynames

function getproperty(logger::VisualLogger, sym::Symbol)
    val = getfield(logger, :val)
    if sym in keys(val)
        return val[sym]
    else # fallback to getfield
        return getfield(logger, sym)
    end    
end

function propertynames(logger::VisualLogger, private = false)
    return [keys(getfield(logger, :val))..., :val, :consolelogger]
end

Than its really convenient to call it

vl = VisualLogger()
with_logger(vl) do
    heavy_calculation(arr, par)
end
plot(VL.offset_check...)

I really appreciate the well thought design of the Logger infrastructure by @Chris_Foster and others.

2 Likes

Awesome, this is exactly the kind of thing I had in mind when I designed the logging frontend, but which ironically I’ve never actually used myself. It’s great to see it in use :slight_smile: