Open hugohills-regnosys opened 1 year ago
Simplified example from the DRR project - how to implement without inheritance and dispatch functions.
The example below show that with the current DSL features, the implementation requires many if
statements to manually check which payout leg exists and process it accordingly.
Add type to represent single payout leg. Note that it does not use inheritance; PayoutLeg
contains attributes of each payout:
type PayoutLeg:
optionPayout OptionPayout (0..1)
cashflow Cashflow (0..1)
creditDefaultPayout CreditDefaultPayout (0..1)
condition: one-of
Function to extract the PayoutLeg
from the Trade
:
func NotionalPayoutLeg1:
inputs:
trade Trade (1..1)
output:
payoutLeg PayoutLeg (0..1)
set Payout( trade ) -> optionPayout:
// logic to extract option leg 1 if exists
set Payout( trade ) -> cashflow:
// logic to extract FX leg 1 if exists
set Payout( trade ) -> creditDefaultPayout:
// logic to extract CDS leg 1 if exists
Function to extract the notional amount from the PayoutLeg
:
func NotionalAmount: <"Extract notional amount from the PayoutLeg.">
inputs:
payoutLeg PayoutLeg (0..1)
output:
amount number (0..1)
set amount:
if payoutLeg -> optionPayout exists then
// logic to extract notional amount from optionPayout
else if payoutLeg -> cashflow exists then
// logic to extract notional amount from cashflow
else if payoutLeg -> creditDefaultPayout exists then
// logic to extract notional amount from creditDefaultPayout
Same example from the DRR project, with added support for inheritance and dispatch functions
The example below show that with the improved inheritance and dispatch function features, the implementation is more elegant with less code where the functions are dynamically selected based on the runtime type.
Each payout leg extends PayoutLeg
:
type PayoutLeg:
// common attributes
type InterestRatePayout extends PayoutLeg:
// interest rate payout attributes
type CreditDefaultPayout extends PayoutLeg:
// CDS payout attributes
type OptionPayout extends PayoutLeg:
// option payout attributes
Function to extract the payout leg from the Trade
. Note that the function return type PayoutLeg
is the super type of each payout leg.
func NotionalPayoutLeg1:
inputs:
trade Trade (1..1)
output:
payoutLeg PayoutLeg (0..1)
set payoutLeg:
if Payout( trade ) -> optionPayout exists then
// logic to extract option leg 1 if exists
else if Payout( trade ) -> cashflow exists then
// logic to extract FX leg 1 if exists
else if Payout( trade ) -> creditDefaultPayout exists then
// logic to extract CDS leg 1 if exists
Dispatch functions all have the same name (i.e. NotionalAmount
) and each one provides an function implementation for a sub-class of PayoutLeg
. At runtime the function that is selected to be invoked would match the sub-type PayoutLeg
. This provides a elegant separation of function instances, without messy if statements and additional objects.
func NotionalAmount:
inputs: optionPayout OptionPayout (0..1)
output: amount number (0..1)
set amount:
// logic to extract notional amount from optionPayout
func NotionalAmount:
inputs: cashflow Cashflow (0..1)
output: amount number (0..1)
set amount:
// logic to extract notional amount from cashflow
func NotionalAmount:
inputs: creditDefaultPayout CreditDefaultPayout (0..1)
output: amount number (0..1)
set amount:
// logic to extract notional amount from creditDefaultPayout
I've got an alternative suggestion inspired by functional programming, with benefits in increased type safety, and much increased extensibility compared to OO-inspired dispatching.
Introducing a trait
.
Instead of defining a superclass for PayoutLeg
, define a (very much dialed-down version of a) "trait" (so called in Rust, but could be named something else such as func group
; also called "typeclass" in Haskell; is similar to an interface in Java, but more extensible):
trait NotationalAmount for PayoutLeg: // `PayoutLeg` is a type parameter here. A trait could define multiple ones.
inputs:
payoutLeg PayoutLeg (1..1)
output:
amount number (1..1)
Then, for each of the of the concrete payout types, define an implementation:
type InterestRatePayout: // note: no super type
// interest rate payout attributes
type CreditDefaultPayout:
// CDS payout attributes
type OptionPayout:
// option payout attributes
implement NotationalAmount for InterestRatePayout:
set amount: // logic to extract notional amount from InterestRatePayout
implement NotationalAmount for CreditDefaultPayout:
set amount: // logic to extract notional amount from CreditDefaultPayout
implement NotationalAmount for OptionPayout:
set amount: // logic to extract notional amount from OptionPayout
Then, an attribute can define its type as "any type implementing NotationalAmount
":
type Trade:
with types: // same syntax in functions
NotationalAmount for PayoutLeg
...
payoutLeg PayoutLeg (1..1)
...
Advantages over OO dispatching:
PayoutLeg
will always have an implementation for NotationalAmount
. This is impossible to guarantee with dispatch methods, because a modeller might always add a new type of PayoutLeg
and forget to implement it.PayoutLeg
by adding an implementation for NotationalAmount
. I.e., if a modeller decides that the number
type can also represent a payout leg, then it's as easy as writing
implement NotationalAmount for number:
// provide implementation
Note that, in contrast to an OO interface, a client extending the DRR model can define their own trait and then add implementations to existing CDM/DRR types for that trait, again with benefits of improved extensibility and customization.
Just an example to show its expressiveness at its full power:
// Define a trait for things that can be added together.
// Note that the left and right type might differ, as well as the output type.
trait Add for Left, Right, Result:
inputs:
left Left (1..1)
right Right (1..1)
output:
result Result (1..1)
implement Add for number, number, number:
set result: left + right
implement Add for string, string, string:
set result: left + right
implement Add for date, int, date: // types don't have to be the same
set result: AddDaysToDate(left, right)
func Double:
with types:
Add for Input, Input, Result // Note that we demand that the `Left` and `Right` types are equal
inputs:
a Input (1..1)
output:
result Result (1..1)
set result:
Add(a, a)
// CDM could use this to implement additions of quantities as well
implement Add for Quantity, Quantity, Quantity:
// ...
implement Add for Quantity, number, Quantity: // or add a raw number to a quantity
// ...
// Now `Add` will automagically work both for adding two quantities and adding a raw number to a quantity.
// Which implementation to use is figured out automatically by looking at the input types.
// Note that the `with types` syntax is much more powerful that dispatching as well.
// E.g., imagine something such as the following constraint:
func Dummy:
with types:
Add for T, U, T // type T is both the left parameter and the output of `Add`
NotationalAmount for U // type U is both the right parameter of `Add`, and also a kind of `Payout`.
Dispatch functions proposal / discussion.
Dynamic dispatch is the process of selecting which implementation of a polymorphic function to call at run time.
To demonstrate the benefits of dispatch functions, this issue will show examples of how to implement the reporting rule below without dispatch functions (e.g. existing features), and with dispatch functions.