Home > other >  Playground Crash: "error: Execution was interrupted, reason: signal SIGKILL." Seeking tech
Playground Crash: "error: Execution was interrupted, reason: signal SIGKILL." Seeking tech

Time:01-10

My project involves creating a table of data with a row for each of the 12 months of the year. The user of the app will define their monthly pay and percentage of pay they want to contribute to his/her 401k. The table shows in which month the user has reached the IRS contribution limit ($22,500) and consequently cannot contribute any more until next year.

I'm using Xcode playground to test my formulae. Since each month from February through December must look back at contributions and sums from the previous months, there is a considerable amount of looping which drives excessive execution time. I have a computed variable titled "personal401kContributionMonthly" which computes what the user has defined for their desired percent of monthly pay to contribute to their 401k. This computed property ran 4,940,531 times before the playground crashed with the following error: "error: Execution was interrupted, reason: signal SIGKILL. [12211:365921] Unable to quarantine process. (Error: -1.)"

I am seeking for advice/techniques to simplify the code to avoid the crash.

var monthlyPay = 15203.0
var personal401kLimit = 22500.0
var personal401kPercentage = 10.0
var roth401kPercentage = 10.0
var personal401kContributionMonthly: Double {
    monthlyPay * (personal401kPercentage/100)
}
personal401kContributionMonthly

