Modeling data
Wipple isn’t an object-oriented language, but you can work with data in a similar way using structures and functions. Let’s make a program that manages a bank account to explain how!
We’ll start by defining our Bank-Account
structure to hold a balance
:
Bank-Account : type {
balance :: Number
}
Next, we’ll define deposit
:
Bank-Account : type {
balance :: Number
}
deposit :: Bank-Account Number -> Bank-Account
deposit : {balance : balance} amount -> {balance : balance + amount}
For convenience, we’ll implement Describe
as well:
Bank-Account : type {
balance :: Number
}
instance (Describe Bank-Account) : {balance : balance} -> "$_" balance
Let’s try it out!
Bank-Account : type {
balance :: Number
}
instance (Describe Bank-Account) : {balance : balance} -> "$_" balance
deposit :: Bank-Account Number -> Bank-Account
deposit : {balance : balance} amount -> {balance : balance + amount}
my-account : Bank-Account {balance : 500}
show my-account
my-account : deposit my-account 100
show my-account
$500
$600
In Wipple, you generally create functions that return new values, rather than modifying the original. Doing it this way makes it easier to debug your code, because after you make a change, you still have the old value to compare against. In the example above, we don’t care about the old bank account after we make our deposit, so we just overwrite the my-account
variable.
There’s an even better way to write the bank account example: rather than making deposit
accept both the bank account and the amount at once, we can split the function into two. Let’s try it:
Bank-Account : type {
balance :: Number
}
deposit :: Number -> (Bank-Account -> Bank-Account)
deposit : amount -> ({balance : balance} -> {balance : balance + amount})
Here, we create a function that accepts the amount to deposit, and returns another function that does the actual work of updating the account. If this seems strange, keep reading!
We actually don’t need the parentheses around the second function, either — the arrow reads from right to left.
Bank-Account : type {
balance :: Number
}
deposit :: Number -> Bank-Account -> Bank-Account
deposit : amount -> {balance : balance} -> {balance : balance + amount}
Now, we can split the deposit operation into two steps:
Bank-Account : type {
balance :: Number
}
instance (Describe Bank-Account) : {balance : balance} -> "$_" balance
deposit :: Number -> Bank-Account -> Bank-Account
deposit : amount -> {balance : balance} -> {balance : balance + amount}
payday : deposit 100
my-account : Bank-Account {balance : 500}
show my-account
show (payday my-account)
$500
$600
First, we call deposit 100
. This returns another function that accepts a Bank-Account
and produces a new Bank-Account
with an additional $100. We give this function a name: payday
. Finally, we call payday
on my-account
, and receive the updated account.
This pattern is called currying, and it’s useful because it lets you separate the action — what you’re trying to do — from the logic — how the action is executed. Notice that payday
works independently of any specific bank account! The Wipple standard library uses currying in a lot of places, so it’s something you’ll get used to over time.
A good rule of thumb is to have the outer function accept the “data” inputs, and the inner function accept the “state” input. In object-oriented parlance, deposit(amount)
is like a method on a BankAccount
class.
One more thing: remember that functions can be used directly — you don’t need to give them a name. So if you have an action that’s difficult to name or is only used once, you can just call the inner function directly!
Bank-Account : type {
balance :: Number
}
instance (Describe Bank-Account) : {balance : balance} -> "$_" balance
deposit :: Number -> Bank-Account -> Bank-Account
deposit : amount -> {balance : balance} -> {balance : balance + amount}
my-account : Bank-Account {balance : 500}
show ((deposit 100) my-account)
$600
And since (deposit 100) my-account
returns another Bank-Account
, we can call deposit 200
on that:
Bank-Account : type {
balance :: Number
}
instance (Describe Bank-Account) : {balance : balance} -> "$_" balance
deposit :: Number -> Bank-Account -> Bank-Account
deposit : amount -> {balance : balance} -> {balance : balance + amount}
my-account : Bank-Account {balance : 500}
show ((deposit 200) ((deposit 100) my-account))
$800
This gets unwieldy quickly, though. Fortunately, Wipple has a dot operator (.
) for chaining function calls — .
calls the function on the right-hand side with the input on the left-hand side.
More formally:
a . f
is equivalent tof a
,a . f b c
is equivalent to(f b c) a
,a . f b . g c
is equivalent to(g c) ((f b) a)
,- and so on.
Bank-Account : type {
balance :: Number
}
instance (Describe Bank-Account) : {balance : balance} -> "$_" balance
deposit :: Number -> Bank-Account -> Bank-Account
deposit : amount -> {balance : balance} -> {balance : balance + amount}
my-account : Bank-Account {balance : 500}
show (my-account . deposit 100 . deposit 200)
$800
Although my-account . deposit 100
looks very similar to the myAccount.deposit(100)
method call syntax in object-oriented languages, it’s important to remember that they work differently! The result of calling deposit 100
is just a function, which you should give a name to if it makes things more clear.
When you’re reading code that uses the dot operator, it’s helpful to mentally group each piece of code between the dots and think about each action being performed. Then, once you understand the sequence of actions, go back to the beginning and read the input, so you know what those actions are being performed on. For example:
upgraded-car : car . swap engine . refill coolant . rotate tires
“First,
swap
theengine
. Then,refill
thecoolant
. Finally,rotate
thetires
. Perform this maintenance oncar
, resulting inupgraded-car
.”
In the next chapter, we’ll be using the dot operator a lot!