Programming an IOU
Introduction
Protocols are core constructs of NPL that represent contracts, regulations, and operating procedures. They represent agreements between parties on the deontic modalities of permissions, obligations, and prohibitions.
In this section we'll show how protocols are defined in NPL by programming an IOU.
Creating the IOU
Let's start by creating the definition for our IOU protocol. An IOU represents an acknowledgement of debt between two parties. The party who issues the IOU and owes the money is referred to as the issuer, and the party who lent the money to the issuer is the payee.
An NPL definition starts with the keyword protocol
, and is immediately followed by the protocol parties, placed
between square brackets. Then follows the name, for which we'll use Iou
, and the input parameters required to
instantiate the protocol.
For our IOU we'll need to know how much money is owed, so let's add an input parameter of type Number.
protocol[issuer, payee] Iou(var forAmount: Number) {
// Protocol body
};
Type annotations are important in NPL, because it is a compiled and statically typed language. This means the compiler will verify that you have given it the correct type even before the code is run. For an explanation of NPL's type system, see Type system.
Deriving rights from our IOU
Above we have defined an IOU, but we can't do anything with it yet! For this IOU, we'd like the following behavior:
- the issuer should be able to pay off the debt all at once, or in increments
- the payee should be able to forgive the debt at any point in time, unless of course the issuer has already paid
- either party should be able to request both the currently outstanding amount, and view the payment history
Let's start by adding a way for the issuer to make a payment.
The deontic modalities available in NPL are permissions, obligations, and prohibitions. Permissions and obligations are
implemented directly on the protocols (more about prohibitions later on), and are referred to as actions. They start
with the keyword permission
or obligation
, followed by the party that is obliged or permitted placed between square
brackets. Then follows the name, along with any input values required to invoke the action.
In this case, we'll use a permission called pay
for the payment mechanism, and we'll simply allow the issuer to
specify how much has been paid.
protocol[issuer, payee] Iou(var forAmount: Number) {
permission[issuer] pay(amount: Number) {
// payment logic
};
};
In real systems the burden of proof could be placed on external mechanisms instead, but these are beyond the scope of our IOU.
It is important to note that any party referenced in a protocol must be part of the protocol header. Otherwise, parties external to the protocol might accidentally be required to perform actions they have not agreed upon.
Storing data
The pay permission has now been defined, but it doesn't do anything yet. Because incoming payments will affect the outstanding amount, we must keep track of them.
Because we required that both parties need to be able to view the payment history, we have to keep track of both the
amount and the time at which the payment was made. To do so, we'll introduce a special data structure that holds a
Number
and a DateTime
called a Struct
. Below we've defined a struct called TimestampedAmount
that holds two
fields: one named amount
of type Number
, and one named timestamp
of type DateTime
.
struct TimestampedAmount {
amount: Number,
timestamp: DateTime
};
On our IOU each incoming payment corresponds to one struct. We'll introduce a protocol member called payments
that
represents a List
on which each item is a TimestampedAmount
.
struct TimestampedAmount {
amount: Number,
timestamp: DateTime
};
protocol[issuer, payee] Iou(var forAmount: Number) {
var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();
};
Because NPL does not allow us to leave protocol members unassigned, we have initialized this list as an empty list of
TimestampedAmount
. An empty list is instantiated using the listOf
function, which is a built-in NPL function.
Assignment expressions for protocol members are always run as part of the protocol instantiation.
Adding execution logic to actions
Now that we have a place to store our data, let's make sure the payment is stored on the protocol. First, wrap the
incoming payment into a TimestampedAmount
, and then add it to the list.
struct TimestampedAmount {
amount: Number,
timestamp: DateTime
};
protocol[issuer, payee] Iou(var forAmount: Number) {
var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();
permission[issuer] pay(amount: Number) {
var p = TimestampedAmount(amount = amount, timestamp = now());
payments = payments.with(p);
};
};
An instance of TimestampedAmount
is created by using its name, followed by the named parameters and their values in
braces. For the timestamp
field, we invoke the now
function. This is another NPL built-in function that returns the
current DateTime
. Then, we invoke the .with
method, an NPL built-in on lists that returns an existing list with a
new element added. The result is then assigned to the payments field on the protocol.
Prohibitions using state guards
Another requirement for our IOU was that the payee should be able to forgive the debt at any point in time. Let's introduce this as a protocol permission.
struct TimestampedAmount {
amount: Number,
timestamp: DateTime
};
protocol[issuer, payee] Iou(var forAmount: Number) {
var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();
permission[issuer] pay(amount: Number) {
var p = TimestampedAmount(amount = amount, timestamp = now());
payments = payments.with(p);
};
permission[payee] forgive() {
// forgive logic
};
};
What should the forgive
permission body do? Forgiving means the issuer no longer needs to pay, and in fact should no
longer be able to pay.
In order to accommodate such protocol logic, NPL natively supports states as an action prohibition mechanism. States are
defined on the protocol using the keyword state
, followed by the name. One of the states must be prefixed by the
keyword initial
, indicating that the protocol will in that state upon creation. Other states may optionally be
labelled final
, indicating that once reached, no further state transitions are allowed.
Transitioning to another protocol state is achieved using the keyword become
, followed by the name of the state.
States may also be used as guards on actions (a form of prohibition) using the | stateName
on protocol actions.
Let's use states for our IOU. We introduce an initial state called unpaid
, and two final states paid
and forgiven
.
Invoking the forgive
permission causes a state transition to forgiven
. We'll also add a state guard such that pay
and forgive
may only be invoked as long as the protocol state is unpaid
.
struct TimestampedAmount {
amount: Number,
timestamp: DateTime
};
protocol[issuer, payee] Iou(var forAmount: Number) {
initial state unpaid;
final state paid;
final state forgiven;
var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();
permission[issuer] pay(amount: Number) | unpaid {
var p = TimestampedAmount(amount = amount, timestamp = now());
payments = payments.with(p);
};
permission[payee] forgive() | unpaid {
become forgiven;
};
};
Now the invocation of forgive
will automatically cause pay
to become unavailable!
Helper functions
Our pay
permission still lacks certain features. Specifically, regardless of how much is paid, we have not added a
state transition to paid
. We have also not added a permission that shows the currently outstanding amount. Both of
these features rely on the currently outstanding amount, and are a prime candidate for helper functions.
Helper functions are defined using the keyword function
, and they do not specify any parties. Functions are chiefly
intended to facilitate the reuse of shared logic, and to separate code into smaller parts. If defined on a protocol they
may access the protocol's members, but they may only be invoked from within other permissions or functions in that
protocol. Note that helper functions may also be defined outside of the protocol.
Let's add two helper functions. The first is called total
and simply sums up all amounts on a list of
TimestampedAmount
. The second is amountOwed
and is defined on the protocol itself. Note how it uses the protocol's
forAmount
member directly.
struct TimestampedAmount {
amount: Number,
timestamp: DateTime
};
function total(entries: List<TimestampedAmount>) returns Number -> {
return entries.map(function(p: TimestampedAmount) returns Number -> p.amount).sum();
};
protocol[issuer, payee] Iou(var forAmount: Number) {
initial state unpaid;
final state paid;
final state forgiven;
var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();
function amountOwed() returns Number -> {
return forAmount - total(payments);
};
permission[issuer] pay(amount: Number) | unpaid {
var p = TimestampedAmount(amount = amount, timestamp = now());
payments = payments.with(p);
};
permission[payee] forgive() | unpaid {
become forgiven;
};
};
The syntax for functions is such that it specifies its input values, and explicitly defines its return type using the
returns
keyword. It explicitly uses the return
keyword to return a result.
Next we'll add a permission called getAmountOwed
that returns the currently outstanding amount.
struct TimestampedAmount {
amount: Number,
timestamp: DateTime
};
function total(entries: List<TimestampedAmount>) returns Number -> {
return entries.map(function(p: TimestampedAmount) returns Number -> p.amount).sum();
};
protocol[issuer, payee] Iou(var forAmount: Number) {
initial state unpaid;
final state paid;
final state forgiven;
var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();
function amountOwed() returns Number -> {
return forAmount - total(payments);
};
permission[issuer] pay(amount: Number) | unpaid {
var p = TimestampedAmount(amount = amount, timestamp = now());
payments = payments.with(p);
};
permission[payee] forgive() | unpaid {
become forgiven;
};
permission[issuer|payee] getAmountOwed() returns Number {
return amountOwed();
};
};
The getAmountOwed
permission simply returns the result of the amountOwed
function. Unlike the other permissions,
this permission returns a result, and it is therefore annotated using returns
. It also lists two parties, making this
permission available to either party.
Note that the permission explicitly uses the return
keyword to return a result.
Our helper function also allows us to add the state transition to the pay
permission.
struct TimestampedAmount {
amount: Number,
timestamp: DateTime
};
function total(entries: List<TimestampedAmount>) returns Number -> {
return entries.map(function(p: TimestampedAmount) returns Number -> p.amount).sum();
};
protocol[issuer, payee] Iou(var forAmount: Number) {
initial state unpaid;
final state paid;
final state forgiven;
var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();
function amountOwed() returns Number -> {
return forAmount - total(payments);
};
permission[issuer] pay(amount: Number) | unpaid {
var p = TimestampedAmount(amount = amount, timestamp = now());
payments = payments.with(p);
if (amountOwed() == 0) {
become paid;
};
};
permission[payee] forgive() | unpaid {
become forgiven;
};
permission[issuer|payee] getAmountOwed() returns Number {
return amountOwed();
};
}
Prohibitions using require conditions
There are still some loopholes in our protocol, the biggest of which is that the issuer is allowed to make negative
payments. Furthermore, our amountOwed() == 0
check will only work if the issuer never pays more than what is owed.
NPL uses require
conditions to prohibit certain parameter inputs. A require
is a boolean expression that serves as
precondition. Let's add them for the pay
permission.
struct TimestampedAmount {
amount: Number,
timestamp: DateTime
};
function total(entries: List<TimestampedAmount>) returns Number -> {
return entries.map(function(p: TimestampedAmount) returns Number -> p.amount).sum();
};
protocol[issuer, payee] Iou(var forAmount: Number) {
initial state unpaid;
final state paid;
final state forgiven;
var payments: List<TimestampedAmount> = listOf<TimestampedAmount>();
function amountOwed() returns Number -> {
return forAmount - total(payments);
};
permission[issuer] pay(amount: Number) | unpaid {
require(amount > 0, "Payments must be strictly positive");
require(amount <= amountOwed(), "Cannot pay more than is owed");
var p = TimestampedAmount(amount = amount, timestamp = now());
payments = payments.with(p);
if (amountOwed() == 0) {
become paid;
};
};
permission[payee] forgive() | unpaid {
become forgiven;
};
permission[issuer|payee] getAmountOwed() returns Number {
return amountOwed();
};
}
Our protocol is based on the premise that forAmount
is strictly positive. require
conditions may also be defined on
a protocol level, so we'll add one there as well.
struct TimestampedAmount {
amount: Number,
timestamp: DateTime
};
function total(entries: List<TimestampedAmount>) returns Number -> {
return entries.map(function(p: TimestampedAmount) returns Number -> p.amount).sum();
};
@api
protocol[issuer, payee] Iou(var forAmount: Number) {
require(forAmount > 0, "Initial amount must be strictly positive");
initial state unpaid;
final state paid;
final state forgiven;
private var payments = listOf<TimestampedAmount>();
function amountOwed() returns Number -> {
return forAmount - total(payments);
};
@api
permission[issuer] pay(amount: Number) | unpaid {
require(amount > 0, "Amount must be strictly positive");
require(amount <= amountOwed(), "Amount may not exceed amount owed");
var p = TimestampedAmount(amount = amount, timestamp = now());
payments = payments.with(p);
if (amountOwed() == 0) {
become paid;
};
};
@api
permission[payee] forgive() | unpaid {
become forgiven;
};
@api
permission[issuer|payee] getAmountOwed() returns Number {
return amountOwed();
};
}
Obligations
Note
Obligations are under active development: Some features are subjects to change.
Now that everything is in place, it is time to reconsider the rather informal nature of the pay
permission. Instead,
we'd really like to make sure that the issuer
has some motivation to pay back quickly.
To accomplish this, the payee
and issuer
agree on a reasonable payment deadline and a late payment fee. These are
part of the protocol's instantiation, and are added to the constructor.
protocol[issuer, payee] Iou(var forAmount: Number, var paymentDeadline: DateTime, var lateFee: Number) {
If the payment is not complete before the deadline, the payee
has the right (but not the obligation) to charge a late
payment fee the total amount by some pre-agreed late payment fee. If the payment is indeed raised, the deadline is
automatically extended by one month.
The corresponding NPL permission relies on an additional state default
(indicating that the issuer
's payment is
late) and looks as follows.
@api
permission[payee] chargeLatePaymentFee() | default {
forAmount = forAmount + lateFee;
paymentDeadline = now() + months(1);
become unpaid;
};
There may be other factors that lead to the final decision to charge the payment fee, but the issuer
should be able to
pay again as soon as the payee
has invoked the right to charge the late payment fee.
@api
obligation[issuer] pay(amount: Number) before paymentDeadline | unpaid, default {
require(amount > 0, "Amount must be strictly positive");
require(amount <= amountOwed(), "Amount may not exceed amount owed");
var p = TimestampedAmount(amount = amount, timestamp = now());
payments = payments.with(p);
if (amountOwed() == 0) {
become paid;
};
} otherwise become default;
Now pay
is an obligation with a deadline as specified. Note that forgive
may also be invoked if the state is
default
.
@api
protocol[issuer, payee] Iou(var forAmount: Number, var paymentDeadline: DateTime, var lateFee: Number) {
require(forAmount > 0, "Initial amount must be strictly positive");
require(paymentDeadline.isAfter(now() + months(1), false), "Payment deadline must be at least one month in the future");
require(lateFee > 0, "Late fee must be strictly positive");
initial state unpaid;
state default;
final state paid;
final state forgiven;
private var payments = listOf<TimestampedAmount>();
function amountOwed() returns Number -> {
return forAmount - total(payments);
};
@api
obligation[issuer] pay(amount: Number) before paymentDeadline | unpaid, default {
require(amount > 0, "Amount must be strictly positive");
require(amount <= amountOwed(), "Amount may not exceed amount owed");
var p = TimestampedAmount(amount = amount, timestamp = now());
payments = payments.with(p);
if (amountOwed() == 0) {
become paid;
};
} otherwise become default;
@api
permission[payee] chargeLatePaymentFee() | default {
forAmount = forAmount + lateFee;
paymentDeadline = now() + months(1);
become unpaid;
};
@api
permission[payee] forgive() | unpaid, default {
become forgiven;
};
@api
permission[issuer|payee] getAmountOwed() returns Number {
return amountOwed();
};
}
Our protocol definition is now complete!