Binding a variable with a sequence of expressions

Hi Community! I’m trying to determine the best way to bind a sequence of Update expressions to the cps:[CalculationPeriod]:

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

  -- Define payment calculation period
  ...

  mapA (generateSinglePeriod payRelativeTo offset adj . replicate 1) cps

  where

    ...

    generateSinglePeriod payRelativeTo offset adj cps = do
      let baseDate = case payRelativeTo of
                      PayRelativeToEnumCalculationPeriodStartDate ->
                        (get "adjustedStartDate" . (\cp -> cp.adjustedStartDate) . head) cps
                      PayRelativeToEnumCalculationPeriodEndDate ->
                        (get "adjustedEndDate" . (\cp -> cp.adjustedEndDate) . last) $ cps
                      otherwise -> throwNotSupportedError (show payRelativeTo)

      ...

      return PaymentCalculationPeriod
                { id = None
                , adjustedPaymentDate = Some aPayDate
                , calculationPeriod = cps
                , discountFactor = None
                , fixedPaymentAmount = None
                , forecastPaymentAmount = None
                , presentValueAmount = None
                , unadjustedPaymentDate = Some uPayDate
                }

In the above function, cps generates Optional ResetDates to set the value for floatingRateDefinition.

My function below includes Optional OtherDates to also set otherValueDefinition:

generatePaymentCalculationPeriods : (Fetch f) => CalculationPeriodDates -> Optional ResetDates -> Optional OtherDates -> Optional PaymentDates -> f [PaymentCalculationPeriod]
generatePaymentCalculationPeriods cpds rds ods (fmap (checkPaymentDates cpds) -> pds) = do
  -- Roll out calculation periods

  cps <-
    case (rds, ods) of
      (Some r, None) -> generateResetPeriods cpds rds
      (None, Some o) -> generateOtherPeriods cpds ods
      (Some r, Some o) -> do
        let r = generateResetPeriods cpds rds
        let o = generateOtherPeriods cpds ods
        -- sequence expression to return both r and o
        return ...
      (None, None) -> error "expecting calculation period"  

The cps <- generate{Reset/Other}Periods functions have the same type signature, and their Update expressions are independent of each other, such that their respective floatingRateDefinition and otherValueDefinition will set.

1 Like

Hi @Chris_Rivers, maybe you are looking for something like the following:

generatePaymentCalculationPeriods : (Fetch f) => CalculationPeriodDates -> Optional ResetDates -> Optional OtherDates -> Optional PaymentDates -> f [PaymentCalculationPeriod]
generatePaymentCalculationPeriods cpds rds ods (fmap (checkPaymentDates cpds) -> pds) = do
  -- Roll out calculation periods

  cps <-
    case (rds, ods) of
      (Some r, None) -> generateResetPeriods cpds rds
      (None, Some o) -> generateOtherPeriods cpds ods
      (Some r, Some o) -> do
        r <- generateResetPeriods cpds rds
        o <- generateOtherPeriods cpds ods
        -- sequence expression to return both r and o
        return (r ++ o)
      (None, None) -> error "expecting calculation period"

The crucial part is that you first sequence the Updates with <- and then combine the results using ++ (assuming you are working with lists here).

You can also abstract over some of the repetition here to make it easier to extend with more optional values. Don’t take this as the best solution. What to abstract over and whether it’s worth abstracting over it at all heavily depends on your application:

handleOptional : Optional a -> (a -> f [b]) -> f [b]
handleOptional None _ = return []
handleOptional (Some x) f = f x

generatePaymentCalculationPeriods : (Fetch f) => CalculationPeriodDates -> Optional ResetDates -> Optional OtherDates -> Optional PaymentDates -> f [PaymentCalculationPeriod]
generatePaymentCalculationPeriods cpds rds ods (fmap (checkPaymentDates cpds) -> pds) = do
  -- Roll out calculation periods

  when (all isNone [rds, ods]) $
    abort "Expecting calculation period"

  results <- sequenceA 
    [ handleOptional rds (generateResetPeriods cpds)
    , handleOptional ods (generateOtherPeriods ods)
    ]
  let cps = concat results
  …
