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.