Reputation: 399
I am trying to make the following class immutable. I know the theory of how to do this but I think my implementation is wrong. Can you help?
Thanks
Mutable class:
class BankAccount {
private var balance = 0
def deposit(amount: Int) {
if (amount > 0)
balance += amount
}
def withdraw(amount: Int): Int =
if (0 < amount && amount <= balance) {
balance -= amount
balance
} else {
error("insufficient funds")
}
Immutable Class
case class BankAccount(b:Int) {
private def deposit(amount: Int):BankAccount {
if (amount > 0)
{
return BankAccount(amount)
}
}
private def withdraw(amount: Int): BankAccount ={
if (0 < amount && amount <= balance) {
return BankAccount(b-amount)
} else {
error("insufficient funds")
}
}
}
Upvotes: 6
Views: 4610
Reputation: 369604
First, the good news: your objects are almost immutable. Now, the bad news: they don't work.
The are only "almost" immutable because your class isn't final
: I can extend it and override the methods to mutate some state.
Now, why doesn't it work? The most obvious bug is that in your deposit
method, you return a new BankAccount
that has its balance set to the amount that was deposited. So, you lose all the money that was in there before the deposit! You need to add the deposit to the balance, not replace the balance with the deposit.
There are also other problems: your deposit
method has a return type of BankAccount
, but it doesn't always return a BankAccount
: if the amount
is less than or equal to zero, it returns Unit
. The most specific common supertype of BankAccount
and Unit
is Any
, so your method actually returns Any
. There are multiple ways to fix this, e.g. returning an Option[BankAccount]
, a Try[BankAccount]
, or an Either[SomeErrorType, BankAccount]
, or just throwing an exception. For my example, I'm simply going to ignore the validation altogether. (A similar problem exists in withdraw
.)
Something like this:
final case class BankAccount(balance: Int) {
private def deposit(amount: Int) = copy(balance = balance + amount)
private def withdraw(amount: Int) = copy(balance = balance - amount)
}
Note I am using the compiler-generated copy
method for case classes that allows you to create a copy of an instance with only one field changed. In your particular case, you have only one field, but it's a good practice to get into.
So, that works. Or … does it? Well, no, actually, it doesn't! The problem is that we are creating new bank accounts … with money in them … we are creating new money out of thin air! If I have 100 dollars in my account, I can withdraw 90 of them, and I get returned a new bank account object with 10 dollars in it. But I still have access to the old bank account object with 100 dollars in it! So, I have two bank accounts with a total of 110 dollars plus the 90 I withdrew; I now have 200 dollars!
Solving this is non-trivial, and I will leave it for now.
In closing, I wanted to show you something that is a little bit close to how real-world banking systems actually work, by which I both mean "banking systems in the real-world, as in, before the invention of electronic banking", as well as "electronic banking systems as they are actually used", because surprisingly (or not), they actually work the same.
In your system, the balance is data and depositing and withdrawing are operations. But in the real world, it's exactly the dual: deposits and withdrawals are data, and computing the balance is an operation. Before we hat computers, bank tellers would write transaction slips for every transaction, then those transaction slips would be collected at the end of the day, and all the money movements added up. And electronic banking systems work the same, roughly like this:
final case class TransactionSlip(source: BankAccount, destination: BankAccount, amount: BigDecimal)
final case class BankAccount {
def balance =
TransactionLog.filter(slip.destination == this).map(_.amount).reduce(_ + _) -
TransactionLog.filter(slip.source == this).map(_.amount).reduce(_ + _)
}
So, the individual transactions are recorded in a log, and the balance is computed by adding up the amount of all transactions that have the account as a destination and subtracting from that the sum of the amount of all transactions that have the account as a source. There are obviously a lot of implementation details I haven't shown you, e.g. how the transaction log works, and there should probably be some caching of the balance so that you don't need to compute it over and over again. Also, I ignored validation (which also requires computing the balance).
I added this example to show you that the same problem can be solved by very different designs, and that some designs lend themselves more naturally to a functional approach. Note that this second system is the way banking was done for decades, long before computers even existed, and it lends itself very naturally towards functional programming.
Upvotes: 20
Reputation: 14825
In functional programming you do not change the state in place, instead you create new state and return it.
Here is how your use case can be solved using functional programming.
case class BankAccount(val money: Int)
The above case class represents BankAccount
Instead of mutating the state, create new state with computed value and return it to the user.
def deposit(bankAccount: BankAccount, money: Int): BankAccount = {
BankAccount(money + backAccount.money)
}
In the same way, check for funds and create new state and return it to the user.
def withDraw(bankAccount: BankAccount, money: Int): BankAccount = {
if (money >= 0 && bankAccount.money >= money) {
BankAccount(bankAccount.money - money)
} else error("in sufficient funds")
}
In functional programming it is very common to create new state instead of trying to mutate the old state.
Create new state and return, thats it !!!
Upvotes: 10