3 Likes

Your recommendation is helpful. On the abstraction comment, I had in mind of combining arrays based on a predicate for both rds and ods, albeit with more verbose language, but your suggestion is better.

After the sequence the Updates with <-, only the last Update takes effect, however. I see this because the Scenario sets the otherValueDefinition from o <- generate..., however, the error expects the floatingRateDefinition to set from the r <- generate....

Does this have something to do with this from above?: mapA (generateSinglePeriod payRelativeTo offset adj . replicate 1) cps

1 Like

Could you show us the code you’re using now or at least the relevant parts? If you call sequenceA on a list of Updates, all of them will be applied not just the last one.

1 Like

This class generates payment details:

generatePaymentCalculationPeriods : (Fetch f) => CalculationPeriodDates -> Optional ResetDates -> Optional OtherDates -> Optional PaymentDates -> f [PaymentCalculationPeriod]
generatePaymentCalculationPeriods cpds rds ods (fmap (checkPaymentDates cpds) -> pds) = do
  -- Roll out calculation periods
  cps <-
    case (rds, ods) of
      (Some r, None) -> generateResetPeriods cpds rds
      (None, Some o) -> generateOtherPeriods cpds ads
      (Some r, Some o) -> do
        r <- generateResetPeriods cpds rds
        o <- generateOtherPeriods cpds ods
        return (r ++ o)
      (None, None) -> error "expecting calculation period"  

  -- Define payment calculation period
  let adj = optional noAdj (\x -> get "paymentDatesAdjustments" x.paymentDatesAdjustments) pds
  let payRelativeTo = optional PayRelativeToEnumCalculationPeriodEndDate (\x -> get "payRelativeTo" x.payRelativeTo) pds
  let offset = (\x -> x.paymentDaysOffset) =<< pds
  mapA (generateSinglePeriod payRelativeTo offset adj . replicate 1) cps

  where
    noAdj = BusinessDayAdjustments
              { id = None
              , businessCenters = None
              , businessDayConvention = BusinessDayConventionEnumNONE
              }

    generateSinglePeriod :
      (Fetch f)
      => PayRelativeToEnum
      -> Optional Offset
      -> BusinessDayAdjustments
      -> [CalculationPeriod]
      -> f PaymentCalculationPeriod
    generateSinglePeriod _ _ _ [] = error "expecting at least one calculation period"
    generateSinglePeriod payRelativeTo offset adj cps = do
      let baseDate = case payRelativeTo of
                      PayRelativeToEnumCalculationPeriodStartDate ->
                        (get "adjustedStartDate" . (\cp -> cp.adjustedStartDate) . head) cps
                      PayRelativeToEnumCalculationPeriodEndDate ->
                        (get "adjustedEndDate" . (\cp -> cp.adjustedEndDate) . last) $ cps
                      otherwise -> throwNotSupportedError (show payRelativeTo)

      uPayDate <- optional (pure baseDate) (\o -> applyOffset o adj.businessCenters baseDate) offset
      aPayDate <- adjustDate adj uPayDate
      return PaymentCalculationPeriod
                { id = None
                , adjustedPaymentDate = Some aPayDate
                , calculationPeriod = cps
                , discountFactor = None
                , fixedPaymentAmount = None
                , forecastPaymentAmount = None
                , presentValueAmount = None
                , unadjustedPaymentDate = Some uPayDate
                }

The PaymentCalculationPeriod details for the floatingRateDefinition:

