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
}
}