(This is basically a blog post, I just don't have a blog, no TL;DR for you)
I'm working on a DSL for defining business processes. Everything ends up translated into SQL in some way.
Basic things like defining type aliases and shapes might look 'Rusty', here's a B2B example to give you an idea:
alias TaxId = String(20) // [tax_id] denotes the unique key for these objects struct Customer [tax_id] { tax_id: TaxId, title: String(1000), // automatically creates a dependent table with an autoincrement id field // the name of this type is Customer::Contact contacts: struct Contact { name: String(100), tel: String(20), email: String(100), }, // there is a special 'type' for addresses // which gets embedded in multiple columns addresses: struct Address { label: String(100), // there is a special 'type' for addresses: Addr // which gets embedded in multiple columns address: Addr } documents: struct Document { description: String(1000), // files are special references to the table of files doc: File } } // financial values: 64 bit int with 6 decimal places alias Fin = Decimal(6) struct Item [item_id] { item_id: UUID, name: String(100), description: String(1000), cost: Fin, price: Fin, }
Given the above an order-to-cash process could look like this:
-
Sales-person takes customer's
Order
recording items and applying a discount to each line. -
Commercial manager reviews and approves the discounts.
-
Distribution executes (perhaps partially) the order producing an
Invoice
and aDeliveryNote
-
The customer pays the invoice producing a
Payment
Given a schema language I would say that after you define those structures, you then write code which manifests the business process. BUT a business process is a hierarchical state machine, and in theory you should be able to describe it using linear types.
I don't have much experience programming with linear types (I've done some simplistic protocol stuff leveraging ownership in rust), but this is roughly what I'm thinking:
// a linear type is defined with the 'step' keyword // the 'from' keyword denotes its source // in this case unit: it can be created from nothing step Order from () [order_number] { order_number: Serial, // autoincrement UInt64 customer: ref (Customer, orders), address: ref (self::customer::Address, orders), lines: step Line from () { // heirarchical state representation? item: ref (Item, order_lines), quantity: UInt32, discount: Decimal(6), // discount fraction }, overall_discount: Decimal(6), // discount over the total }
The ref meta-type takes a tuple (TargetType, reverse_name), the target type is the name of the type which is referenced by this entry, the embedding of the field could be multiple columns -- as many as needed to uniquely identify the target. the reverse_name is the 'virtual' field on the target type which refers to all instances that refer to it via this reference.
In the second ref (address) the target type is narrowed! it says that the address must come from addresses associated with this instance's customer.
When an order is created it is in an 'open' state, a new step must consume it in order for it to close. Once closed it cannot be consumed by another step. However, orders are composed of lines which are also linear and created in an open state, the lines must also be consumed, and crucially: the Order can only close if all its sub-steps close.
The next part is order approvals. An Order may be rejected outright, or each line must be individually approved or rejected, THEN the order can be approved.
This means we have four types: RejectedOrder, ApprovedOrderLine, RejectedOrderLine, and ApprovedOrder.
Note that user identity and timestamps are implicit in this DSL so there's no need to explicitly say there is an approving user. The user that creates the record IS the approver. The time of record creation is the time of approval.
To describe this stuff I need the following machinery:
-
Consume a top-level step while its sub-steps are still open (RejectedOrder)
-
Consume a sub-step (ApprovedOrderLine, RejectedOrderLine)
-
Consume a top level step IFF its sub-steps are all closed (ApprovedOrder)
This is what I'm thinking:
// order rejection consumes an order // final means this step is created in a closed state // partial means the order does not need all its sub-steps to be closed final step RejectedOrder from partial Order {} final step RejectedOrderLine from Order::Line {} step ApprovedOrderLine from Order::Line {} // order approval consumes the whole order step ApprovedOrder from Order {}
It is implicit in the last line here that the Order must have had all its lines approved or rejected (because it is not 'from partial'). Also note that we end up with a closed Order, but there's also a bunch of open ApprovedOrderLines, and the only way to get to them is through the association ApprovedOrder -> Order -> Lines -> ApprovedOrderLines
The next step is to produce a delivery note to deliver goods. One delivery note can service several lines from several orders, as long as all the orders have the same customer and address.
Here's a naive definition:
step DeliveryNote from () [dn_number] { dn_number: Serial, customer: ref (Customer, delivery_notes), address: ref (Customer::Address, delivery_notes), orders: struct Order {order: ref (ApprovedOrder, delivery_notes)}, lines: step Line from ApprovedOrderLine {quantity_delivered: UInt64}, }
There's so much to do here!
I need a way to represent the selected customer or the customer and address of the selected order etc. it might be helpful to have something like rust's where clause:
step DeliveryNote from () where { C: Customer, // there is one customer A: C.addresses, // there is one address belonging to the customer O < ApprovedOrder, // a set of approved orders O.customer = C, O.address = A, L: O -> Order -> Line -> ApprovedOrderLine, } [dn_number] { dn_number: Serial, customer: ref (C, delivery_notes), address: ref (A, delivery_notes), orders: struct Order {order: ref (O, delivery_notes)}, lines: step Line from ApprovedOrderLine {quantity_delivered: UInt64}, }
This took me three hours to write and I'm exhausted so I'm going to leave this for now.
If you read the damn thing I would love to hear your thoughts, especially if you know of a language that expresses ideas like these. I know that in SQL joins can do what I want, but I need a higher level language to achieve some abstractions w.r.t. SQL.