populatePCP : [(Optional Text, ResetPrimitive)] -> [(Optional Text, EventPrimitive)] -> InterestRatePayout -> PaymentCalculationPeriod -> (PaymentCalculationPeriod, Lineage)
populatePCP existingResets existingEvents irp pcp =
  let (resetReferences, eventReferences, cpsFull) = unzip3 $ map (setInterestRate . setDayCountFraction . setQuantity) pcp.calculationPeriod
      lineage = emptyLineage
                  { interestRatePayoutReference = [referenceWithEmptyMeta irp.primeKey]
                  , eventReference = map referenceWithEmptyMeta $ concat resetReferences
                  , eventReference = map referenceWithEmptyMeta $ concat eventReferences
                  }
      pcpFull = pcp { calculationPeriod = cpsFull }
  in (pcpFull, lineage)
  where
    ...

    setInterestRate : CalculationPeriod -> ([Text], [Text], CalculationPeriod)
    setInterestRate cp
      ...    

      | Some floatingRate <- irp.rateSpecification.floatingRate
      , Some otherValue <- irp.rateSpecification.otherValue
      = let frDef = get "floatingRateDefinition" cp.floatingRateDefinition
            frdNew = FR.calcFloatingRate existingResets floatingRate frDef
            ovDef = get "otherValueDefinition" cp.otherValueDefinition
            ovdNew = OV.calcOtherValue existingEvents otherValue ovDef
        in (fst frdNew, fst ovdNew, cp { floatingRateDefinition = Some (snd frdNew)
                                       ; otherValueDefinition = Some (snd ovdNew) })

      | otherwise = error "expecting exactly two 'rateSpecification'"

So which of those updates is not getting applied?

1 Like

I’ve made an update to the code example, the otherValue sets, however, the floatingRate from the setInterestRate does not:

Scenario execution failed on commit at Test.Event:118:28:

Aborted: expecting 'floatingRateDefinition' to be set

Stack trace:

- get (Org.Isda.Cdm.EventSpecificationModule.Impl.Utils:39:18)
- get (Org.Isda.Cdm.EventSpecificationModule.Impl.Utils:38:1)
- populatePCP (Org.Isda.Cdm.EventSpecificationModule.Impl.Contract.Payout.InterestRatePayout:166:13)
- populatePCP (Org.Isda.Cdm.EventSpecificationModule.Impl.Contract.Payout.InterestRatePayout:166:9)
- populatePCP (Org.Isda.Cdm.EventSpecificationModule.Impl.Contract.Payout.InterestRatePayout:131:72)
- $u002e (GHC.Base:53:11)
- map (GHC.Base:41:25)
- map (GHC.Base:41:15)
- populatePCP (Org.Isda.Cdm.EventSpecificationModule.Impl.Contract.Payout.InterestRatePayout:131:67)
- populatePCP (Org.Isda.Cdm.EventSpecificationModule.Impl.Contract.Payout.InterestRatePayout:131:58)
- populatePCP (Org.Isda.Cdm.EventSpecificationModule.Impl.Contract.Payout.InterestRatePayout:130:1)
- map (GHC.Base:41:25)
- map (GHC.Base:41:15)
- buildEvents (Org.Isda.Cdm.EventSpecificationModule.Impl.Contract.Payout.InterestRatePayout:61:7)
- $$c$u003e$u003e$u003d (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:49:34)
- $$c$u003e$u003e$u003d (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:48:23)
- $$cfmap (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:37:47)
- $$cfmap (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:37:24)
- $$c$u003c$u002a$u003e (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:42:47)
- $$c$u003c$u002a$u003e (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:42:23)
- $$cfmap (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:37:47)
- $$cfmap (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:37:24)
- $$c$u003e$u003e$u003d (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:48:46)
- $$c$u003e$u003e$u003d (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:48:23)
- $$cfmap (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:37:47)
- $$cfmap (Org.Isda.Cdm.EventSpecificationModule.Types.ReferenceData.Fetch:37:24)
- buildDerivedEvents (Org.Isda.Cdm.EventSpecificationModule.EventBuilder.Derived:37:41)
- buildDerivedEvents (Org.Isda.Cdm.EventSpecificationModule.EventBuilder.Derived:37:1)
- flip (DA.Internal.Prelude:369:1)
- $$cfmap (DA.Internal.LF:129:33)
- $$cfmap (DA.Internal.LF:129:22)
1 Like

