Skip to main content

Simplify formulas with the With function

Headshot of article author Greg Lindhorst

Welcome the With function to Canvas apps!  You can now simplify large formulas by dividing them into named sub-formulas, eliminate redundant sub-formulas, and easily work with functions that return records.  All while staying declarative and automatically responding to dependency changes.

That’s a mouthful, it’s easier to show you.  Let’s create a simple mortgage calculator to calculate monthly loan payments:

Today, the payment calculation in this app would be a bit complex:

(RateSlider/8/100/12)*(AmountSlider*10000) / (1-(1+(RateSlider/8/100/12))^(-(YearsSlider*12)))

Hmm, I wonder if that is even correct?  It’s hard to tell, there is a lot going in here.  Let’s start to unpack this with With.  The core of the calculation is the well known interest formula c = rP / (1 − (1+r)−n) where r is interest rate, P the loan amount, n the number of payments, and c the resulting payment amount.  Let’s use that formula verbatim with named values that pull apart the formula into small sub-formulas:

With( { r: RateSlider/8/100/12,     // interest rate
        P: AmountSlider*10000,      // loan amount
        n: YearsSlider*12 },        // number of payments
      r*P / (1 − (1+r)^-n) )        // standard interest calculation

Great, that’s a lot clearer.   We just created named values that we could plug in to our standard formula.  Besides making it simpler, we can reuse the value for r in two places, eliminating the redundant sub-formulas and the chance that the two get out of sync.  These names values are scoped to this With.

But those slider calculations are still a bit complicated.  It’s because, although you can set the min and max values on a slider control, the increment is always 1.  In the case of the Rate, we want fractional interest rates, so we divide the slider value by 8 to work with 1/8th of a percentage point.  In the case of the Amount, it is easier to position the slider if we snap to increments of 10,000 by multiplying by that factor.  Finally, the interest rate and number of payments need to be in monthly vs. yearly values, requiring the division and multiplication by 12 months respectively.  We can pull all these adjustements out into another level of With:

With( { AnnualRate: RateSlider/8/100,           // slider moves in 1/8th increments and convert to percentage
        Amount: AmountSlider*10000,             // slider moves by 10,000 increment
        Years: YearsSlider,                     // slider moves in single year increments, no adjustment required
        AnnualPayments: 12 },                   // number of payments per year
      With( { r: AnnualRate/AnnualPayments,     // interest rate
              P: Amount,                        // loan amount
              n: Years*AnnualPayments },        // number of payments
            r*P / (1 - (1+r)^-n)                // standard interest calculation
      )
)

Way clearer and easier to verify as correct.   As long as the names don’t collide, we have access to all of the values in both With records.

We have now seen three formulas that do exactly the same thing, no more and no less.  And that’s one of the advantages of With: it can be used anywhere, in any context, to break up a complex formula with no other impact.   It can be used in any declarative context as shown here, where the app is recalculating the formula automatically as the sliders change value.   There are no OnChange event handlers used in this app, there are no state variables to manage with Set or UpdateContext.  It is our hope that With will reduce the need for some state variables and thus reduce the complexity of apps.

The curly braces in the first argument are no accident.  We are actually defining a record, similar to how UpdateContext is used.   With is a function for creating your own record scopes, enjoyed by lots of our functions such as Filter and Sum.  This also means we can pass a record in as the first argument.

Imagine you are working with SQL Server and want to create a record in the Orders table and then add records to the OrderDetails table that have a foreign key to it.   When creating records, the Patch function will return the newly create record including the server generated primary key.  We need to hold on to this value just long enough to use it to Patch in the OrderDetails records (held in the NewOrderDetails collection).  With can also be used in behavior formulas to do imperative work:

With( Patch( Orders, Defaults( Orders ), { OrderStatus: "New" } ),
      ForAll( NewOrderDetails, 
              Patch( OrderDetails, Defaults( OrderDetails ), 
                     { Order: OrderID,          // from With's first argument, primary key of Patch result
                       Quantity: Quantity,      // from ForAll's NewOrderDetails table
                       ProductID: ProductID }   // from ForAll's NewOrderDetails table
              )
      )
)

We’d like to do something similar for Common Data Service but we don’t yet have a way to reference the records from the first arguments of With and ForAll as a whole.  We are actively working on it.

If you depend on capturing error results from Patch, there is currently a bug that prevents that from being properly sent through With.  The bug has already been fixed and is making its way through our deployment process and should be in your hands in a few weeks.

And we can now finally complete our time parsing journey.   We started last November with 39 lines and in January trimmed that to 6 lines.  We can now take it to down to 3.  The Match function returns a record of the named matches within a regular expression and With makes it easy to use those values with the Time function.

With( Match( "PT2H1M39S", "PT(?:(?<hours>\d+)H)?(?:(?<minutes>\d+)M)?(?:(?<seconds>\d+)S)?" ),
      Time( Value( hours ), Value( minutes ), Value( seconds ) )
)

With might look familiar if you know Visual Basic’s With … End With syntax, from which our function got its name.