Skip to content

11. Methods and overlays

The strength of a cube and the strength of a cylinder are calculated by two different formulas, and which one applies depends on the specimen, not on a number. This chapter declares one method with a variant for each shape, and then changes how the method rounds for one jurisdiction with an overlay. Each trace says which variant ran and whose rounding rule applied.

The program is self-contained: it declares its own quantities and does not build on the previous chapters' programs.

One method, several variants

A variant applies to a kind of specimen, and the kind is named by a tag: an empty struct, never instantiated:

struct Cube
{
};
struct Cylinder
{
};

The cube is measured by the two sides of its loaded face, the cylinder by its diameter:

using Load = formula::Quantity<struct LoadTag, "F", "maximum load", unit::Kilonewton>;
using SideA = formula::Quantity<struct SideATag, "a", "first side of the loaded face", unit::Millimetre>;
using SideB = formula::Quantity<struct SideBTag, "b", "second side of the loaded face", unit::Millimetre>;
using Diameter = formula::Quantity<struct DiameterTag, "d", "diameter of the cylinder", unit::Millimetre>;

formula::method(variants, rounding rule, constraints) builds a method from its three parts, in that order:

constexpr formula::DecimalRounding tenthMpa { unit::Megapascal,
                                              formula::DecimalPlaces { 1 },
                                              formula::RoundingMode::HalfAwayFromZero };

inline constexpr auto compressiveStrength = formula::method(
    formula::variants(
        formula::variant<Cube>(var<Load> / (var<SideA> * var<SideB>)),
        formula::variant<Cylinder>(var<Load> / (formula::pi * formula::pow<2>(var<Diameter>) / formula::number(4)))),
    formula::rounding_rule<tenthMpa>(),
    formula::constraints());
  • formula::variants(...) lists the variants, each written formula::variant<Tag>(expression). The cube's strength is the load over a * b, the cylinder's the load over pi * d^2 / 4. formula::pi is the library's fixed fraction for pi, 245850922/78256779, within 8e-17 of it; the trace shows which number was used. Every variant must measure the same dimension, here a stress.
  • formula::rounding_rule<tenthMpa>() rounds whichever variant ran, to one decimal place of a megapascal, ties away from zero.
  • formula::constraints() is the list of checks a result must pass; this method has none.

formula::explain_method<Tag>(method, environment) runs the variant Tag names, rounds the result by the method's rule, and records each step. Its outcome is a std::expected that holds an error or a std::optional, empty when a measurement the variant reads is missing; a value is in the coherent SI unit, here pascals. Its trace is the trace:

/// Evaluates @p method for the variant @p Tag names, and prints @p heading and
/// the trace. False when the method fails or gives no value.
template <typename Tag, typename M, typename Env>
bool show(char const* heading, M const& method, Env const& specimen)
{
    auto const run = formula::explain_method<Tag>(method, specimen);
    if (!run.outcome)
    {
        std::println("{}: {}", heading, run.outcome.error());
        return false;
    }
    if (!run.outcome->has_value())
    {
        std::println("{}: no value", heading);
        return false;
    }
    std::println("{}:", heading);
    std::print("{}", formula::render_trace(run.trace, { .maxSteps = 20 }));
    return true;
}

The tag is always stated by the caller, who knows what the specimen is. A tag the method has no variant for does not compile.

The specimens

A cube of 150 mm by 150 mm carried 675 kN, and a cylinder of 150 mm diameter carried 540 kN:

auto const cube = formula::environment(
    formula::Measured<Load> { 675 }, formula::Measured<SideA> { 150 }, formula::Measured<SideB> { 150 });
auto const cylinder = formula::environment(formula::Measured<Load> { 540 }, formula::Measured<Diameter> { 150 });

Each environment holds only what its variant reads.

if (!show<Cube>("cube, base method", compressiveStrength, cube))
    return 1;
std::println("");
if (!show<Cylinder>("cylinder, base method", compressiveStrength, cylinder))
    return 1;

The cube's strength is 675 kN over 22500 mm², exactly 30 MPa. The cylinder's loaded area is pi times 22500 mm² over 4, about 17671.5 mm², and its strength 540 kN over that area, about 30.56 MPa, which the method's rule rounds to 30.6 MPa; the trace writes it as the exact fraction 153/5 MPa. The last two steps of each trace say whose rule rounded the value, (method default), and which variant ran, variant Cube (1st of 2), selected by tag.

An overlay changes a method for a jurisdiction

A jurisdiction's annex may change a method: fix a constant, replace a variant's formula, drop a variant, or round differently. An overlay lists those changes, each with the citation of the clause that makes it, and formula::apply(overlay, method) returns a new method with the changes made. The base method stays as it is.