Could you explain how things fit together in your example? The error expecting 'floatingRateDefinition' to be set doesn’t seem to be thrown by the code in your example which might hint at the issue being somewhere else. It also looks like you are not calling your populatePCP function.

1 Like

The error seems to be thrown by this line. get is basically fromSomeNote.

In the code snippet you provide, it’s unclear to me how cp gets set. I imagine it comes directly from pcp.calculationPeriod, so the issue seems to be outside the function you are sharing (i.e. pcp is “wrong” to start with) or in the setDayCountFraction . setQuantity function, which may not be setting the expected field.

2 Likes

@cocreature and @Gary_Verhaegen the floatingRateDefinition (and, our novel, otherValueDefinition) are a result of the DerivedEventsWorkflow, which obtains all future events, in the form of a list, DerivedEvent(s). As a consequence of the DerivedEvent, the CreateDerivedEvent choice results in an EventInstance. Taking the EventInstance, the Lifecycling can be triggered by consuming a list of ContractInstance(s), the result of which is a new (or modified) ContractInstances and an EventNotification. Since this event requires a cash transfer, the payer will first instruct then allocate cash in order to Lifecycle the event.

Within the DerivedEventsWorkflow, novel, reset and cash events correspond to NovelDates, ResetDates and PaymentDates, which are then used to get and set the value of the floatingRateDefinition and (novel) otherValueDefinition.

We’ve had success with either using the Update expression for cps <- generateResetPeriods to set the floatingRateDefinition or cps <- generateOtherPeriods to set the otherValueDefinition, but not both. Essentially, when Update expressions are executed in a sequence, only the last expression sets its respective value which, in this instance, is the otherValueDefinition.

I think I see where my confusion was coming from:
generateResetPeriods has the following type

generateResetPeriods
   : (Fetch f)
   => CalculationPeriodDates
   -> Optional ResetDates
   -> f [CalculationPeriod]

Note that there is no Update involved and Fetch does not have an instance for Update afaict.
I assume that generateOtherPeriods which you’ve added yourself has a similar type with only the second argument being different.

generateResetPeriods generates a list of CalculationPeriods and sets the floatingRangeDefinition field in each of them. I assume generateOtherPeriods sets otherValueDefinition which you’ve added as a field to CalculationPeriod.

My suggestion calls both idependently and then concatenates the results using ++. So you get back a list that first contains CalculationPeriods with floatingRangeDefinition set and then CalculationPeriods with otherValueDefinitionset but not both.

If you want to combine them in a way where both fields are set you need to pull apart the definition of generateResetPeriods. For the case where the argument is Some, generateResetPeriods does two things:

  1. First it generates the list of periods using generateCalculationPeriods.
  2. Afterwards it modifies the results using a combination of generateResetPeriodDates and setResetDates. This is effectively a map over the result of generateCalculationPeriods so it does not change the number of CalculationPeriods.

If you split out step 2 into a function (I’ll let you choose a better name :slightly_smiling_face: )

f : Fetch f => CalculationPeriod -> f [CalculationPeriod]

and do the same for your implementation of generateOtherPeriods (I assume it also first calls generateCalculationPeriods) and call the step 2 there g you can express the combined effects using

combinedPeriods cpds = do
  cps <- generateCalculationPeriods cpds
  map (g . f) cpds
1 Like

Abstracting a bit away from your specific case, if you want to combine multiple updates, you have essentially two options: either the updates are independent, or the updates depend on each other. If they are independent, your code needs to look something like:

do
  result1 <- updateFn1 some args
  result2 <- updateFn2 other params
  -- at this point you have both results as "pure" values with
  -- no update anymore
  let combined = combineFunction result1 result2
  -- now you can actually use the combined result