var persTradContJan: Double {
    personal401kContributionMonthly <= personal401kLimit ? (personal401kContributionMonthly * (1 - (roth401kPercentage/100))) : personal401kLimit * (1 - (roth401kPercentage/100))
}
persTradContJan
var persRothContJan: Double {
    roth401kPercentage == 0.0 ? 0.0 : personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : personal401kLimit * (roth401kPercentage/100)
}
persRothContJan
var pers401ksumFeb: Double {
    persTradContJan   persRothContJan
}
var persTradContFeb: Double {
    pers401ksumFeb   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumFeb < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumFeb)) : 0.0
}
persTradContFeb
var persRothContFeb: Double {
    roth401kPercentage == 0.0 ? 0.0 : pers401ksumFeb   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumFeb < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumFeb) : 0.0
}
persRothContFeb
var pers401ksumMar: Double {
    persTradContJan   persRothContJan   persTradContFeb   persRothContFeb
}
var persTradContMar: Double {
    pers401ksumMar   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumMar < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumMar)) : 0.0
}
persTradContMar
var persRothContMar: Double {
    roth401kPercentage == 0.0 ? 0.0 : pers401ksumMar   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumMar < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumMar) : 0.0
}
persRothContMar
var pers401ksumApr: Double {
    persTradContJan   persRothContJan   persTradContFeb   persRothContFeb   persTradContMar   persRothContMar
}
var persTradContApr: Double {
    pers401ksumApr   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumApr < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumApr)) : 0.0
}
persTradContApr
var persRothContApr: Double {
    roth401kPercentage == 0.0 ? 0.0 : pers401ksumApr   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumApr < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumApr) : 0.0
}
persRothContApr
var pers401ksumMay: Double {
    persTradContJan   persRothContJan   persTradContFeb   persRothContFeb   persTradContMar   persRothContMar   persTradContApr   persRothContApr
}
var persTradContMay: Double {
    pers401ksumMay   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumMay < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumMay)) : 0.0
}
persTradContMay
var persRothContMay: Double {
    roth401kPercentage == 0.0 ? 0.0 : pers401ksumMay   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumMay < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumMay) : 0.0
}
persRothContMay
var pers401ksumJun: Double {
    persTradContJan   persRothContJan   persTradContFeb   persRothContFeb   persTradContMar   persRothContMar   persTradContApr   persRothContApr   persTradContMay   persRothContMay
}
var persTradContJun: Double {
    pers401ksumJun   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumJun < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumJun)) : 0.0
}
persTradContJun
var persRothContJun: Double {
    roth401kPercentage == 0.0 ? 0.0 : pers401ksumJun   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumJun < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumJun) : 0.0
}
persRothContJun
var pers401ksumJul: Double {
    persTradContJan   persRothContJan   persTradContFeb   persRothContFeb   persTradContMar   persRothContMar   persTradContApr   persRothContApr   persTradContMay   persRothContMay   persTradContJun   persRothContJun
}
var persTradContJul: Double {
    pers401ksumJul   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumJul < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumJul)) : 0.0
}
persTradContJul
var persRothContJul: Double {
    roth401kPercentage == 0.0 ? 0.0 : pers401ksumJul   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumJul < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumJul) : 0.0
}
persRothContJul
var pers401ksumAug: Double {
    persTradContJan   persRothContJan   persTradContFeb   persRothContFeb   persTradContMar   persRothContMar   persTradContApr   persRothContApr   persTradContMay   persRothContMay   persTradContJun   persRothContJun   persTradContJul   persRothContJul
}
var persTradContAug: Double {
    pers401ksumAug   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumAug < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumAug)) : 0.0
}
persTradContAug
var persRothContAug: Double {
    roth401kPercentage == 0.0 ? 0.0 : pers401ksumAug   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumAug < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumAug) : 0.0
}
persRothContAug
var pers401ksumSep: Double {
    persTradContJan   persRothContJan   persTradContFeb   persRothContFeb   persTradContMar   persRothContMar   persTradContApr   persRothContApr   persTradContMay   persRothContMay   persTradContJun   persRothContJun   persTradContJul   persRothContJul   persTradContAug   persRothContAug
}
var persTradContSep: Double {
    pers401ksumSep   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumSep < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumSep)) : 0.0
}
persTradContSep
var persRothContSep: Double {
    roth401kPercentage == 0.0 ? 0.0 : pers401ksumSep   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumSep < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumSep) : 0.0
}
persRothContSep
var pers401ksumOct: Double {
    persTradContJan   persRothContJan   persTradContFeb   persRothContFeb   persTradContMar   persRothContMar   persTradContApr   persRothContApr   persTradContMay   persRothContMay   persTradContJun   persRothContJun   persTradContJul   persRothContJul   persTradContAug   persRothContAug   persTradContSep   persRothContSep
}
var persTradContOct: Double {
    pers401ksumOct   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumOct < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumOct)) : 0.0
}
persTradContOct
var persRothContOct: Double {
    roth401kPercentage == 0.0 ? 0.0 : pers401ksumOct   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumOct < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumOct) : 0.0
}
persRothContOct
var pers401ksumNov: Double {
    persTradContJan   persRothContJan   persTradContFeb   persRothContFeb   persTradContMar   persRothContMar   persTradContApr   persRothContApr   persTradContMay   persRothContMay   persTradContJun   persRothContJun   persTradContJul   persRothContJul   persTradContAug   persRothContAug   persTradContSep   persRothContSep   persTradContOct   persRothContOct
}
var persTradContNov: Double {
    pers401ksumNov   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumNov < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumNov)) : 0.0
}
persTradContNov
var persRothContNov: Double {
    roth401kPercentage == 0.0 ? 0.0 : pers401ksumNov   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumNov < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumNov) : 0.0
}
persRothContNov
var pers401ksumDec: Double {
    persTradContJan   persRothContJan   persTradContFeb   persRothContFeb   persTradContMar   persRothContMar   persTradContApr   persRothContApr   persTradContMay   persRothContMay   persTradContJun   persRothContJun   persTradContJul   persRothContJul   persTradContAug   persRothContAug   persTradContSep   persRothContSep   persTradContOct   persRothContOct   persTradContNov   persRothContNov
}
var persTradContDec: Double {
    pers401ksumDec   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (1 - (roth401kPercentage/100)) : pers401ksumDec < personal401kLimit ? (1 - (roth401kPercentage/100)) * (personal401kLimit - (pers401ksumDec)) : 0.0
}
persTradContDec
var persRothContDec: Double {
    roth401kPercentage == 0.0 ? 0.0 : pers401ksumDec   personal401kContributionMonthly <= personal401kLimit ? personal401kContributionMonthly * (roth401kPercentage/100) : pers401ksumDec < personal401kLimit ? (roth401kPercentage/100) * (personal401kLimit - pers401ksumDec) : 0.0
}
persRothContDec

CodePudding user response:

Your playground crashes because all those computed variables reference other computed variables in ways that explode the call tree exponentially, so as @matt, with tongue in cheek, pointed out in comments, it's a literal stack overflow. Computed variables are just syntactic sugar for function calls. Were you intending lazy assignment instead?

In any case, you asked about refactoring the code.

I think your example is the kind of thing that wants to be in a data structure where you index by the month rather than having individually named computed variables. As is often the case, it can be accomplished multiple ways. What follows is one way. I'll break into part.

