Do I need a macro to compile a list of functions?

Ok, here is a rough sketch for a macro solution:

using Random

abstract type AbstractProblem end

# Generic functions
function column_names end
function problem_data end
function problem_solution end

# Note: API as in solution by abraemer

# Define sample problem by hand
struct Problem4_1 <: AbstractProblem end

column_names(::Problem4_1) = [
    :pr4_1_swimming_pool_size_liters, 
    :pr4_1_pipe_inflow_liters_sec,
    :pr4_1_pipe_outflow_liters_sec, 
    :pr4_1_time_to_fill_min]

function problem_data(::Problem4_1, rng::AbstractRNG)
    swimming_pool_size_liters = rand(rng, 1000:10:2000)
    pipe_inflow_liters_sec = rand(rng, 20:30)
    pipe_outflow_liters_sec = rand(rng, 5:10)
    #
    time_to_fill_min = problem_solution(Problem4_1(), swimming_pool_size_liters, pipe_inflow_liters_sec, pipe_outflow_liters_sec)
    #
    return (swimming_pool_size_liters,pipe_inflow_liters_sec,pipe_outflow_liters_sec,
            time_to_fill_min)
end

function problem_solution(::Problem4_1, swimming_pool_size_liters, pipe_inflow_liters_sec, pipe_outflow_liters_sec)
    return swimming_pool_size_liters/(pipe_inflow_liters_sec-pipe_outflow_liters_sec)/60
end

# Macro to define such problems

skiplinenums(exprs) = filter(e -> !(e isa LineNumberNode), exprs)

function parse_body(body)
    defs = []
    sol = nothing
    for expr in skiplinenums(body.args)
        if expr.head == :call && expr.args[1] == :(~)
            push!(defs, expr.args[2] => expr.args[3])
        elseif expr.head == :macrocall && expr.args[1] == Symbol("@solution") && isnothing(sol)
            sol = expr.args[end]
            @assert sol.head == :(=)
        else
            error("TODO: Better error message/handling!")
        end
    end
    defs, sol
end

macro problem(name, body)
    @assert body.head == :block "Syntax error: Expecting block of definitions!"
    defs, sol = parse_body(body)
    colnames = [var for (var, val) in defs]
    quote
        begin
            struct $(esc(name)) <: AbstractProblem end
            function $(esc(:column_names))(::$(esc(name)))
                [$([:(Symbol($(string(c)))) for c in colnames]...)]
            end
            function $(esc(:problem_solution))(::$(esc(name)), $(esc.(first.(defs))...))
                $(esc(sol.args[2]))
            end
            function $(esc(:problem_data))(problem::$(esc(name)), rng::AbstractRNG)
                $([:($(esc(var)) = rand(rng, $(esc(val)))) for (var, val) in defs]...)
                $(esc(sol.args[1])) = $(esc(:problem_solution))(problem, $(esc.(colnames)...))
                ($(esc.(colnames)...), $(esc(sol.args[1])))
            end
        end
    end
end

macro problemset(name, body)
    @assert body.head == :block "Syntax error: Expecting block of definitions!"
    prob_names = []
    for prob in skiplinenums(body.args)
        @assert (prob.head == :macrocall && prob.args[1] == Symbol("@problem")) "Only problems allowed in problemset!"
        push!(prob_names, skiplinenums(prob.args)[2])
    end
    quote
        $(esc(body))
        $(esc(name)) = [$([:($(esc(prob))()) for prob in prob_names]...)]
    end
end

# Check with @macroexpand that this basically generates the same code as above for Problem4_1

@problem Problem4_2 begin
    swimming_pool_size_liters ~ 1000:10:2000
    pipe_inflow_liters_sec ~ 20:30
    pipe_outflow_liters_sec ~ 5:10
    @solution time_to_fill_min = swimming_pool_size_liters/(pipe_inflow_liters_sec-pipe_outflow_liters_sec)/60
end

Random.seed!(123)
@show column_names(Problem4_2())
@show problem_data(Problem4_2(), Random.default_rng())

@problemset MyProblems begin
    @problem Problem4_3 begin
        swimming_pool_size_liters ~ 1000:10:2000
        pipe_inflow_liters_sec ~ 20:30
        pipe_outflow_liters_sec ~ 5:10
        @solution time_to_fill_min = swimming_pool_size_liters/(pipe_inflow_liters_sec-pipe_outflow_liters_sec)/60
    end
    @problem Simple4 begin
        x ~ 1:3
        y ~ 2:5
        @solution xy = x + y
    end
end

Random.seed!(123)
@show column_names.(MyProblems)
@show problem_data.(MyProblems, Ref(Random.default_rng()))

Note that the syntax is quite strict and error handling is somewhat rough.

1 Like