the following, however, would not work and would result in the issue you are describing:

do
  result <- updateFn1 some args
  result <- updateFn2 other params
  -- trying to use result here will look like only the second
  -- effect was applied; the result of the first one is lost
  -- because the result variable has been re-bound, shadowing
  -- the result of updateFn1.

If the updates do depend on each other somehow, then you need to thread the result through:

do
  result1 <- updateFn1 some args
  result2 <- updateFn2 result1 other params -- note result1 is passed in
  -- use result2 here, as it already represents a combined result
1 Like

Am I?: (1) renaming the Some case of generateResetPeriods to f or (2) adding a new function, f, which includes generateResetPeriodDates and setResetDates

1 Like

Where does this gets added? Is this a nested function of f or a renamed Some case from the do block?

1 Like

Thanks for this walk-through, Gary–very informative! In our application, we expect the floatingRateDefinition and otherValueDefinition to be set independently and are not dependent upon each other.

We’re working on @cocreature’s suggestion at the moment. Cheers!

1 Like

Bear in mind that I know nothing about the domain nor the existing code in ex-cdm-swaps, so this may be completely off-base and should be taken with a boatload of salt.

It looks to me like, going back to your original block of code:

  cps <-
    case (rds, ods) of
      (Some r, None) -> generateResetPeriods cpds rds
      (None, Some o) -> generateOtherPeriods cpds ods
      (Some r, Some o) -> do
        let r = generateResetPeriods cpds rds
        let o = generateOtherPeriods cpds ods
        -- sequence expression to return both r and o
        return ...
      (None, None) -> error "expecting calculation period"  

both generateResetPeriods and generateOtherPeriods are going to walk down the list of values in cpds and, for each element in there (possibly based on the values in rds or ods), generate a new element that has some properties added.

Simplifying a bit, let’s assume we have the following definitions:

data CPD = CPD { reset: Optional Int, other: Optional Int }

generateResetPeriods: [CPD] -> Optional Int -> [CPD]
generateResetPeriods cpds None = cpds
generateResetPeriods cpds (Some x) =
  map (\cpd -> cpd with reset = Some x) cpds

generateOtherPeriods: [CPD] -> Optional Int -> [CPD]
generateOtherPeriods cpds None = cpds
generateOtherPeriods cpds (Some x) =
  map (\cpd -> cpd with other = Some x) cpds

Obviously what the code is doing here is much more complicated and involves fetches, but if the issue is what I think it is, this should be enough to illustrate it.

This gives us the following situation:

  cps <-
    case (rds, ods) of
      -- works as expected: all elements of cpds end up with
      -- the reset field set to the given value
      (Some _, None) -> generateResetPeriods cpds rds

      -- works as expected: all elements of cpds end up with
      -- the other field set to the given value
      (None, Some _) -> generateOtherPeriods cpds ods

      -- this doesn't work: applying both functions seems
      -- to work well enough at first, but then, what can we
      -- do with the results?
      (Some _, Some _) -> do
        let r = generateResetPeriods cpds rds
        let o = generateOtherPeriods cpds ods
        -- r and o cannot easily be combined: we have two
        -- sets of results where we want one answer
        return ...
      (None, None) -> error "blow up" 

So what to do? In this very simplified case, the input and output types are the same, so we could easily resolve the issue by changing the (Some _, Some _) case to:

let intermediateResult = generateResetPeriods cpds rds
let finalResult = generateOtherPeriods intermediateResult ods
return finalResult

But that doesn’t work in your case because the input and output types don’t match. Also, you have a Fetch instance there. First, we could get rid of the Fetch instance by turning the lets into <-. Second, we need a way to combine the results. Let’s update our example to match your case a bit better:

data Input = Input {}
data Output = Output { reset: Optional Int, other: Optional Int }

class (Action f) => Fetch f

