functional-programmingsanctuary

Data modelling challenges in sanctuary.js


I am building an app based on domain-driven design using functional programming in javascript. I've settled on the sanctuary.js ecosystem as my tool of choice, but I'm facing some challenges in modelling types.

To put things in context, let's take the code below as an example:

  const { create } = require('sanctuary')
  const $ = require('sanctuary-def')
  const def = create({
    checkTypes: process.env.NODE_ENV === 'development',
    env: $.env
  })

  const Currency = $.EnumType
    ('Currency')
    ('http://example.com')
    (['USD', 'EUR'])

  const Payment = $.RecordType({
    amount: $.PositiveNumber,
    currency: Currency,
    method: $.String
  })

My points of confusion follow:

  1. How do I define a simple type with sanctuary-def? From the example above, if I wanted to define a Amount type and use it in the Payment RecordType definition, how would I go about it? Would I have to define another RecordType for that?
  2. I may be wrong here, but my understanding so far is that the RecordType is equivalent to product types in functional programming. What is the way to define a sum type using sanctuary.js? The EnumType above comes close, but seems to be for simple values, as opposed to other types. More like, if I had two other types, Cash and Card, how would I model another type PaymentMethod as a choice between Cash and Card?

I'd be glad for any pointers. I'm pretty new to both functional programming and sanctuary.js so if there's something obvious I'm missing, I'd appreciate a nudge in the right direction.

Much thanks.


Solution

  • Let's consider payment methods. To keep the example simple, let's assume that a payment method is either cash, or a credit/debit card with an associated card number. In Haskell, we could define the type and its data constructors like so:

    data PaymentMethod = Cash | Card String
    

    In order to define PaymentMethod using sanctuary-def we need to know how the Cash and Card data constructors are implemented. One is free to define these manually or to use a library such as Daggy. Let's write them by hand:

    //    Cash :: PaymentMethod
    const Cash = {
      '@@type': 'my-package/PaymentMethod',
      'tagName': 'Cash',
    };
    
    //    Card :: String -> PaymentMethod
    const Card = number => ({
      '@@type': 'my-package/PaymentMethod',
      'tagName': 'Card',
      'number': number,
    });
    

    Having defined the data constructors, we can then use $.NullaryType to define the PaymentMethod type:

    const $ = require ('sanctuary-def');
    const type = require ('sanctuary-type-identifiers');
    
    //    PaymentMethod :: Type
    const PaymentMethod = $.NullaryType
      ('PaymentMethod')
      ('https://example.com/my-package#PaymentMethod')
      ([])
      (x => type (x) === 'my-package/PaymentMethod');
    

    Note that because every PaymentMethod value will have the special @@type property, we can use type to determine whether an arbitrary JavaScript value is a member of the PaymentMethod type.

    I realize that approximating that one line of Haskell in JavaScript is quite involved. I hope this example shows how the various pieces of the puzzle fit together.


    We might like to define a case-folding function for PaymentMethod like so:

    //    foldPaymentMethod :: a -> (String -> a) -> PaymentMethod -> a
    const foldPaymentMethod = cash => card => paymentMethod => {
      switch (paymentMethod.tagName) {
        case 'Cash': return cash;
        case 'Card': return card (paymentMethod.number);
      }
    };
    

    Having defined Cash, Card, and foldPaymentMethod we are able to construct and deconstruct PaymentMethod values without worrying about implementation details. For example:

    > foldPaymentMethod ('Cash') (number => `Card (${S.show (number)})`) (Cash)
    'Cash'
    
    > foldPaymentMethod ('Cash') (number => `Card (${S.show (number)})`) (Card ('2468101214161820'))
    'Card ("2468101214161820")'