Hi DAML Forum! @jamesljaworski85 and I are extending the ISDA CDM model to generate CalculationPeriods that include resets and novel events. The current function does so based on CalculationPeriodDates and ResetDates:
generateResetPeriods : (Fetch f) => CalculationPeriodDates -> Optional ResetDates -> f [CalculationPeriod]
generateResetPeriods cpds None = generateCalculationPeriods cpds
generateResetPeriods cpds (Some (checkResetDates cpds -> rds)) = do
-- Roll out calculation periods and unadjusted reset dates
cps <- generateCalculationPeriods cpds
Our current pattern for doing this:
generateSchedulePeriods : (Fetch f) => CalculationPeriodDates -> Optional ResetDates -> Optional NovelDates -> f [CalculationPeriod]
generateSchedulePeriods cpds None None = generateCalculationPeriods cpds
generateSchedulePeriods cpds (Some (checkResetDates cpds -> rds)) (Some (checkNovelDates cpds -> nds)) = do
-- Roll out calculation periods and unadjusted reset dates
cps <- generateCalculationPeriods cpds
This function implies a pattern for handling None-None, Some-Some, None-Some and Some-None instances. In addition to the above function, we have two do blocks for None-Some and Some-None cases.
Is there a better pattern that handles resets and a second set of dates that correspond to novel event(s)?
The resets and novel events have the same number of parameters which load into the PaymentCalculationPeriods function:
I’m not entirely sure what you are after. If you have four do blocks, you can’t avoid writing some matching logic to switch between the four. I personally prefer case expressions to several function definitions:
t : Int -> Optional Int -> Optional Int -> Int
t i ij ik =
case (fmap (+1) ij, fmap (+1) ik) of
(Some j, Some k) -> i * j * k
(Some j, _) -> i * j
(_, Some k) -> i * k
_ -> i
In your particular case, that would presumably look something like
generatePaymentCalculationPeriods : (Fetch f) => CalculationPeriodDates -> Optional ResetDates -> Optional NovelDates -> Optional PaymentDates -> f [PaymentCalculationPeriod]
generatePaymentCalculationPeriods cpds ords onds opds =
case (fmap (checkResetDates cpds) ords, fmap (checkNovelDates cpds) onds, fmap (checkPaymentDates cpds) opds) of
(Some rds, Some nds, Some pds) -> do
...
(Some rds, Some nds, _) -> do
...
(Some rds, _, Some pds) -> do
...
(_, Some nds, Some pds) -> do
...
(Some rds, _, _) -> do
...
(_, Some nds, _) -> do
...
(_, _, Some pds) -> do
...
_ -> do
...
As Bernhard said, there’s no real way of getting around specifying the semantics for each independent case - if they are independent. However, if more than one of the cases overlap, there are several combinators that can make the code more concise.
For example, if you want to fail-fast with a None if any of your inputs is None, you could try something like
liftA2 (,) maybeLeft maybeRight
This returns an Optional (a, a), which you can then do a single pattern match against. You can achieve something similar for higher arities with <*>, also in the Applicative typeclass.
Haskell also has a handy Alternative typeclass which unfortunately we don’t include in the standard library. It has <|>, which given two Optionals, returns a single one, the first non-empty going left-to-right. So this is different from the above, as it won’t fail if you have at least one Some as input. You can google for it to see how it’s implemented.
Alternatively you can implement your own combinator function and then re-use it whenever you need! I do recommend googling around to see if you find your exact use-case covered in Haskell.
I think what I wanted to illustrate was the general approach of re-using functions to handle Optional behaviour, as an alternative to doing a pattern match.
In the case you describe it may not bring much, but for instance I do find the <|> combinator really useful in cases where you would otherwise have to repeat yourself.
We’re evaluating the ResetEvents and NovelEvents (both contained in the SchedulePeriod class, based on ResetDates class) within our three do blocks and then passing the output to the generatePaymentCalculationPeriods function.
The generatePaymentCalculationPeriods generates the PaymentCalculationPeriod from the CalculationPeriodDates (required) and ResetDates, NovelDates and PaymentDates, all of which are optional.
We’d like to:
(1) set floatingRateDefinition through resets, which is partially based on generating ResetDates class, which in turn is based on CalculationPeriodDates class, as shown above in generateResetPeriods function.
(2) set the novel events, which have a similar DAML model and behavior as resets. (This is why you see ResetDates and NovelDates in the generateSchedulePeriods.)
(3) validate our approach with your suggestion: we have three do blocks in generateSchedulePeriods with None case handled outside of do block:
generateSchedulePeriods : (Fetch f) => CalculationPeriodDates -> Optional ResetDates -> Optional NovelDates -> f [CalculationPeriod]
generateSchedulePeriods cpds None None = generateCalculationPeriods cpds --should have own do block?
generateSchedulePeriods cpds (Some (checkResetDates cpds -> rds)) (Some (checkNovelDates cpds -> nds)) = do
-- Roll out calculation periods, unadjusted reset dates and unadjusted novel dates
cps <- generateCalculationPeriods cpds
...
generateSchedulePeriods cpds (Some (checkResetDates cpds -> rds)) None = do
...
generateSchedulePeriods cpds None (Some (checkNovelDates cpds -> ads)) = do
...
Right now, we have the following error from the cps <- generateSchedulePeriods... from inside the do block. This is because all of the args’ expected type is Optional, although its currently being represented as required:
...daml/Org/Isda/Cdm/EventSpecificationModule/Impl/Contract/Payout/InterestRatePayout/Schedule/PaymentDates.daml:44:43: error:
Couldn't match expected type `Optional ResetDates'
with actual type `ResetDates'
In the second argument of `generateSchedulePeriods', namely `rds'
In a stmt of a 'do' block:
cps <- generateSchedulePeriods cpds rds nds
In the expression:
do cps <- generateSchedulePeriods cpds rds nds
let adj = optional noAdj (\ x -> ...) pds
let payRelativeTo
= optional
PayRelativeToEnumCalculationPeriodEndDate (\ x -> ...) pds
let offset = (\ x -> ...) =<< pds
....
Is it because the fmap expects a non-empty list? There will be cases where rds and nds are empty.
@Chris_Rivers I think the first line(s) of your error message are missing. I’m not seeing the issue just from the code snippets you posted.
EDIT to answer your question " Is it because the fmap expects a non-empty list? There will be cases where rds and nds are empty.":
fmap doesn’t expect a list at all. It expects a Functor, which is a container. If you are from the Java world, think of it as a collection. The construct (fmap (checkPaymentDates cpds) -> pds) is a view pattern. Since it comes in the fourth place in the definition of generatePaymentCalculationPeriods, it means
“take the fourth argument, apply the function fmap (checkPaymentDates cpds) to it, and put the result in pds”.
The fourth argument is of type Optional PaymentDates, and checkPaymentDates cpds is of type PaymentDates -> PaymentDates. The fmap just maps that over the Optional functor so that fmap (checkPaymentDates cpds) : Optional PaymentDates -> Optional PaymentDates.
Since pds isn’t used in the expression that is giving you the error, I don’t think this is what’s causing the issues.
I’ve updated my original post’s error above. The compiler has the same error for ResetDates, NovelDates and PaymentDates.
The generateSchedulePeriods follow this pattern with the addition of NovelEvents. The result of which is applied to the generatePaymentCalculationPeriods similar to this.
The error is telling you that you are trying to use the optional function on something that is not of type Optional a for some type a. Usually you either pattern match or use the optional function to avoid a pattern match. If you’ve already pattern matched on the Some case there is no need to use optional.
However, the floatingRateDefinition is still not set, when consolidating ResetDates and NovelDates into one class, SchedulePeriod, which includes the generateSchedulePeriods as shown above. (To quote Bernard, our current approach maybe overkill).
generatePaymentCalculationPeriods : (Fetch f) => CalculationPeriodDates -> Optional ResetDates -> Optional PaymentDates -> f [PaymentCalculationPeriod]
generatePaymentCalculationPeriods cpds rds (fmap (checkPaymentDates cpds) -> pds) = do
-- Roll out calculation periods
cps <- generateResetPeriods cpds rds
What would this look like? While the floatingRateDefinition sets using the generateResetPeriods, it won’t set when using the generateSchedulePeriods as above.