generateResetPeriods: (Fetch f) =>  [Input] -> Optional Int -> f [Output]
generateResetPeriods cpds None =
  return $ map (const $ Output None None) cpds
generateResetPeriods cpds (Some x) =
  return $ map (const $ Output with other = None, reset = Some x) cpds

generateOtherPeriods: (Fetch f) => [Input] -> Optional Int -> f [Output]
generateOtherPeriods cpds None =
  return $ map (const $ Output None None) cpds
generateOtherPeriods cpds (Some x) =
  return $ map (const $ Output with other = Some x, reset = None) cpds

The (Some _, Some _) case would become:

(Some _, Some _) -> do
  let r1 =  generateResetPeriods cpds rds
  let r2 = generateOtherPeriods cpds ods
  -- r1 and r2 have the same type here: f [Output]
  -- what to do about that `f`?

First, let’s take care of the Fetch issue: we would much rather deal with pure lists if possible. In this example, [Output], in your case, [CalculationPeriod]. As mentioned above, we can do that by using <- notation:

(Some _, Some _) -> do
  r1 <-  generateResetPeriods cpds rds
  r2 <- generateOtherPeriods cpds ods
  -- r1 and r2 have the same type here: [Output]
  -- we still have two lists and we want only one

I can see two options here. The first one is what @cocreature was suggesting: traverse the original list only once. In the case of my simple example here, this would amount to rewriting the two generate functions to something like:


import DA.Action((>=>))

data Input = Input {}
data Output = Output { reset: Optional Int, other: Optional Int }

class (Action f) => Fetch f

commonStep: (Fetch f) => Input -> f Output
commonStep i = return $ Output with other = None, reset = None

-- note: swapped arguments for notational convenience later on
resetStep: (Fetch f) => Optional Int -> Output -> f Output
resetStep None o = return o
resetStep (Some x) o = return $ o with reset = Some x

otherStep : (Fetch f) => Optional Int -> Output -> f Output
otherStep None o = return o
otherStep (Some x) o = return $ o with other = Some x

-- By having decomposed things in this way, we can write:

f: (Fetch f) => [Input] -> Optional Int -> Optional Int -> f [Output]
f cpds rds ods = do
    case (rds, ods) of
      (Some _, None) -> mapA (commonStep >=> resetStep rds) cpds
      (None, Some _) -> mapA (commonStep >=> otherStep ods) cpds
      (Some _, Some _) ->
        mapA (commonStep >=> resetStep rds >=> otherStep ods) cpds
      (None, None) -> error "blow up"

This, however, means a lot of changes to the code as you need to extract a lot of logic from the guts of generateResetPeriods and generateOtherPeriods. Another option (which may or may not be applicable depending on some details of what these functions do that I don’t fully understand) is to combine the results of those functions after the fact, rather than combine their application. This can only be done if both functions do, as I believe, return lists of essentially the same things in the same order, just with some of them decorated with additional data.

In that case, the simplified example code becomes:

data Input = Input {}
data Output = Output { reset: Optional Int, other: Optional Int }

class (Action f) => Fetch f

generateResetPeriods: (Fetch f) =>  [Input] -> Optional Int -> f [Output]
generateResetPeriods cpds None =
  return $ map (const $ Output None None) cpds
generateResetPeriods cpds (Some x) =
  return $ map (const $ Output with other = None, reset = Some x) cpds

generateOtherPeriods: (Fetch f) => [Input] -> Optional Int -> f [Output]
generateOtherPeriods cpds None =
  return $ map (const $ Output None None) cpds
generateOtherPeriods cpds (Some x) =
  return $ map (const $ Output with other = Some x, reset = None) cpds

combine: Output -> Output -> Output
combine o1 o2 = Output with reset = o1.reset, other = o2.reset

