ArguMend.jl

Dev Build Status Coverage Aqua QA

ArguMend.jl injects a function with logic to help with mistyped keyword arguments.

@argumend function f(a, b; niterations=10, kw2=2)
    a + b - niterations + kw2
end

This results in a nicer mechanism for MethodErrors:

julia> f(1, 2; iterations=1)
ERROR: SuggestiveMethodError: in call to `f`, found unsupported
       keyword argument: `iterations`, perhaps you meant `niterations`

This becomes increasingly useful when calling into a large interface with many possible options.

This mechanism has (very likely) zero runtime cost, as it relies on adding splatted keyword arguments to the function call, which will re-compile the function if any keyword arguments change, skipping the ArguMend functions altogether.

You can also set this up manually, without a macro:

using ArguMend: suggest_alternative_kws, SuggestiveMethodError

function f(; kws...)
    valid_kws = (:kw1, :kw2, :kw3)
    invalid_kws = filter(k -> !(k in valid_kws), keys(kws))
    if !isempty(invalid_kws)
        msg = suggest_alternative_kws(invalid_kws, valid_kws)
        throw(SuggestiveMethodError(f, msg))
    end

    # Do other stuff
end

The core function used for computing candidate keywords is extract_close_matches, which is a clean-room pure-Julia re-implementation of Python's difflib.getclosematches.

Longer example

I wrote this because SymbolicRegression.jl has a massive number of options, and I wanted the error to tell me or a user what parameter name was mistyped.

<details> <summary> The full list of options is as follows (expand): </summary>

function Options(;
    binary_operators=[+, -, /, *],
    unary_operators=[],
    constraints=nothing,
    elementwise_loss::Union{Function,Nothing}=nothing,
    loss_function::Union{Function,Nothing}=nothing,
    tournament_selection_n::Integer=12,
    tournament_selection_p::Real=0.86,
    topn::Integer=12,
    complexity_of_operators=nothing,
    complexity_of_constants::Union{Nothing,Real}=nothing,
    complexity_of_variables::Union{Nothing,Real}=nothing,
    parsimony::Real=0.0032,
    dimensional_constraint_penalty::Union{Nothing,Real}=nothing,
    dimensionless_constants_only::Bool=false,
    alpha::Real=0.100000,
    maxsize::Integer=20,
    maxdepth::Union{Nothing,Integer}=nothing,
    turbo::Bool=false,
    bumper::Bool=false,
    migration::Bool=true,
    hof_migration::Bool=true,
    should_simplify::Union{Nothing,Bool}=nothing,
    should_optimize_constants::Bool=true,
    output_file::Union{Nothing,AbstractString}=nothing,
    node_type=nothing,
    populations::Integer=15,
    perturbation_factor::Real=0.076,
    annealing::Bool=false,
    batching::Bool=false,
    batch_size::Integer=50,
    mutation_weights=NamedTuple(),
    crossover_probability::Real=0.066,
    warmup_maxsize_by::Real=0.0,
    use_frequency::Bool=true,
    use_frequency_in_tournament::Bool=true,
    adaptive_parsimony_scaling::Real=20.0,
    population_size::Integer=33,
    ncycles_per_iteration::Integer=550,
    fraction_replaced::Real=0.00036,
    fraction_replaced_hof::Real=0.035,
    verbosity::Union{Integer,Nothing}=nothing,
    print_precision::Integer=5,
    save_to_file::Bool=true,
    probability_negate_constant::Real=0.01,
    seed=nothing,
    bin_constraints=nothing,
    una_constraints=nothing,
    progress::Union{Bool,Nothing}=nothing,
    terminal_width::Union{Nothing,Integer}=nothing,
    optimizer_algorithm::AbstractString="BFGS",
    optimizer_nrestarts::Integer=2,
    optimizer_probability::Real=0.14,
    optimizer_iterations::Union{Nothing,Integer}=nothing,
    optimizer_f_calls_limit::Union{Nothing,Integer}=nothing,
    optimizer_options=NamedTuple(),
    use_recorder::Bool=false,
    recorder_file::AbstractString="pysr_recorder.json",
    early_stop_condition::Union{Function,Real,Nothing}=nothing,
    timeout_in_seconds::Union{Nothing,Real}=nothing,
    max_evals::Union{Nothing,Integer}=nothing,
    skip_mutation_failures::Bool=true,
    nested_constraints=nothing,
    deterministic::Bool=false,
    # Not search options; just construction options:
    define_helper_functions::Bool=true,
    deprecated_return_state=nothing,
)
    return nothing
end

</details>

If I wrap this call with @argumend, I get much more useful error messages:

julia> Options(; npopulations=3)
ERROR: SuggestiveMethodError: in call to `Options`, found unsupported keyword argument:
      `npopulations`, perhaps you meant `populations` or `population_size`

rather than the standard Julia output (scroll to the right):

ERROR: MethodError: no method matching Options(; npopulations::Int64)

Closest candidates are:
  Options(; binary_operators, unary_operators, constraints, elementwise_loss, loss_function, tournament_selection_n, tournament_selection_p, topn, complexity_of_operators, complexity_of_constants, complexity_of_variables, parsimony, dimensional_constraint_penalty, dimensionless_constants_only, alpha, maxsize, maxdepth, turbo, bumper, migration, hof_migration, should_simplify, should_optimize_constants, output_file, node_type, populations, perturbation_factor, annealing, batching, batch_size, mutation_weights, crossover_probability, warmup_maxsize_by, use_frequency, use_frequency_in_tournament, adaptive_parsimony_scaling, population_size, ncycles_per_iteration, fraction_replaced, fraction_replaced_hof, verbosity, print_precision, save_to_file, probability_negate_constant, seed, bin_constraints, una_constraints, progress, terminal_width, optimizer_algorithm, optimizer_nrestarts, optimizer_probability, optimizer_iterations, optimizer_f_calls_limit, optimizer_options, use_recorder, recorder_file, early_stop_condition, timeout_in_seconds, max_evals, skip_mutation_failures, nested_constraints, deterministic, define_helper_functions, deprecated_return_state) got unsupported keyword argument "npopulations"