API design

Let’s continue working on our bank account example! We’ll start by adding an identifier to each account, so we can look up the owner.

Bank-Account : type {
    id :: Number
    balance :: Number
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "account #_: $_" id balance

Then we can define an open-account function to build a bank account with the provided identifier:

Bank-Account : type {
    id :: Number
    balance :: Number
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "account #_: $_" id balance
open-account :: Number -> Bank-Account
open-account : id -> {
    id : id
    balance : 0
}

Finally, we’ll make a new account!

Bank-Account : type {
    id :: Number
    balance :: Number
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "account #_: $_" id balance

open-account :: Number -> Bank-Account
open-account : id -> {
    id : id
    balance : 0
}
my-account : open-account 500
show my-account
account #500: $0

Hmm, it looks like we made a mistake — this code seems like it’s trying to open an account with $500, but instead it creates an account associated with the identifier 500. open-account takes a Number as input, but it’s not clear what that number actually represents.

Let’s make this API easier to understand and less error-prone by introducing a new type!

Account-ID : type Number

Rather than listing the fields (for a structure) or variants (for an enumeration), when you provide a single type, Wipple creates a wrapper for you:

Account-ID : type Number
-- Create an account ID wrapping 0
my-id : Account-ID 0

-- Unwrap the account ID to get the number back
Account-ID id-number : my-id

Let’s refactor Bank-Account to use our new Account-ID type!

Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id

Bank-Account : type {
    id :: Account-ID
    balance :: Number
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: $_" id balance

open-account :: Account-ID -> Bank-Account
open-account : id -> {
    id : id
    balance : 0
}

Now if we try to open a bank account with a plain number, we get an error:

Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id

Bank-Account : type {
    id :: Account-ID
    balance :: Number
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: $_" id balance

open-account :: Account-ID -> Bank-Account
open-account : id -> {
    id : id
    balance : 0
}
my-account : open-account 500
example:20:27: error: expected `Account-ID` here, but found a number

Great! Now it’s clear what kind of data open-account accepts. In general, when you’re designing APIs, try to create wrapper types around “plain” values like Number and Text to give the user of your API more information.

We can do the same thing for our balance, too. Another benefit of wrapper types is that you can customize how they’re displayed!

Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id
Balance : type Number

instance (Describe Balance) : (Balance dollars) -> "$_" dollars

Bank-Account : type {
    id :: Account-ID
    balance :: Balance
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: _" id balance

Next, let’s implement Add for Balance, and refactor deposit to use it:

Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id

Bank-Account : type {
    id :: Account-ID
    balance :: Balance
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: _" id balance

Balance : type Number

instance (Describe Balance) : (Balance dollars) -> "$_" dollars
instance (Add Balance Balance Balance) :
    (Balance current) (Balance amount) -> Balance (current + amount)

open-account :: Account-ID -> Bank-Account
open-account : id -> {
    id : id
    balance : Balance 0
}

deposit :: Balance -> Bank-Account -> Bank-Account
deposit : deposit -> {
    id : id
    balance : current
} -> {
    id : id
    balance : current + deposit
}

my-account : open-account (Account-ID 123)
show (my-account . deposit (Balance 50))

What about withdraw? Withdrawing is a bit trickier, since you can’t withdraw more than the account’s balance. Let’s use Maybe to represent this condition — if you have enough money in your account, you get back a Some value, and if you try to withdraw too much, you get back None:

Balance : type Number
instance (Subtract Balance Balance (Maybe Balance)) :
    (Balance current) (Balance amount) ->
        if (amount <= current) {Some (Balance (current - amount))} {None}

Now it’s up to you how to implement withdraw. In this example, we’ll revert back to the bank account as it was before attempting the withdrawal. This is where producing new values in Wipple, rather than mutating them in place, comes in handy!

Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id

Bank-Account : type {
    id :: Account-ID
    balance :: Balance
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: _" id balance

Balance : type Number

instance (Describe Balance) : (Balance dollars) -> "$_" dollars

instance (Subtract Balance Balance (Maybe Balance)) :
    (Balance current) (Balance amount) ->
        if (amount <= current) {Some (Balance (current - amount))} {None}

open-account :: Account-ID -> Bank-Account
open-account : id -> {
    id : id
    balance : Balance 0
}
withdraw :: Balance -> Bank-Account -> Bank-Account
withdraw : withdrawal -> {
    id : id
    balance : current
} -> when (current - withdrawal) {
    Some new -> {
        id : id
        balance : new -- use the new balance
    }
    None -> {
        id : id
        balance : current -- revert to the balance as it was before withdrawing
    }
}

my-account : open-account (Account-ID 123)
show (my-account . withdraw (Balance 50))
account #123: $0

Great, now our API is designed so it’s impossible to have a negative balance!

Here’s the full code for our bank account API, along with documentation comments:

-- An identifier for a bank account.
Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id

-- An amount of money stored in a bank account.
Balance : type Number

instance (Describe Balance) : (Balance dollars) -> "$_" dollars

instance (Add Balance Balance Balance) :
    (Balance current) (Balance amount) -> Balance (current + amount)

instance (Subtract Balance Balance (Maybe Balance)) :
    (Balance current) (Balance amount) ->
        if (amount <= current) {Some (Balance (current - amount))} {None}

-- A bank account.
Bank-Account : type {
    id :: Account-ID
    balance :: Balance
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: _" id balance

-- Open an account with the provided identifier.
open-account :: Account-ID -> Bank-Account
open-account : id -> {
    id : id
    balance : Balance 0
}

-- Deposit some money into a bank account.
deposit :: Balance -> Bank-Account -> Bank-Account
deposit : deposit -> {
    id : id
    balance : current
} -> {
    id : id
    balance : current + deposit
}

-- Attempt to withdraw some money from a bank account. If the account's balance
-- is too low, it will be left unchanged.
withdraw :: Balance -> Bank-Account -> Bank-Account
withdraw : withdrawal -> {
    id : id
    balance : current
} -> when (current - withdrawal) {
    Some new -> {
        id : id
        balance : new -- use the new balance
    }
    None -> {
        id : id
        balance : current -- revert to the balance as it was before withdrawing
    }
}