f: (Fetch f) => [Input] -> Optional Int -> Optional Int -> f [Output]
f cpds rds ods = do
    case (rds, ods) of
      -- no change
      (Some _, None) -> generateResetPeriods cpds rds

      -- no change
      (None, Some _) -> generateOtherPeriods cpds ods

      (Some _, Some _) -> do
        r1 <- generateResetPeriods cpds rds
        r2 <- generateOtherPeriods cpds ods
        -- the <- got rid of the fetches so we have two lists of results,
        -- we just need to combine them by walking through both lists
        -- at the same time
        return $ zipWith combine r1 r2
      (None, None) -> error "blow up" 

Hope this helps.

2 Likes

Great summary @Gary_Verhaegen! You are completely correct that writing the combining function afterwards and using zipWith also works. However, I would still recommend splitting things up for a few reasons:

  1. I think the code is easier to understand if you separate the generation of the list from the modification afterwards.
  2. It’s more robust against refactoring. The zipWith version works as long as both generate lists of the same length. But if you modify one of them to return a longer or shorter list you will drop the additional elements from the longer list. By factoring it out, you make it much clearer that the generation should be shared and cannot end up accidentally changing it.
  3. Writing the combining function isn’t quite as easy if you have extra fields in the type that aren’t modified by either of the functions (that seems to be the case here looking at generateResetPeriods). You now need to make an arbitrary choice to either take the first or the second value of those fields or enforce that they are the same and error out if that’s not the case. However, I do agree that it’s probably still less work in the short term than the separation I’m proposing.

Going back to the concrete example, here’s how I would split up generateResetPeriods. For reference this is the original case (I’ve switched fom Optional ResetDates to ResetDates since we only call it with Some). To keep things simple, I’ve replaced the implementation of everything we’re not going to modify by .

generateResetPeriods : Fetch f => CalculationPeriodDates -> ResetDates -> f [CalculationPeriod]
generateResetPeriods cpds (checkResetDates cpds -> rds) = do
  -- Roll out calculation periods and unadjusted reset dates
  cps <- generateCalculationPeriods cpds
  let rpDatesV = map generateResetPeriodDates cps

  -- Set reset and fixing dates
  let adj = …
  let fixingOffset = …
  let resetRelativeTo =…
  mapA (setResetDates resetRelativeTo fixingOffset adj) $ zip cps rpDatesV

  where
    -- multiple resets per period not supported yet
    generateResetPeriodDates : CalculationPeriod -> [(Date, Date)]
    generateResetPeriodDates cp = …

    setResetDates :
      (Fetch f)
      => ResetRelativeToEnum
      -> RelativeDateOffset
      -> BusinessDayAdjustments
      -> (CalculationPeriod, [(Date, Date)])
      -> f CalculationPeriod
    setResetDates resetRelativeTo fixingOffset adj (cp, rpDates) = …

Now, let’s split out the modifications we do on each period generated by generateCalculationPeriods:

modifyCalculationPeriod : Fetch f => ResetDates -> CalculationPeriod -> f CalculationPeriod
modifyCalculationPeriod rds period = do
  -- Set reset and fixing dates
  let adj = error "unimplemented"
  let fixingOffset = …
  let resetRelativeTo = …
  setResetDates resetRelativeTo fixingOffset adj (period, generateResetPeriodDates period)

  where
    -- multiple resets per period not supported yet
    generateResetPeriodDates : CalculationPeriod -> [(Date, Date)]
    generateResetPeriodDates cp = …

    setResetDates :
      (Fetch f)
      => ResetRelativeToEnum
      -> RelativeDateOffset
      -> BusinessDayAdjustments
      -> (CalculationPeriod, [(Date, Date)])
      -> f CalculationPeriod
    setResetDates resetRelativeTo fixingOffset adj (cp, rpDates) = …

We can now recover generateResetPeriodDates by using this function with mapA.

generateResetPeriods : Fetch f => CalculationPeriodDates -> ResetDates -> f [CalculationPeriod]
generateResetPeriods cpds (checkResetDates cpds -> rds) = do
  cps <- generateCalculationPeriods cpds
  mapA (modifyCalculationPeriod rds) cps

