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:
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.
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")'