finos / rune-dsl

The project containing the Rune DSL grammar and default code generators
Apache License 2.0
25 stars 30 forks source link

Dynamic Dispatch functions #546

Open hugohills-regnosys opened 1 year ago

hugohills-regnosys commented 1 year ago

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.

reporting rule NotionalAmountLeg1 <"Notional Amount Leg 1">
    extract TradeOrTerminatedBeforeTrade( ReportableEvent )
    then extract NotionalPayoutLeg1( Trade )
    then extract NotionalAmount( PayoutLeg )
        as "31 Notional amount-Leg 1"
hugohills-regnosys commented 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
hugohills-regnosys commented 1 year ago

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
SimonCockx commented 1 year ago

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:

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`.