Assuming you have done the same to generateOtherPeriods and have a modifyOtherPeriod function you can now get the combined function as follows:

generateCombinedPeriods : Fetch f => CalculationPeriodDates -> ResetDates -> OtherValue -> f [CalculationPeriod]
generateCombinedPeriods cpds (checkResetDates cpds -> rds) other = do
  cps <- generateCalculationPeriods cpds
  mapA combine cps
  where combine period = do
                period' <- modifyCalculationPeriod rds period
                modifyOtherPeriod other period'
2 Likes

Thank you for laying this out, @cocreature! Initially, generatePaymentCalculationPeriods had Optional type for ResetDates and OtherDates.

The compiler complains in the InterestRatePayout class at:

pcps <- generatePaymentCalculationPeriods irp.calculationPeriodDates irp.resetDates irp.otherDates irp.paymentDates

The error:

/Applications/DEV/DAML/ex-cdm-swaps- copy/daml/Org/Isda/Cdm/EventSpecificationModule/Impl/Contract/Payout/InterestRatePayout.daml:43:11: error:
    * Couldn't match type `Optional ResetDates' with `ResetDates'
        arising from a functional dependency between:
          constraint `DA.Internal.Record.HasField
                        "resetDates" InterestRatePayout ResetDates'
            arising from a use of `DA.Internal.Record.getField'
          instance `DA.Internal.Record.HasField
                      "resetDates" InterestRatePayout (Optional ResetDates)'
            at <no location info>
    * In the second argument of `generatePaymentCalculationPeriods', namely
        `(DA.Internal.Record.getField @"resetDates" irp)'
      In a stmt of a 'do' block:
        pcps <- generatePaymentCalculationPeriods
                  (DA.Internal.Record.getField @"calculationPeriodDates" irp)
                  (DA.Internal.Record.getField @"resetDates" irp)
                  (DA.Internal.Record.getField @"otherDates" irp)
                  (DA.Internal.Record.getField @"paymentDates" irp)
      In the expression:
        do pcps <- generatePaymentCalculationPeriods
                     (DA.Internal.Record.getField @"calculationPeriodDates" irp)
                     (DA.Internal.Record.getField @"resetDates" irp)
                     (DA.Internal.Record.getField @"otherDates" irp)
                     (DA.Internal.Record.getField @"paymentDates" irp)
           let firstPcp = getFirst "PaymentCalculationPeriod" pcps
           let pastResetEvents = filter (\ e -> ...) pastEvents
           let existingResets = getExistingResets firstPcp pastResetEvents irp
           ....compiler

There’s also a warning for the cps <-...:

 /Applications/DEV/DAML/ex-cdm-swaps- copy/daml/Org/Isda/Cdm/EventSpecificationModule/Impl/Contract/Payout/InterestRatePayout.daml:46:7: warning:
    Pattern match is redundant
    In a case alternative: (r, o) -> ...
1 Like

Change your call-site from

 cps <-
    case (rds, ods) of
      -- works as expected: all elements of cpds end up with
      -- the reset field set to the given value
      (Some _, None) -> generateResetPeriods cpds rds

to

 cps <-
    case (rds, ods) of
      -- works as expected: all elements of cpds end up with
      -- the reset field set to the given value
      (Some rds', None) -> generateResetPeriods cpds rds'

or alternatively change generateResetPeriods to accept an Optional.

2 Likes

Should the combined function be called by cps <-... in generatePaymentCalculationPeriods or it remain in the ResetDates class?

I’m getting the same error as before of floatingRateDefinition not set coming from here, existingResets, resetReferences and then floatingRateDefinition from the populatePCP function.

Could it be that the Fetch: State, Functor and Applicative need to include combined function (as both floatingRateDefinition and otherValueDefinition depend on different Reference Data)?