Frege and QuickCheck: Combine properties

Sometimes our function should follow more than one property at the same time. How can I check more than one property in a given specification?

Combining properties

In the following example we’re pretending to be a small financial institution that lends money.

So we have a Loan:

Loan
data Risk = PENDING | NORMAL | RISKY | REJECTED

derive Eq   Risk
derive Show Risk
derive Enum Risk

data Loan = Loan { name :: Maybe String , amount :: Double, risk :: Risk }

derive Show Loan
This is an example, please don’t use this data as reference for any financial development :P

We need to build a function to process an incoming loan. At this point we only know which are the properties to reject a loan. It will be rejected:

  • When it has a negative amount

  • When it goes outside the company defined boundaries (min: 0, max: 100000)

  • When it has no name

Lets define these properties:

Min property
rejectNegative :: Loan -> Bool
rejectNegative loan = if isNegative
                      then statusResult == Risk.REJECTED
                      else true
  where isNegative   = loan.amount < 0
        loanResult   = calculateRisk loan
        statusResult = loanResult.risk
Max property
rejectBeyondMax :: Loan -> Bool
rejectBeyondMax loan = if isBeyondMax
                       then statusResult == Risk.REJECTED
                       else true
  where isBeyondMax  = loan.amount > 100_000
        loanResult   = calculateRisk loan
        statusResult = loanResult.risk
Name property
rejectAnonymous :: Loan -> Bool
rejectAnonymous loan = if isAnonymous
                       then statusResult == Risk.REJECTED
                       else true
  where isAnonymous  = loan.name == Nothing
        loanResult   = calculateRisk loan
        statusResult = loanResult.risk

Lets see how we’ve defined our function with these requirements in mind:

Implementation
calculateAmountRisk :: Loan -> Loan (1)
calculateAmountRisk loan
  | inRange (1,9999) loan.amount         = loan.{ risk = NORMAL }
  | inRange (10_000,100_000) loan.amount = loan.{ risk = RISKY }
  | otherwise                            = loan.{ risk = REJECTED }

calculateAnonymousRisk :: Loan -> Loan (2)
calculateAnonymousRisk loan = case loan of
  Loan Nothing _ _ -> loan.{ risk = REJECTED }
  _                -> loan

calculateRisk :: Loan -> Loan (3)
calculateRisk loan = (calculateAnonymousRisk . calculateAmountRisk) loan
1 Function to calculate risk based on the loan ammount
2 Function to calculate risk based on the loan name
3 Function combining previous two functions
Please notice I’m not mutating the data structure when doing loan.{ risk = Risk.REJECTED}. In this case, changing a field means copying the data structure and setting the new value in the new copy.

Ok, lets see if the function holds for these properties:

Testing properties
checkName = property rejectAnonymous
checkMin  = property rejectNegative
checkMax  = property rejectBeyondMax
quickCheck result
qc.CombineCheck.checkName: +++ OK, passed 100 tests
qc.CombineCheck.checkMin: +++ OK, passed 100 tests
qc.CombineCheck.checkMax: +++ OK, passed 100 tests

So far we have tested our properties isolated, but in the real world I would make sure that a given loan passes those properties all at once. How do I do that ?

Well there’s a function called conjoin which takes care of it:

Check several properties at once
calculateRiskCheck = conjoin [rejectNegative, rejectBeyondMax, rejectAnonymous]
Name property check
qc.CombineCheck.calculateRiskCheck: +++ OK, passed 100 tests

Now I’m sure the loan passes if all properties over a loan pass the test.

You can also use the conjuntion infix function .&&.:

Check several properties at once
calculateRiskCheck2 = rejectNegative .&&.rejectBeyondMax .&&. rejectAnonymous