I'm going to assume that your playground is intended as a sort of prototyping environment for some code that you want to use in a real app. If it's just a learning exercise, using Double to represent money is fine, but binary floating point types can't exactly represent all decimal numbers. See here for examples and why this is the case. For this reason it's better to use Decimal, so that's what I'll do.

Unfortunately Decimal is slow compared to Double or Float, and doesn't have the most fully supported API in Swift. Actually its Objective-C counterpart, NSDecimalNumber is pretty crufty in Objective-C too. Still, it is the best standardly available type to do financial computations.

Next I don't see any explicit rounding in your code example, and as far as I know fractional penny contributions to 401ks (or IRAs) are not a thing, so you need a way to round to whole pennies. It's probably the most awkward code I'm going to present, so let's get it out of the way first.

There are two ways to do rounding on Decimal, and unfortunately neither of them are especially convenient. Unlike Double, Decimal does not provide .rounded() method.

One way is to call the NSDecimalRound free function, which requires getting a pointer to the value to round. I'd probably use that way in my own code, but let's avoid pointers.

The second way is to make use of the fact that Decimal bridges to NSDecimalNumber which provides a rounding(accordingToBehavior:) method. That requires creating a NSDecimalNumberBehaviors instance that configures how the rounding will be done... Did I mention the API is crufty? We have to cast to NSDecimalNumber then call rounding(accordingToBehavior:) and then cast back to Decimal. It looks like this:

import Foundation

let pennyRoundingBehavior = NSDecimalNumberHandler(
    roundingMode: .bankers,
    scale: 2,
    raiseOnExactness: false,
    raiseOnOverflow: true,
    raiseOnUnderflow: true,
    raiseOnDivideByZero: true
)

/**
 Computes `dollarAmount * percentage`, rounded to the nearest penny.
 
 Uses "Banker's" rounding: If truncated digits represent a value greater than
 half a penny, the result is rounded up.  If it's less than half a penny, the
 result is rounded down.  If it's exactly half a penny, the value is rounded to
 the nearest even penny.  So `10.015 is rounded to `10.02`, while `10.025` is
 also rounded to `10.02`.
 
 - Parameters:
    - percentage: `Decimal` value in the range 0...100
    - amount: `Decimal` to which `percentage` is applied.
 */
func roundToNearestPenny(percentage: Decimal, of dollarAmount: Decimal) -> Decimal
{
    assert((0...100).contains(percentage))
    
    let x = ((dollarAmount * percentage / 100) as NSDecimalNumber)
    return x.rounding(accordingToBehavior: pennyRoundingBehavior) as Decimal
}

Fortunately, all the other math we need to do with Decimal can be done just like for Double.

Since you want contributions per month, I define an enum to represent months:

enum ContributionMonth: Int, CaseIterable
{
    case January, February, March, April, May, June,
         July, August, September, October, November, December
    
    static var count:Int  { allCases.count }
}

I represent the contributions for a given month as a struct

struct MonthlyContributions
{
    var roth        : Decimal = 0
    var traditional : Decimal = 0
}

extension MonthlyContributions
{
    var total: Decimal { roth   traditional }
    
    init(combinedAmount: Decimal, rothPercentage: Decimal)
    {
        let roth = roundToNearestPenny(
            percentage: rothPercentage,
            of: combinedAmount
        )
        self.init(roth: roth, traditional: combinedAmount - roth)
    }
}

Note the init in the extension computes the contribution distribution, which will simplify later code.

Since I'll be storing instances of MonthlyContributions in an Array with elements that correspond to the months starting with January it would be nice to just index that array by the month. To do that I extend Array when it holds MonthlyContributions as elements:

extension Array where Element == MonthlyContributions {
    subscript (index: ContributionMonth) -> Element { self[index.rawValue] }
}

An alternative would be to use a Dictionary instead of an Array. For this example, I think Array is slightly simpler, but it doesn't really matter much. With Dictionary you wouldn't need the extension, but adding a value requires using both a key and a value, whereas with Array I can just append. Dictionary would be the way to go if you don't want to populate it with contributions for all months.

So next up, for a given month, we need to adjust combined contribution so that total annual contributions will be capped to the legal limit:

func calculateCombinedContribution(
    monthlyContribution        monthly    : Decimal,
    annualContributionLimit    limit      : Decimal,
    contributionsSoFarThisYear cummulative: Decimal) -> Decimal
{
    min(max(0, limit - cummulative), monthly)
}

Now all of that is used to compute a whole years worth of monthly contributions:

func computeMonthlyContributions(
    monthlyPay            : Decimal,
    contributionPercentage: Decimal,
    rothPercentage        : Decimal,
    contributionLimit     : Decimal) -> [MonthlyContributions]
{
    assert((0...100).contains(contributionPercentage))
    assert((0...100).contains(rothPercentage))
    assert(monthlyPay >= 0)
    assert(contributionLimit >= 0)
    
    var totalContributions  : Decimal = 0
    var monthlyContributions: [MonthlyContributions] = []
    monthlyContributions.reserveCapacity(ContributionMonth.count)

    let monthlyContribution = roundToNearestPenny(
        percentage: contributionPercentage,
        of: monthlyPay
    )
    
    for _ in ContributionMonth.allCases
    {
        let combinedAmount = calculateCombinedContribution(
            monthlyContribution       : monthlyContribution,
            annualContributionLimit   : contributionLimit,
            contributionsSoFarThisYear: totalContributions
        )
        
        let contribution = MonthlyContributions(
            combinedAmount: combinedAmount,
            rothPercentage: rothPercentage
        )

        monthlyContributions.append(contribution)
        totalContributions  = contribution.total
    }
    
    return monthlyContributions
}

The assertions are just there to validate basic numeric assumptions, at least in DEBUG builds. For each month we determine how much the combined contribution will be for that month, which takes into account whether the total contributions have reached the legal annual limit, then create a MonthlyContributions instance, which distributes the amount internally between roth and traditional, append it to the array, and update the total contributions.

Now we can call it with some values, and print out the calculated monthly contributions:

var monthlyPay            : Decimal = 15203.0
var personal401kLimit     : Decimal = 22500.0
var personal401kPercentage: Decimal = 10.0
var roth401kPercentage    : Decimal = 10.0

let monthlyContributions = computeMonthlyContributions(
    monthlyPay            : monthlyPay,
    contributionPercentage: personal401kPercentage,
    rothPercentage        : roth401kPercentage,
    contributionLimit     : personal401kLimit
)

for month in ContributionMonth.allCases
{
    let curContribution = monthlyContributions[month]
    print("Contribution for \(month)")
    print("    Roth       : $\(curContribution.roth)")
    print("    Traditional: $\(curContribution.traditional)")
    print()

}

This just uses the values you provide. The output is:

Contribution for January
    Roth       : $152.03
    Traditional: $1368.27

Contribution for February
    Roth       : $152.03
    Traditional: $1368.27

Contribution for March
    Roth       : $152.03
    Traditional: $1368.27

Contribution for April
    Roth       : $152.03
    Traditional: $1368.27

Contribution for May
    Roth       : $152.03
    Traditional: $1368.27

Contribution for June
    Roth       : $152.03
    Traditional: $1368.27

Contribution for July
    Roth       : $152.03
    Traditional: $1368.27

Contribution for August
    Roth       : $152.03
    Traditional: $1368.27

Contribution for September
    Roth       : $152.03
    Traditional: $1368.27

Contribution for October
    Roth       : $152.03
    Traditional: $1368.27

Contribution for November
    Roth       : $152.03
    Traditional: $1368.27

Contribution for December
    Roth       : $152.03
    Traditional: $1368.27

As you can see the annual contribution limit isn't triggered by that monthlyPay value, so increasing it to 30000.0 does trigger it:

Contribution for January
    Roth       : $300
    Traditional: $2700

Contribution for February
    Roth       : $300
    Traditional: $2700

Contribution for March
    Roth       : $300
    Traditional: $2700

Contribution for April
    Roth       : $300
    Traditional: $2700

Contribution for May
    Roth       : $300
    Traditional: $2700

Contribution for June
    Roth       : $300
    Traditional: $2700

Contribution for July
    Roth       : $300
    Traditional: $2700

Contribution for August
    Roth       : $150
    Traditional: $1350

Contribution for September
    Roth       : $0
    Traditional: $0

Contribution for October
    Roth       : $0
    Traditional: $0

Contribution for November
    Roth       : $0
    Traditional: $0

Contribution for December
    Roth       : $0
    Traditional: $0
  • Related