The invented "Example jurisdiction" rounds the strength to whole megapascals. A rounding rule keeps a number of decimal places of a unit, so a coarser step such as 0.5 MPa cannot be stated as one; whole megapascals is the nearest coarser rule.

constexpr formula::DecimalRounding wholeMpa { unit::Megapascal,
                                              formula::DecimalPlaces { 0 },
                                              formula::RoundingMode::HalfAwayFromZero };

inline constexpr formula::Citation exampleRounding { .title = "Example jurisdiction",
                                                     .reference = "Example Standard 12:2020 NA",
                                                     .section = "NA.4" };

inline constexpr auto exampleJurisdiction = formula::overlay(formula::with_rounding<wholeMpa>(exampleRounding));

inline constexpr auto inExampleJurisdiction = formula::apply(exampleJurisdiction, compressiveStrength);

formula::with_rounding<rounding>(citation) replaces the method's rounding rule. Every overlay operation needs a citation; an operation without one does not compile. The overlaid method is run exactly like the base method:

if (!show<Cube>("cube, Example jurisdiction", inExampleJurisdiction, cube))
    return 1;
std::println("");
if (!show<Cylinder>("cylinder, Example jurisdiction", inExampleJurisdiction, cylinder))
    return 1;

The cube's strength is a whole number of megapascals, so it is 30 MPa under either rule. The cylinder's is not: about 30.56 MPa rounds to 31 MPa under the jurisdiction's rule, where the base method gives 30.6 MPa. The rounding step of each trace taken under the jurisdiction's rule reads rounded to 0 dp, and names the jurisdiction's overlay and its citation in place of (method default), so a reader of the trace can see which rule applied and where it comes from.

Output

cube, base method:
1. F = 675 kN
2. a = 150 mm
3. b = 150 mm
4. #2 * #3 = 9/400 m^2
5. #1 / #4 = 30000000 kg/(m s^2)
6. round(#5, in MPa) = 30 MPa [rounded to 1 dp (method default); nearest, ties away from zero]
7. #6 = 30 MPa [variant Cube (1st of 2), selected by tag]

cylinder, base method:
1. F = 540 kN
2. pi = 245850922/78256779
3. d = 150 mm
4. #3^2 = 9/400 m^2
5. #2 * #4 = 368776383/5217118600 m^2
6. 4
7. #5 / #6 = 368776383/20868474400 m^2
8. #1 / #7 = 3756325392000000/122925461 kg/(m s^2)
9. round(#8, in MPa) = 153/5 MPa [rounded to 1 dp (method default); nearest, ties away from zero]
10. #9 = 153/5 MPa [variant Cylinder (2nd of 2), selected by tag]

cube, Example jurisdiction:
1. F = 675 kN
2. a = 150 mm
3. b = 150 mm
4. #2 * #3 = 9/400 m^2
5. #1 / #4 = 30000000 kg/(m s^2)
6. round(#5, in MPa) = 30 MPa [rounded to 0 dp (jurisdiction overlay: Example jurisdiction, Example Standard 12:2020 NA, NA.4); nearest, ties away from zero]
7. #6 = 30 MPa [variant Cube (1st of 2), selected by tag]

cylinder, Example jurisdiction:
1. F = 540 kN
2. pi = 245850922/78256779
3. d = 150 mm
4. #3^2 = 9/400 m^2
5. #2 * #4 = 368776383/5217118600 m^2
6. 4
7. #5 / #6 = 368776383/20868474400 m^2
8. #1 / #7 = 3756325392000000/122925461 kg/(m s^2)
9. round(#8, in MPa) = 31 MPa [rounded to 0 dp (jurisdiction overlay: Example jurisdiction, Example Standard 12:2020 NA, NA.4); nearest, ties away from zero]
10. #9 = 31 MPa [variant Cylinder (2nd of 2), selected by tag]

Summary

  • formula::method(variants, rounding_rule, constraints) -- one method, its variants, the rule that rounds whichever ran, and its checks.
  • formula::variant<Tag>(expression) -- the formula for the specimens Tag names.
  • formula::explain_method<Tag>(method, environment) -- runs the variant for Tag, rounds it, and records the steps; the value is in the coherent SI unit.
  • formula::overlay(...) -- a jurisdiction's changes to a method, each with its citation.
  • formula::with_rounding<rounding>(citation) -- replaces the method's rounding rule.
  • formula::apply(overlay, method) -- the method with the overlay's changes made; the base method is unchanged.

Further reading